cctx-cli 1.11.0__tar.gz → 1.12.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/CHANGELOG.md +64 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/PKG-INFO +1 -1
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/__init__.py +1 -1
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/cli.py +52 -1
- cctx_cli-1.12.0/cctx/parsers/otel.py +346 -0
- cctx_cli-1.12.0/docs/quickstart-openai-agents.md +106 -0
- cctx_cli-1.12.0/docs/superpowers/plans/2026-06-19-otel-parser.md +1215 -0
- cctx_cli-1.12.0/docs/superpowers/specs/2026-06-19-otel-parser-design.md +115 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/pyproject.toml +1 -1
- cctx_cli-1.12.0/tests/fixtures/otel_fanout.jsonl +1 -0
- cctx_cli-1.12.0/tests/fixtures/otel_handoff.jsonl +1 -0
- cctx_cli-1.12.0/tests/test_otel_parser.py +350 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/.github/workflows/ci.yml +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/.github/workflows/publish.yml +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/.github/workflows/release.yml +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/.gitignore +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/CLAUDE.md +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/DESIGN.md +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/PRODUCT.md +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/README.md +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/action.yml +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/agents.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/diagnostician/__init__.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/diagnostician/aggregate.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/diagnostician/inflection.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/diagnostician/patterns/__init__.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/diagnostician/patterns/dead_end.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/diagnostician/patterns/fan_out.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/diagnostician/patterns/project_specific.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/diagnostician/patterns/retry_loop.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/diagnostician/patterns/scope_creep.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/diagnostician/patterns/stale_context.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/diagnostician/patterns/tool_thrash.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/discovery.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/exporters/__init__.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/exporters/csv.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/exporters/json.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/exporters/jsonl.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/harvest.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/hook_installer.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/models.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/parsers/__init__.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/parsers/claude_code.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/pricing.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/recommender/__init__.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/recommender/claude_md.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/recommender/evidence.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/renderers/__init__.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/renderers/github.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/renderers/report.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/renderers/templates/autopsy.html.j2 +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/renderers/terminal.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/renderers/trace_tui.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/tokenizer.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx/watcher.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/cctx-project-brief.md +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/demo.gif +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/demo.tape +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/docs/health-reviews/2026-05-15-deep-review-summary.md +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/docs/health-reviews/2026-05-15-health-review.md +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/docs/product-reviews/2026-05-15-product-review.md +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/docs/product-reviews/2026-06-09-product-review.md +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/docs/superpowers/plans/2026-05-12-claude-code-parser.md +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/docs/superpowers/plans/2026-05-14-autopsy-v0.md +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/docs/superpowers/plans/2026-05-16-readme-pypi-release.md +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/docs/superpowers/plans/2026-05-17-harvest-check-depth.md +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/docs/superpowers/plans/2026-05-17-project-pattern-detection.md +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/docs/superpowers/plans/2026-05-19-claude-agents-live-integration.md +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/docs/superpowers/specs/2026-05-12-claude-code-parser-design.md +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/docs/superpowers/specs/2026-05-14-autopsy-design.md +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/docs/superpowers/specs/2026-05-14-harvest-design.md +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/docs/superpowers/specs/2026-05-14-trace-tui-design.md +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/docs/superpowers/specs/2026-05-16-readme-pypi-release-design.md +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/docs/superpowers/specs/2026-05-17-harvest-check-depth-design.md +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/docs/superpowers/specs/2026-05-17-project-pattern-detection-design.md +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/docs/superpowers/specs/2026-05-19-claude-agents-live-integration-design.md +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/docs/superpowers/specs/2026-06-09-cross-agent-emit-design.md +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/__init__.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/conftest.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/diagnostician/__init__.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/diagnostician/conftest.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/diagnostician/test_dead_end.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/diagnostician/test_inflection.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/diagnostician/test_orchestrator.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/diagnostician/test_project_specific.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/diagnostician/test_retry_loop.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/diagnostician/test_scope_creep.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/diagnostician/test_stale_context.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/diagnostician/test_tool_thrash.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/exporters/__init__.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/exporters/test_csv.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/exporters/test_jsonl.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/README.md +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/short-clean/short-clean.jsonl +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a0b4c2cf1dde0ca56.meta.json +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a116ae34b1b09c332.meta.json +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a1c4c417b35658c9e.meta.json +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a1e41a901de38f1b5.meta.json +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a338f8d0c74612a24.meta.json +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a34f6f3c0e7094186.meta.json +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a5a5a0cff4d13308b.meta.json +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a6b0a3da6a0484db5.meta.json +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a7f73f1790b02cde5.meta.json +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a7f7c17c38a9d8788.meta.json +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a853259e2cd7bbe8a.meta.json +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a8d9aedb0d0c6e12d.meta.json +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aa778bc1d59e4a441.meta.json +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aba869dedee4a12ba.meta.json +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-ada2746d9774b94db.meta.json +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aea0132068c64d2dd.meta.json +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aea215eff50874d5f.meta.json +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-afee21f2b3852a4a0.meta.json +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-attachments/with-attachments.jsonl +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a051d9c9a6b2f5cc3.jsonl +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a051d9c9a6b2f5cc3.meta.json +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a171f16f4e65cfe75.jsonl +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a171f16f4e65cfe75.meta.json +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a1b77fea2c0a2269b.jsonl +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a1b77fea2c0a2269b.meta.json +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a20da4c01a54acca8.jsonl +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a20da4c01a54acca8.meta.json +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a3c82739b1383fb14.jsonl +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a3c82739b1383fb14.meta.json +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a49e8539611c5fe12.jsonl +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a49e8539611c5fe12.meta.json +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a7bb58f3fff2b3e8d.jsonl +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a7bb58f3fff2b3e8d.meta.json +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a92b48c0331195aac.jsonl +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a92b48c0331195aac.meta.json +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ab96c4264099694a9.jsonl +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ab96c4264099694a9.meta.json +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-acb2895c5e34ffec0.jsonl +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-acb2895c5e34ffec0.meta.json +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-adb2302769938fb3f.jsonl +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-adb2302769938fb3f.meta.json +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ae585eca15cb93b9c.jsonl +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ae585eca15cb93b9c.meta.json +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-aec9c917feb903d67.jsonl +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-aec9c917feb903d67.meta.json +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-compaction/with-compaction.jsonl +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-a1a3a21aeb76bb0a9.jsonl +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-a1a3a21aeb76bb0a9.meta.json +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-aaa1d6ecc05a78442.jsonl +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-aaa1d6ecc05a78442.meta.json +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-af3c545ccd30036d2.jsonl +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-af3c545ccd30036d2.meta.json +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-subagents/with-subagents/tool-results/btwp2bzro.txt +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-subagents/with-subagents/tool-results/byqjbgy4b.txt +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-subagents/with-subagents.jsonl +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-tool-results/with-tool-results/tool-results/bosbkda0h.txt +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/claude_code/with-tool-results/with-tool-results.jsonl +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/scrub.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/synthetic/bookkeeping_only.jsonl +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/synthetic/malformed_middle.jsonl +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/synthetic/truncated_final_line.jsonl +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/synthetic/unknown_attachment_shape.jsonl +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/fixtures/synthetic/unknown_type.jsonl +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/parsers/__init__.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/parsers/test_claude_code.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/parsers/test_claude_code_integration.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/recommender/__init__.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/recommender/test_claude_md.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/recommender/test_evidence.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/renderers/__init__.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/renderers/test_report.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/renderers/test_terminal_renderer_full.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/test_agents.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/test_aggregate.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/test_cli.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/test_cli_export.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/test_diagnostician_subagents.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/test_discovery.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/test_efficacy.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/test_fanout_classifier.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/test_github_summary.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/test_harvest.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/test_harvest_check.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/test_harvest_emit.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/test_init.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/test_models.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/test_models_project_pattern.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/test_recommender.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/test_smoke.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/test_terminal_renderer.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/test_tokenizer.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/test_trace_tui.py +0 -0
- {cctx_cli-1.11.0 → cctx_cli-1.12.0}/tests/test_watcher.py +0 -0
|
@@ -2,6 +2,70 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- version list -->
|
|
4
4
|
|
|
5
|
+
## v1.12.0 (2026-06-20)
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
- Otel parser — per-trace warnings, multiple-root warning, full span list for subagent turns
|
|
10
|
+
([#116](https://github.com/jacquardlabs/cctx/pull/116),
|
|
11
|
+
[`a6b56b6`](https://github.com/jacquardlabs/cctx/commit/a6b56b61ccff5493dd26f375a698eb59aff62272))
|
|
12
|
+
|
|
13
|
+
- Ruff F401 + I001 in test_otel_parser.py ([#116](https://github.com/jacquardlabs/cctx/pull/116),
|
|
14
|
+
[`a6b56b6`](https://github.com/jacquardlabs/cctx/commit/a6b56b61ccff5493dd26f375a698eb59aff62272))
|
|
15
|
+
|
|
16
|
+
### Documentation
|
|
17
|
+
|
|
18
|
+
- OTEL parser design spec — OpenAI Agents SDK support via parsers/otel.py
|
|
19
|
+
([#116](https://github.com/jacquardlabs/cctx/pull/116),
|
|
20
|
+
[`a6b56b6`](https://github.com/jacquardlabs/cctx/commit/a6b56b61ccff5493dd26f375a698eb59aff62272))
|
|
21
|
+
|
|
22
|
+
- OTEL parser implementation plan ([#116](https://github.com/jacquardlabs/cctx/pull/116),
|
|
23
|
+
[`a6b56b6`](https://github.com/jacquardlabs/cctx/commit/a6b56b61ccff5493dd26f375a698eb59aff62272))
|
|
24
|
+
|
|
25
|
+
- Quickstart guide for OpenAI Agents SDK + cctx OTEL integration
|
|
26
|
+
([#116](https://github.com/jacquardlabs/cctx/pull/116),
|
|
27
|
+
[`a6b56b6`](https://github.com/jacquardlabs/cctx/commit/a6b56b61ccff5493dd26f375a698eb59aff62272))
|
|
28
|
+
|
|
29
|
+
### Features
|
|
30
|
+
|
|
31
|
+
- _detect_source() — auto-detect Claude Code vs OTEL trace format
|
|
32
|
+
([#116](https://github.com/jacquardlabs/cctx/pull/116),
|
|
33
|
+
[`a6b56b6`](https://github.com/jacquardlabs/cctx/commit/a6b56b61ccff5493dd26f375a698eb59aff62272))
|
|
34
|
+
|
|
35
|
+
- OpenAI Agents SDK support via OTEL parser ([#116](https://github.com/jacquardlabs/cctx/pull/116),
|
|
36
|
+
[`a6b56b6`](https://github.com/jacquardlabs/cctx/commit/a6b56b61ccff5493dd26f375a698eb59aff62272))
|
|
37
|
+
|
|
38
|
+
- OTEL parser skeleton — span loading and session trace construction
|
|
39
|
+
([#116](https://github.com/jacquardlabs/cctx/pull/116),
|
|
40
|
+
[`a6b56b6`](https://github.com/jacquardlabs/cctx/commit/a6b56b61ccff5493dd26f375a698eb59aff62272))
|
|
41
|
+
|
|
42
|
+
- Wire OTEL auto-detection into autopsy — cctx autopsy <otel.jsonl> just works
|
|
43
|
+
([#116](https://github.com/jacquardlabs/cctx/pull/116),
|
|
44
|
+
[`a6b56b6`](https://github.com/jacquardlabs/cctx/commit/a6b56b61ccff5493dd26f375a698eb59aff62272))
|
|
45
|
+
|
|
46
|
+
### Testing
|
|
47
|
+
|
|
48
|
+
- Add OTLP JSONL fixtures for otel parser (handoff + fanout)
|
|
49
|
+
([#116](https://github.com/jacquardlabs/cctx/pull/116),
|
|
50
|
+
[`a6b56b6`](https://github.com/jacquardlabs/cctx/commit/a6b56b61ccff5493dd26f375a698eb59aff62272))
|
|
51
|
+
|
|
52
|
+
- OTEL parser error handling — malformed JSON, unknown spans, empty file
|
|
53
|
+
([#116](https://github.com/jacquardlabs/cctx/pull/116),
|
|
54
|
+
[`a6b56b6`](https://github.com/jacquardlabs/cctx/commit/a6b56b61ccff5493dd26f375a698eb59aff62272))
|
|
55
|
+
|
|
56
|
+
- Verify child AgentSpan → subagents mapping (handoff + fan-out)
|
|
57
|
+
([#116](https://github.com/jacquardlabs/cctx/pull/116),
|
|
58
|
+
[`a6b56b6`](https://github.com/jacquardlabs/cctx/commit/a6b56b61ccff5493dd26f375a698eb59aff62272))
|
|
59
|
+
|
|
60
|
+
- Verify FunctionSpan → ToolUse + ToolResult mapping
|
|
61
|
+
([#116](https://github.com/jacquardlabs/cctx/pull/116),
|
|
62
|
+
[`a6b56b6`](https://github.com/jacquardlabs/cctx/commit/a6b56b61ccff5493dd26f375a698eb59aff62272))
|
|
63
|
+
|
|
64
|
+
- Verify GenerationSpan → Turn mapping for OTEL parser
|
|
65
|
+
([#116](https://github.com/jacquardlabs/cctx/pull/116),
|
|
66
|
+
[`a6b56b6`](https://github.com/jacquardlabs/cctx/commit/a6b56b61ccff5493dd26f375a698eb59aff62272))
|
|
67
|
+
|
|
68
|
+
|
|
5
69
|
## v1.11.0 (2026-06-11)
|
|
6
70
|
|
|
7
71
|
### Bug Fixes
|
|
@@ -13,6 +13,7 @@ Commands:
|
|
|
13
13
|
"""
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
|
+
import json as _json
|
|
16
17
|
from datetime import datetime, timedelta, timezone
|
|
17
18
|
from pathlib import Path
|
|
18
19
|
from typing import IO
|
|
@@ -179,6 +180,48 @@ def _render_check_findings(findings: list, target_dir: Path) -> None:
|
|
|
179
180
|
con.print(f" {badge:<6} {f.heading} {label}: {f.detail}")
|
|
180
181
|
|
|
181
182
|
|
|
183
|
+
_CLAUDE_CODE_LINE_TYPES = frozenset({
|
|
184
|
+
"user", "assistant", "system", "attachment",
|
|
185
|
+
"last-prompt", "permission-mode", "ai-title", "custom-title",
|
|
186
|
+
"queue-operation", "file-history-snapshot", "pr-link",
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _detect_source(path: Path) -> str:
|
|
191
|
+
"""Sniff first non-empty lines to detect trace format.
|
|
192
|
+
|
|
193
|
+
Returns "claude_code" or "otel".
|
|
194
|
+
Raises click.UsageError if the format cannot be determined.
|
|
195
|
+
"""
|
|
196
|
+
try:
|
|
197
|
+
with path.open(encoding="utf-8", errors="replace") as f:
|
|
198
|
+
for _ in range(5):
|
|
199
|
+
line = f.readline()
|
|
200
|
+
if not line:
|
|
201
|
+
break
|
|
202
|
+
line = line.strip()
|
|
203
|
+
if not line:
|
|
204
|
+
continue
|
|
205
|
+
try:
|
|
206
|
+
obj = _json.loads(line)
|
|
207
|
+
except _json.JSONDecodeError:
|
|
208
|
+
continue
|
|
209
|
+
if "resourceSpans" in obj:
|
|
210
|
+
return "otel"
|
|
211
|
+
if "traceId" in obj and "spanId" in obj:
|
|
212
|
+
return "otel"
|
|
213
|
+
line_type = obj.get("type")
|
|
214
|
+
if isinstance(line_type, str) and line_type in _CLAUDE_CODE_LINE_TYPES:
|
|
215
|
+
return "claude_code"
|
|
216
|
+
except OSError as exc:
|
|
217
|
+
raise click.UsageError(f"Cannot read file: {path}: {exc}") from exc
|
|
218
|
+
|
|
219
|
+
raise click.UsageError(
|
|
220
|
+
f"Cannot determine trace format for {path}.\n"
|
|
221
|
+
"Expected a Claude Code JSONL session file or an OTLP JSON trace export."
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
|
|
182
225
|
@click.group()
|
|
183
226
|
def cli() -> None:
|
|
184
227
|
"""cctx — find out why your Claude Code session went sideways."""
|
|
@@ -415,7 +458,15 @@ def autopsy(
|
|
|
415
458
|
"TARGET is a directory. Use --since N for cross-session mode, "
|
|
416
459
|
"or pass a .jsonl file directly."
|
|
417
460
|
)
|
|
418
|
-
|
|
461
|
+
source = _detect_source(target)
|
|
462
|
+
if source == "otel":
|
|
463
|
+
from cctx.parsers.otel import parse_otel_file as _parse_otel_file
|
|
464
|
+
otel_traces = _parse_otel_file(target)
|
|
465
|
+
if not otel_traces:
|
|
466
|
+
raise click.UsageError(f"No traces found in {target}")
|
|
467
|
+
trace = tokenize_session(otel_traces[0])
|
|
468
|
+
else:
|
|
469
|
+
trace = tokenize_session(parse_session(target))
|
|
419
470
|
diagnosis = diagnostician.run(trace)
|
|
420
471
|
diagnosis = claude_md.generate(diagnosis)
|
|
421
472
|
if quiet:
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
"""OTLP JSONL parser for OpenAI Agents SDK traces.
|
|
2
|
+
|
|
3
|
+
Public API:
|
|
4
|
+
parse_otel_file(path: Path) -> list[SessionTrace]
|
|
5
|
+
|
|
6
|
+
Reads OTLP JSON (one ResourceSpans batch per line). Maps spans to the
|
|
7
|
+
cctx canonical model. No imports from anthropic, click, or other cctx
|
|
8
|
+
modules except cctx.models.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
from collections import defaultdict
|
|
14
|
+
from datetime import datetime, timezone
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from cctx.models import (
|
|
18
|
+
ParserWarning,
|
|
19
|
+
SessionTrace,
|
|
20
|
+
ToolResult,
|
|
21
|
+
ToolUse,
|
|
22
|
+
Turn,
|
|
23
|
+
Usage,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def parse_otel_file(path: Path) -> list[SessionTrace]:
|
|
28
|
+
"""Read an OTLP JSONL export; return one SessionTrace per trace_id."""
|
|
29
|
+
path = Path(path)
|
|
30
|
+
load_warnings: list[ParserWarning] = []
|
|
31
|
+
spans = _load_spans(path, load_warnings)
|
|
32
|
+
|
|
33
|
+
by_trace: dict[str, list[dict]] = defaultdict(list)
|
|
34
|
+
for span in spans:
|
|
35
|
+
trace_id = span.get("traceId", "")
|
|
36
|
+
if trace_id:
|
|
37
|
+
by_trace[trace_id].append(span)
|
|
38
|
+
|
|
39
|
+
traces = []
|
|
40
|
+
for trace_id, trace_spans in by_trace.items():
|
|
41
|
+
# Each trace gets its own warnings list; load-time warnings are copied in.
|
|
42
|
+
trace_warnings = list(load_warnings)
|
|
43
|
+
traces.append(_build_session_trace(trace_id, trace_spans, path, trace_warnings))
|
|
44
|
+
return traces
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _load_spans(path: Path, warnings: list[ParserWarning]) -> list[dict]:
|
|
48
|
+
"""Load all spans from OTLP JSONL. One ResourceSpans JSON object per line."""
|
|
49
|
+
spans: list[dict] = []
|
|
50
|
+
try:
|
|
51
|
+
with path.open(encoding="utf-8", errors="replace") as f:
|
|
52
|
+
for lineno, line in enumerate(f, 1):
|
|
53
|
+
line = line.strip()
|
|
54
|
+
if not line:
|
|
55
|
+
continue
|
|
56
|
+
try:
|
|
57
|
+
obj = json.loads(line)
|
|
58
|
+
except json.JSONDecodeError:
|
|
59
|
+
warnings.append(
|
|
60
|
+
ParserWarning(
|
|
61
|
+
code="malformed_json",
|
|
62
|
+
detail=f"skipped malformed JSON on line {lineno}",
|
|
63
|
+
line_number=lineno,
|
|
64
|
+
path=path,
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
continue
|
|
68
|
+
for rs in obj.get("resourceSpans", []):
|
|
69
|
+
for ss in rs.get("scopeSpans", []):
|
|
70
|
+
for span in ss.get("spans", []):
|
|
71
|
+
span["_resource"] = rs.get("resource", {})
|
|
72
|
+
spans.append(span)
|
|
73
|
+
except OSError as exc:
|
|
74
|
+
warnings.append(
|
|
75
|
+
ParserWarning(code="read_error", detail=str(exc), path=path)
|
|
76
|
+
)
|
|
77
|
+
return spans
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _build_session_trace(
|
|
81
|
+
trace_id: str,
|
|
82
|
+
spans: list[dict],
|
|
83
|
+
source_path: Path,
|
|
84
|
+
warnings: list[ParserWarning],
|
|
85
|
+
) -> SessionTrace:
|
|
86
|
+
root = _find_root_agent_span(spans, warnings)
|
|
87
|
+
if root is None:
|
|
88
|
+
root = {}
|
|
89
|
+
|
|
90
|
+
turns = _build_turns(root, spans)
|
|
91
|
+
subagents = _build_subagents(root, spans, source_path, warnings)
|
|
92
|
+
primary_model = _primary_model(spans)
|
|
93
|
+
cwd = _attr_str(root, "process.cwd") or ""
|
|
94
|
+
|
|
95
|
+
all_start_times = [
|
|
96
|
+
_nano_to_dt(s.get("startTimeUnixNano"))
|
|
97
|
+
for s in spans
|
|
98
|
+
if s.get("startTimeUnixNano")
|
|
99
|
+
]
|
|
100
|
+
start_time = min((t for t in all_start_times if t), default=None)
|
|
101
|
+
all_end_times = [
|
|
102
|
+
_nano_to_dt(s.get("endTimeUnixNano"))
|
|
103
|
+
for s in spans
|
|
104
|
+
if s.get("endTimeUnixNano")
|
|
105
|
+
]
|
|
106
|
+
end_time = max((t for t in all_end_times if t), default=None)
|
|
107
|
+
|
|
108
|
+
tool_names: list[str] = list({
|
|
109
|
+
name
|
|
110
|
+
for s in spans
|
|
111
|
+
if _attr_str(s, "gen_ai.operation.name") == "invoke_function"
|
|
112
|
+
if (name := _attr_str(s, "gen_ai.tool.name"))
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
return SessionTrace(
|
|
116
|
+
session_id=trace_id,
|
|
117
|
+
parent_session_id=None,
|
|
118
|
+
project_path="",
|
|
119
|
+
cwd=cwd,
|
|
120
|
+
primary_model=primary_model,
|
|
121
|
+
claude_code_version=None,
|
|
122
|
+
turns=turns,
|
|
123
|
+
subagents=subagents,
|
|
124
|
+
attachments=[],
|
|
125
|
+
raw_tool_result_files=[],
|
|
126
|
+
initial_context_tokens=0,
|
|
127
|
+
tool_names_loaded=tool_names,
|
|
128
|
+
start_time=start_time,
|
|
129
|
+
end_time=end_time,
|
|
130
|
+
source_path=source_path,
|
|
131
|
+
subagent_meta={},
|
|
132
|
+
warnings=warnings,
|
|
133
|
+
subagent_parse_errors=[],
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# ---------------------------------------------------------------------------
|
|
138
|
+
# Span tree helpers
|
|
139
|
+
# ---------------------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _find_root_agent_span(
|
|
143
|
+
spans: list[dict], warnings: list[ParserWarning]
|
|
144
|
+
) -> dict | None:
|
|
145
|
+
roots = [
|
|
146
|
+
s for s in spans
|
|
147
|
+
if _attr_str(s, "gen_ai.operation.name") == "run_agent"
|
|
148
|
+
and not s.get("parentSpanId")
|
|
149
|
+
]
|
|
150
|
+
if not roots:
|
|
151
|
+
warnings.append(
|
|
152
|
+
ParserWarning(code="no_root_agent_span", detail="no root AgentSpan found")
|
|
153
|
+
)
|
|
154
|
+
return None
|
|
155
|
+
if len(roots) > 1:
|
|
156
|
+
span_ids = ", ".join(r.get("spanId", "?") for r in roots)
|
|
157
|
+
warnings.append(
|
|
158
|
+
ParserWarning(
|
|
159
|
+
code="multiple_root_agent_spans",
|
|
160
|
+
detail=f"found {len(roots)} root AgentSpans; using first: {span_ids}",
|
|
161
|
+
)
|
|
162
|
+
)
|
|
163
|
+
return roots[0]
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _build_turns(agent_span: dict, all_spans: list[dict]) -> list[Turn]:
|
|
167
|
+
"""Build assistant turns from GenerationSpans that are direct children of agent_span."""
|
|
168
|
+
if not agent_span:
|
|
169
|
+
return []
|
|
170
|
+
agent_id = agent_span.get("spanId", "")
|
|
171
|
+
gen_spans = [
|
|
172
|
+
s for s in all_spans
|
|
173
|
+
if s.get("parentSpanId") == agent_id
|
|
174
|
+
and _attr_str(s, "gen_ai.operation.name") == "chat"
|
|
175
|
+
]
|
|
176
|
+
gen_spans.sort(key=lambda s: int(s.get("startTimeUnixNano") or 0))
|
|
177
|
+
|
|
178
|
+
turns: list[Turn] = []
|
|
179
|
+
for i, gs in enumerate(gen_spans, 1):
|
|
180
|
+
tool_uses, tool_results = _build_tool_uses(gs, all_spans)
|
|
181
|
+
usage = Usage(
|
|
182
|
+
input_tokens=_attr_int(gs, "gen_ai.usage.input_tokens") or 0,
|
|
183
|
+
output_tokens=_attr_int(gs, "gen_ai.usage.output_tokens") or 0,
|
|
184
|
+
cache_creation_5m=0,
|
|
185
|
+
cache_creation_1h=0,
|
|
186
|
+
cache_read=0,
|
|
187
|
+
service_tier=None,
|
|
188
|
+
)
|
|
189
|
+
ts = _nano_to_dt(gs.get("startTimeUnixNano")) or datetime.now(timezone.utc)
|
|
190
|
+
end_ns = gs.get("endTimeUnixNano")
|
|
191
|
+
start_ns = gs.get("startTimeUnixNano")
|
|
192
|
+
duration_ms: int | None = None
|
|
193
|
+
if end_ns and start_ns:
|
|
194
|
+
duration_ms = int((int(end_ns) - int(start_ns)) / 1_000_000)
|
|
195
|
+
|
|
196
|
+
turns.append(
|
|
197
|
+
Turn(
|
|
198
|
+
turn_number=i,
|
|
199
|
+
uuid=gs.get("spanId", ""),
|
|
200
|
+
parent_uuid=agent_id or None,
|
|
201
|
+
role="assistant",
|
|
202
|
+
text=_attr_str(gs, "gen_ai.output.text") or "",
|
|
203
|
+
thinking="",
|
|
204
|
+
tool_uses=tool_uses,
|
|
205
|
+
tool_results=tool_results,
|
|
206
|
+
usage=usage,
|
|
207
|
+
model=_attr_str(gs, "gen_ai.request.model"),
|
|
208
|
+
stop_reason="end_turn",
|
|
209
|
+
timestamp=ts,
|
|
210
|
+
duration_ms=duration_ms,
|
|
211
|
+
)
|
|
212
|
+
)
|
|
213
|
+
return turns
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _build_tool_uses(
|
|
217
|
+
gen_span: dict, all_spans: list[dict]
|
|
218
|
+
) -> tuple[list[ToolUse], list[ToolResult]]:
|
|
219
|
+
"""Extract ToolUse + ToolResult from FunctionSpan children of a GenerationSpan."""
|
|
220
|
+
gen_id = gen_span.get("spanId", "")
|
|
221
|
+
func_spans = [
|
|
222
|
+
s for s in all_spans
|
|
223
|
+
if s.get("parentSpanId") == gen_id
|
|
224
|
+
and _attr_str(s, "gen_ai.operation.name") == "invoke_function"
|
|
225
|
+
]
|
|
226
|
+
func_spans.sort(key=lambda s: int(s.get("startTimeUnixNano") or 0))
|
|
227
|
+
|
|
228
|
+
tool_uses: list[ToolUse] = []
|
|
229
|
+
tool_results: list[ToolResult] = []
|
|
230
|
+
for fs in func_spans:
|
|
231
|
+
tool_name = _attr_str(fs, "gen_ai.tool.name") or "unknown"
|
|
232
|
+
call_id = _attr_str(fs, "gen_ai.tool.call.id") or fs.get("spanId", "")
|
|
233
|
+
result_str = _attr_str(fs, "gen_ai.tool.call.result") or ""
|
|
234
|
+
is_error = fs.get("status", {}).get("code", 1) == 2 # OTEL STATUS_ERROR = 2
|
|
235
|
+
|
|
236
|
+
tool_uses.append(ToolUse(tool_name=tool_name, tool_use_id=call_id, tool_input={}))
|
|
237
|
+
tool_results.append(
|
|
238
|
+
ToolResult(
|
|
239
|
+
tool_name=tool_name,
|
|
240
|
+
tool_use_id=call_id,
|
|
241
|
+
content=result_str,
|
|
242
|
+
structured=None,
|
|
243
|
+
is_error=is_error,
|
|
244
|
+
)
|
|
245
|
+
)
|
|
246
|
+
return tool_uses, tool_results
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _build_subagents(
|
|
250
|
+
root_span: dict,
|
|
251
|
+
all_spans: list[dict],
|
|
252
|
+
source_path: Path,
|
|
253
|
+
warnings: list[ParserWarning],
|
|
254
|
+
) -> list[SessionTrace]:
|
|
255
|
+
"""Build child SessionTraces from child AgentSpans (handoffs + parallel sub-agents)."""
|
|
256
|
+
if not root_span:
|
|
257
|
+
return []
|
|
258
|
+
root_id = root_span.get("spanId", "")
|
|
259
|
+
child_agent_spans = [
|
|
260
|
+
s for s in all_spans
|
|
261
|
+
if s.get("parentSpanId") == root_id
|
|
262
|
+
and _attr_str(s, "gen_ai.operation.name") == "run_agent"
|
|
263
|
+
]
|
|
264
|
+
child_agent_spans.sort(key=lambda s: int(s.get("startTimeUnixNano") or 0))
|
|
265
|
+
|
|
266
|
+
subagents: list[SessionTrace] = []
|
|
267
|
+
for child in child_agent_spans:
|
|
268
|
+
child_id = child.get("spanId", "")
|
|
269
|
+
# child_spans: direct children only (used for primary_model / time bounds).
|
|
270
|
+
# _build_turns receives the full all_spans list so _build_tool_uses can
|
|
271
|
+
# locate FunctionSpan grandchildren regardless of nesting depth.
|
|
272
|
+
child_spans = [s for s in all_spans if s.get("parentSpanId") == child_id]
|
|
273
|
+
agent_name = _attr_str(child, "gen_ai.agent.name") or child_id
|
|
274
|
+
child_turns = _build_turns(child, all_spans)
|
|
275
|
+
child_start = _nano_to_dt(child.get("startTimeUnixNano"))
|
|
276
|
+
child_end = _nano_to_dt(child.get("endTimeUnixNano"))
|
|
277
|
+
|
|
278
|
+
subagents.append(
|
|
279
|
+
SessionTrace(
|
|
280
|
+
session_id=child_id,
|
|
281
|
+
parent_session_id=root_id or None,
|
|
282
|
+
project_path="",
|
|
283
|
+
cwd="",
|
|
284
|
+
primary_model=_primary_model(child_spans),
|
|
285
|
+
claude_code_version=None,
|
|
286
|
+
turns=child_turns,
|
|
287
|
+
subagents=[],
|
|
288
|
+
attachments=[],
|
|
289
|
+
raw_tool_result_files=[],
|
|
290
|
+
initial_context_tokens=0,
|
|
291
|
+
tool_names_loaded=[],
|
|
292
|
+
start_time=child_start,
|
|
293
|
+
end_time=child_end,
|
|
294
|
+
source_path=source_path,
|
|
295
|
+
subagent_meta={"agent_name": agent_name},
|
|
296
|
+
warnings=[],
|
|
297
|
+
subagent_parse_errors=[],
|
|
298
|
+
)
|
|
299
|
+
)
|
|
300
|
+
return subagents
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
# ---------------------------------------------------------------------------
|
|
304
|
+
# Attribute + time helpers
|
|
305
|
+
# ---------------------------------------------------------------------------
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _primary_model(spans: list[dict]) -> str | None:
|
|
309
|
+
models = [
|
|
310
|
+
m for s in spans
|
|
311
|
+
if _attr_str(s, "gen_ai.operation.name") == "chat"
|
|
312
|
+
if (m := _attr_str(s, "gen_ai.request.model"))
|
|
313
|
+
]
|
|
314
|
+
if not models:
|
|
315
|
+
return None
|
|
316
|
+
return max(set(models), key=models.count)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _attr_str(span: dict, key: str) -> str | None:
|
|
320
|
+
for attr in span.get("attributes", []):
|
|
321
|
+
if attr.get("key") == key:
|
|
322
|
+
return attr.get("value", {}).get("stringValue")
|
|
323
|
+
return None
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _attr_int(span: dict, key: str) -> int | None:
|
|
327
|
+
for attr in span.get("attributes", []):
|
|
328
|
+
if attr.get("key") == key:
|
|
329
|
+
val = attr.get("value", {})
|
|
330
|
+
raw = val.get("intValue") or val.get("doubleValue")
|
|
331
|
+
if raw is not None:
|
|
332
|
+
try:
|
|
333
|
+
return int(float(raw))
|
|
334
|
+
except (ValueError, TypeError):
|
|
335
|
+
return None
|
|
336
|
+
return None
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _nano_to_dt(nano: str | int | None) -> datetime | None:
|
|
340
|
+
if nano is None:
|
|
341
|
+
return None
|
|
342
|
+
try:
|
|
343
|
+
ns = int(nano)
|
|
344
|
+
return datetime.fromtimestamp(ns / 1e9, tz=timezone.utc)
|
|
345
|
+
except (ValueError, TypeError, OSError):
|
|
346
|
+
return None
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Using cctx with the OpenAI Agents SDK
|
|
2
|
+
|
|
3
|
+
cctx diagnoses OpenAI Agents SDK runs the same way it diagnoses Claude Code sessions — point it at a trace file and get an autopsy.
|
|
4
|
+
|
|
5
|
+
## 1. Install dependencies
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install cctx opentelemetry-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## 2. Export traces to a local file
|
|
12
|
+
|
|
13
|
+
Add this to your agent script before running your agent. It configures OpenTelemetry to write spans to a local JSONL file.
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
import json
|
|
17
|
+
from typing import Sequence
|
|
18
|
+
|
|
19
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
20
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor, SpanExporter, SpanExportResult
|
|
21
|
+
from opentelemetry.sdk.trace.export import ReadableSpan
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class FileSpanExporter(SpanExporter):
|
|
25
|
+
"""Writes OTLP-style JSON batches to a local file, one line per flush."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, path: str) -> None:
|
|
28
|
+
self._path = path
|
|
29
|
+
|
|
30
|
+
def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult:
|
|
31
|
+
batch = {
|
|
32
|
+
"resourceSpans": [{
|
|
33
|
+
"resource": {"attributes": []},
|
|
34
|
+
"scopeSpans": [{
|
|
35
|
+
"scope": {"name": "openai.agents"},
|
|
36
|
+
"spans": [self._span_to_dict(s) for s in spans],
|
|
37
|
+
}],
|
|
38
|
+
}]
|
|
39
|
+
}
|
|
40
|
+
with open(self._path, "a", encoding="utf-8") as f:
|
|
41
|
+
f.write(json.dumps(batch) + "\n")
|
|
42
|
+
return SpanExportResult.SUCCESS
|
|
43
|
+
|
|
44
|
+
def shutdown(self) -> None:
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
@staticmethod
|
|
48
|
+
def _span_to_dict(span: ReadableSpan) -> dict:
|
|
49
|
+
ctx = span.context
|
|
50
|
+
attrs = [
|
|
51
|
+
{"key": k, "value": _otlp_value(v)}
|
|
52
|
+
for k, v in (span.attributes or {}).items()
|
|
53
|
+
]
|
|
54
|
+
d: dict = {
|
|
55
|
+
"traceId": format(ctx.trace_id, "032x"),
|
|
56
|
+
"spanId": format(ctx.span_id, "016x"),
|
|
57
|
+
"name": span.name,
|
|
58
|
+
"kind": int(span.kind),
|
|
59
|
+
"startTimeUnixNano": str(span.start_time),
|
|
60
|
+
"endTimeUnixNano": str(span.end_time),
|
|
61
|
+
"attributes": attrs,
|
|
62
|
+
"status": {"code": span.status.status_code.value},
|
|
63
|
+
}
|
|
64
|
+
if span.parent is not None:
|
|
65
|
+
d["parentSpanId"] = format(span.parent.span_id, "016x")
|
|
66
|
+
return d
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _otlp_value(v: object) -> dict:
|
|
70
|
+
if isinstance(v, bool):
|
|
71
|
+
return {"boolValue": v}
|
|
72
|
+
if isinstance(v, int):
|
|
73
|
+
return {"intValue": str(v)}
|
|
74
|
+
if isinstance(v, float):
|
|
75
|
+
return {"doubleValue": v}
|
|
76
|
+
return {"stringValue": str(v)}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
provider = TracerProvider()
|
|
80
|
+
provider.add_span_processor(BatchSpanProcessor(FileSpanExporter("agent_trace.jsonl")))
|
|
81
|
+
|
|
82
|
+
# Wire into the OpenAI Agents SDK — exact API depends on your SDK version:
|
|
83
|
+
# from agents import set_trace_processors
|
|
84
|
+
# set_trace_processors([...])
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## 3. Run your agent
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
python my_agent.py
|
|
91
|
+
# agent_trace.jsonl is written
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## 4. Diagnose the run
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
cctx autopsy agent_trace.jsonl
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`cctx autopsy` picks up the OTLP format automatically — no flags needed.
|
|
101
|
+
|
|
102
|
+
## Notes
|
|
103
|
+
|
|
104
|
+
- If your trace file contains multiple runs, cctx diagnoses the first trace by trace ID.
|
|
105
|
+
- Token costs shown in the autopsy are estimates; cctx does not call the OpenAI API.
|
|
106
|
+
- The exact `set_trace_processors` API varies by SDK version — check the `openai-agents` changelog if the import above doesn't work.
|