tracefork 0.2.0__tar.gz → 0.2.1__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.
- {tracefork-0.2.0 → tracefork-0.2.1}/CHANGELOG.md +40 -1
- {tracefork-0.2.0 → tracefork-0.2.1}/CLAUDE.md +14 -9
- {tracefork-0.2.0 → tracefork-0.2.1}/PKG-INFO +55 -8
- {tracefork-0.2.0 → tracefork-0.2.1}/README.md +40 -3
- tracefork-0.2.1/docs/packaging-decisions.md +170 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/pyproject.toml +38 -10
- {tracefork-0.2.0 → tracefork-0.2.1}/src/tracefork/__init__.py +12 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/src/tracefork/adapters/__init__.py +19 -3
- tracefork-0.2.1/src/tracefork/adapters/adk.py +465 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/src/tracefork/adapters/autogen.py +11 -3
- {tracefork-0.2.0 → tracefork-0.2.1}/src/tracefork/adapters/crewai.py +11 -3
- {tracefork-0.2.0 → tracefork-0.2.1}/src/tracefork/adapters/langchain.py +22 -6
- {tracefork-0.2.0 → tracefork-0.2.1}/src/tracefork/adapters/openai_agents.py +11 -3
- tracefork-0.2.1/tests/test_adapters_adk.py +305 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/tests/test_adapters_autogen.py +2 -1
- {tracefork-0.2.0 → tracefork-0.2.1}/tests/test_adapters_crewai.py +2 -1
- {tracefork-0.2.0 → tracefork-0.2.1}/tests/test_adapters_langchain.py +4 -2
- {tracefork-0.2.0 → tracefork-0.2.1}/tests/test_adapters_openai_agents.py +2 -1
- {tracefork-0.2.0 → tracefork-0.2.1}/uv.lock +141 -6
- {tracefork-0.2.0 → tracefork-0.2.1}/.editorconfig +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/.github/dependabot.yml +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/.github/pull_request_template.md +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/.github/workflows/ci.yml +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/.github/workflows/release.yml +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/.gitignore +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/.pre-commit-config.yaml +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/CODE_OF_CONDUCT.md +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/CONTRIBUTING.md +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/LICENSE +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/SECURITY.md +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/SPIKE0.md +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/docs/demo.png +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/examples/demo_report.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/experiments/replay_fixtures/manifest.json +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/experiments/replay_fixtures/single_turn.tape.sqlite +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/experiments/replay_fixtures/two_turn.tape.sqlite +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/experiments/validation_report_committed.json +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/scripts/e2e.sh +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/scripts/gen_replay_fixtures.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/src/tracefork/adapters/base.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/src/tracefork/bedrock_transport.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/src/tracefork/bench.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/src/tracefork/blame.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/src/tracefork/boundary_guard.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/src/tracefork/cli.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/src/tracefork/competing_faults.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/src/tracefork/config.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/src/tracefork/constants.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/src/tracefork/data/pricing.json +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/src/tracefork/divergence.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/src/tracefork/eventstream.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/src/tracefork/faults.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/src/tracefork/fixtures.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/src/tracefork/fork.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/src/tracefork/interop.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/src/tracefork/judge.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/src/tracefork/matcher.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/src/tracefork/mcp_client.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/src/tracefork/nondet.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/src/tracefork/observability.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/src/tracefork/plugins.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/src/tracefork/pricing.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/src/tracefork/providers/__init__.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/src/tracefork/providers/anthropic.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/src/tracefork/providers/base.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/src/tracefork/providers/bedrock.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/src/tracefork/providers/gemini.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/src/tracefork/providers/openai.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/src/tracefork/proxy.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/src/tracefork/py.typed +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/src/tracefork/record_mode.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/src/tracefork/recorder.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/src/tracefork/redact.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/src/tracefork/replay.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/src/tracefork/report.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/src/tracefork/server.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/src/tracefork/store.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/src/tracefork/synthetic.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/src/tracefork/tape.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/src/tracefork/tools.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/src/tracefork/transport.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/src/tracefork/validate.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/src/tracefork/wire.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/src/tracefork_spike/__init__.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/src/tracefork_spike/__main__.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/src/tracefork_spike/agent.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/src/tracefork_spike/fake_llm.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/src/tracefork_spike/nondet.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/src/tracefork_spike/spike.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/src/tracefork_spike/tape.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/src/tracefork_spike/transport.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/tests/__init__.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/tests/fakes.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/tests/fixtures/legacy_tape_v1.blob +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/tests/test_adapters_base.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/tests/test_bedrock_eventstream.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/tests/test_bedrock_provider.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/tests/test_bedrock_transport.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/tests/test_bench.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/tests/test_blame.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/tests/test_boundary_guard.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/tests/test_cli.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/tests/test_cli_smoke.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/tests/test_competing_faults.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/tests/test_concurrency.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/tests/test_config.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/tests/test_divergence.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/tests/test_e2e.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/tests/test_faults.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/tests/test_fork.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/tests/test_gemini_provider.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/tests/test_interop.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/tests/test_judge.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/tests/test_matcher.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/tests/test_mcp_client.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/tests/test_nondet.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/tests/test_observability.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/tests/test_openai_provider.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/tests/test_plugins.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/tests/test_pricing.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/tests/test_provider_faults.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/tests/test_providers.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/tests/test_proxy.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/tests/test_record_mode.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/tests/test_recorder.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/tests/test_redact.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/tests/test_replay.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/tests/test_report.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/tests/test_spike0.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/tests/test_storage.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/tests/test_tape.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/tests/test_tools.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/tests/test_transport.py +0 -0
- {tracefork-0.2.0 → tracefork-0.2.1}/web/report.html +0 -0
|
@@ -7,6 +7,44 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.1] - 2026-07-03
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Google ADK adapter** (`adapters/adk.py`, opt-in `adk` extra) — `bind()` routes
|
|
15
|
+
an ADK `LlmAgent`/`Gemini`'s underlying `google-genai` client through the
|
|
16
|
+
existing `TraceforkTransport` via a short candidate-path search for the
|
|
17
|
+
`google.genai` `BaseApiClient`'s private `_httpx_client`/`_async_httpx_client`
|
|
18
|
+
attributes (the target itself, a `genai.Client`, an ADK `Gemini` model wrapper,
|
|
19
|
+
or an `LlmAgent` whose `.model` already holds one) — the same Gemini
|
|
20
|
+
`generateContent` wire format `providers/gemini.py` already parses. Step
|
|
21
|
+
visibility is a real `BasePlugin` (`make_plugin()`) over ADK's documented
|
|
22
|
+
agent/model/tool before/after callback boundaries, registered once on the
|
|
23
|
+
`Runner` rather than threaded through every agent — observer-only, never a
|
|
24
|
+
second capture path.
|
|
25
|
+
- **Curated `all` extra** (`pip install 'tracefork[all]'`) — a self-referential
|
|
26
|
+
convenience bundle over `providers` + `bedrock` + `mcp` + `observability` (the
|
|
27
|
+
internally-consistent, stable-wire family). Deliberately excludes the five
|
|
28
|
+
independently-capped, fast-moving framework stacks (`frameworks`,
|
|
29
|
+
`openai-agents`, `crewai`, `autogen`, `adk`) so one future cap collision on any
|
|
30
|
+
single framework can't `ResolutionImpossible` the whole `all` install.
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- **Relaxed the `frameworks` extra's version caps to floors** — dropped the
|
|
35
|
+
speculative `<2` upper bounds on `langchain-core`/`langchain-openai`/
|
|
36
|
+
`langchain-anthropic`/`langgraph`. LangChain 1.0 GA commits to no breaking
|
|
37
|
+
changes until 2.0, so a `<2` cap was speculative and, more importantly, useless
|
|
38
|
+
against the real observed failure mode: intra-1.x patch regressions ship
|
|
39
|
+
*inside* the allowed range regardless of the cap. The remaining internal-
|
|
40
|
+
coupling caps (`openai-agents`, `crewai`, `autogen`, `adk` — each an adapter
|
|
41
|
+
injecting into private/undocumented framework internals) are kept, each now
|
|
42
|
+
with an inline "hint, not a guard" rationale and a revisit TODO.
|
|
43
|
+
- **Adapter import guards now chain the root cause** (`raise ImportError(HINT)
|
|
44
|
+
from exc`) in `adapters/{langchain,openai_agents,crewai,autogen,adk}.py`'s
|
|
45
|
+
`require_*()` functions, so an installed-but-broken dependency's real
|
|
46
|
+
`ImportError` is preserved instead of being masked as "not installed".
|
|
47
|
+
|
|
10
48
|
## [0.2.0] - 2026-07-02
|
|
11
49
|
|
|
12
50
|
Generic, multi-provider, production-hardening release. The bit-exact, $0,
|
|
@@ -119,6 +157,7 @@ everything below is additive around it: all engine internals stay byte-stable,
|
|
|
119
157
|
- `src/tracefork_spike/` — the original Spike 0 that de-risked bit-exact, no-key replay
|
|
120
158
|
within the declared determinism boundary.
|
|
121
159
|
|
|
122
|
-
[Unreleased]: https://github.com/pratik916/tracefork/compare/v0.2.
|
|
160
|
+
[Unreleased]: https://github.com/pratik916/tracefork/compare/v0.2.1...HEAD
|
|
161
|
+
[0.2.1]: https://github.com/pratik916/tracefork/compare/v0.2.0...v0.2.1
|
|
123
162
|
[0.2.0]: https://github.com/pratik916/tracefork/compare/v0.1.0...v0.2.0
|
|
124
163
|
[0.1.0]: https://github.com/pratik916/tracefork/releases/tag/v0.1.0
|
|
@@ -10,7 +10,7 @@ step, and measure causal blame with confidence intervals — the instrument itse
|
|
|
10
10
|
validated against runs with injected, known root-cause faults.
|
|
11
11
|
|
|
12
12
|
**Current state: v1 built.** All five product pillars work offline and are tested
|
|
13
|
-
(
|
|
13
|
+
(672 tests, $0): streaming-capable record/replay with drift detection, the three-phase
|
|
14
14
|
fork engine, the causal blame engine with Wilson CIs and a budget governor, the
|
|
15
15
|
single-file web report/UI, and the fault-injection self-validation suite (5 fault
|
|
16
16
|
classes at 1.00 top-1 precision, plus a longer competing-fault fixture that measures
|
|
@@ -29,7 +29,7 @@ record/replay/fork are offline and $0 — **no `ANTHROPIC_API_KEY`, no network**
|
|
|
29
29
|
|
|
30
30
|
```bash
|
|
31
31
|
uv sync --extra dev # install (anthropic, zstandard, typer, fastapi, uvicorn + pytest)
|
|
32
|
-
uv run pytest -q # full offline suite (
|
|
32
|
+
uv run pytest -q # full offline suite (672 tests)
|
|
33
33
|
uv run pytest tests/test_faults.py::test_validation_runner_fingers_fault_step -q # one test
|
|
34
34
|
uv run tracefork validate # self-validation: blame vs injected, known faults
|
|
35
35
|
uv run tracefork validate --check # regression-gate vs experiments/validation_report_committed.json
|
|
@@ -175,13 +175,18 @@ The product lives in `src/tracefork/`:
|
|
|
175
175
|
checkpointer), `openai_agents.py` (defensive client-attribute injection +
|
|
176
176
|
`agents.set_default_openai_client()` + a `TracingProcessor`),
|
|
177
177
|
`crewai.py` (LiteLLM's `client_session`/`aclient_session` — CrewAI's actual httpx
|
|
178
|
-
chokepoint — + a `crewai_event_bus` listener),
|
|
179
|
-
client-attribute injection + an `InterventionHandler` message-level seam)
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
178
|
+
chokepoint — + a `crewai_event_bus` listener), `autogen.py` (defensive
|
|
179
|
+
client-attribute injection + an `InterventionHandler` message-level seam), and
|
|
180
|
+
`adk.py` (Google ADK: candidate-path injection into the `google-genai`
|
|
181
|
+
`BaseApiClient`'s private `_httpx_client`/`_async_httpx_client` — reached
|
|
182
|
+
through the target itself, a `genai.Client`, an ADK `Gemini` model wrapper, or
|
|
183
|
+
an `LlmAgent` — + a `BasePlugin` registered once on the `Runner` for
|
|
184
|
+
agent/model/tool before/after boundaries) are the concrete adapters, each its
|
|
185
|
+
own optional extra with every framework import guarded, so `import tracefork`
|
|
186
|
+
and the whole offline suite work with none of them installed. Where a
|
|
187
|
+
framework's exact internal attribute names/event shapes aren't a documented
|
|
188
|
+
stable API, injection is defensive (a short candidate list, never one
|
|
189
|
+
hard-coded name) — see each module's docstring.
|
|
185
190
|
- `cli.py` — Typer entry point for all eleven commands.
|
|
186
191
|
|
|
187
192
|
`src/tracefork_spike/` holds the original Spike 0 (`fake_llm.py`, `agent.py`, `spike.py`):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tracefork
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Time-travel debugger for AI agents: bit-exact record/replay, fork any step, causal blame with confidence intervals.
|
|
5
5
|
Project-URL: Homepage, https://github.com/pratik916/tracefork
|
|
6
6
|
Project-URL: Repository, https://github.com/pratik916/tracefork
|
|
@@ -27,6 +27,16 @@ Requires-Dist: fastapi>=0.115
|
|
|
27
27
|
Requires-Dist: typer>=0.12
|
|
28
28
|
Requires-Dist: uvicorn>=0.30
|
|
29
29
|
Requires-Dist: zstandard>=0.22
|
|
30
|
+
Provides-Extra: adk
|
|
31
|
+
Requires-Dist: google-adk<3,>=2.3; extra == 'adk'
|
|
32
|
+
Provides-Extra: all
|
|
33
|
+
Requires-Dist: boto3>=1.34; extra == 'all'
|
|
34
|
+
Requires-Dist: google-genai>=0.3; extra == 'all'
|
|
35
|
+
Requires-Dist: mcp>=1.0; extra == 'all'
|
|
36
|
+
Requires-Dist: openai>=1.0; extra == 'all'
|
|
37
|
+
Requires-Dist: opentelemetry-api>=1.27; extra == 'all'
|
|
38
|
+
Requires-Dist: opentelemetry-sdk>=1.27; extra == 'all'
|
|
39
|
+
Requires-Dist: structlog>=24.1; extra == 'all'
|
|
30
40
|
Provides-Extra: autogen
|
|
31
41
|
Requires-Dist: autogen-core<1,>=0.7; extra == 'autogen'
|
|
32
42
|
Requires-Dist: autogen-ext<1,>=0.7; extra == 'autogen'
|
|
@@ -41,10 +51,10 @@ Requires-Dist: pytest-cov>=5; extra == 'dev'
|
|
|
41
51
|
Requires-Dist: pytest>=8; extra == 'dev'
|
|
42
52
|
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
43
53
|
Provides-Extra: frameworks
|
|
44
|
-
Requires-Dist: langchain-anthropic
|
|
45
|
-
Requires-Dist: langchain-core
|
|
46
|
-
Requires-Dist: langchain-openai
|
|
47
|
-
Requires-Dist: langgraph
|
|
54
|
+
Requires-Dist: langchain-anthropic>=1.4; extra == 'frameworks'
|
|
55
|
+
Requires-Dist: langchain-core>=1.4; extra == 'frameworks'
|
|
56
|
+
Requires-Dist: langchain-openai>=1.3; extra == 'frameworks'
|
|
57
|
+
Requires-Dist: langgraph>=1.2; extra == 'frameworks'
|
|
48
58
|
Provides-Extra: mcp
|
|
49
59
|
Requires-Dist: mcp>=1.0; extra == 'mcp'
|
|
50
60
|
Provides-Extra: observability
|
|
@@ -123,7 +133,7 @@ makes no network calls.
|
|
|
123
133
|
```bash
|
|
124
134
|
uv sync --extra dev
|
|
125
135
|
|
|
126
|
-
# 1. The full offline test suite (
|
|
136
|
+
# 1. The full offline test suite (672 tests) -- including a cross-module,
|
|
127
137
|
# whole-pipeline suite (tests/test_e2e.py) and an all-CLI-commands smoke
|
|
128
138
|
# test (tests/test_cli_smoke.py), not just per-module unit tests.
|
|
129
139
|
uv run pytest -q
|
|
@@ -164,6 +174,34 @@ bash scripts/e2e.sh
|
|
|
164
174
|
negative control max flip: 0.00 (threshold 0.30)
|
|
165
175
|
```
|
|
166
176
|
|
|
177
|
+
## Optional extras
|
|
178
|
+
|
|
179
|
+
The core install (`pip install tracefork`) is offline/$0 and pulls in no framework or
|
|
180
|
+
provider SDKs. Everything else — providers, Bedrock, MCP, observability, and each
|
|
181
|
+
framework adapter — is opt-in via an extra. A curated bundle of the internally-
|
|
182
|
+
consistent, stable-wire family is available in one shot:
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
pip install 'tracefork[all]'
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
| Extra | Installs | Note |
|
|
189
|
+
|---|---|---|
|
|
190
|
+
| `all` | `providers` + `bedrock` + `mcp` + `observability` | Convenience bundle. Deliberately **excludes** the framework stacks (`frameworks`, `openai-agents`, `crewai`, `autogen`, `adk`) so one framework's future version cap can't block installing everything else. |
|
|
191
|
+
| `providers` | `openai`, `google-genai` SDKs | Record/replay against OpenAI or Gemini directly — not the same as `openai-agents` below. |
|
|
192
|
+
| `bedrock` | `boto3` | AWS Bedrock record/replay. |
|
|
193
|
+
| `mcp` | `mcp` | Model Context Protocol client record/replay. |
|
|
194
|
+
| `observability` | `structlog`, `opentelemetry-*` | Self-instrumentation logging/tracing of tracefork itself. |
|
|
195
|
+
| `frameworks` | `langchain-core`, `langchain-openai`, `langchain-anthropic`, `langgraph` | LangChain/LangGraph adapter. |
|
|
196
|
+
| `openai-agents` | `openai-agents` (the OpenAI **Agents SDK**) | Not the plain `openai` SDK — that's `providers`. |
|
|
197
|
+
| `crewai` | `crewai` | CrewAI adapter (routes through LiteLLM). |
|
|
198
|
+
| `autogen` | `autogen-core`, `autogen-ext` | AutoGen adapter. |
|
|
199
|
+
| `adk` | `google-adk` (Google **Agent Development Kit**) | An agent framework, not a provider SDK. |
|
|
200
|
+
| `dev` | `pytest`, `ruff`, `mypy`, ... | Local development/test tooling. |
|
|
201
|
+
|
|
202
|
+
Every bracketed install command in this README is single-quoted — unquoted `[...]`
|
|
203
|
+
gets glob-expanded by zsh (macOS's default shell) into `no matches found`.
|
|
204
|
+
|
|
167
205
|
## The CLI
|
|
168
206
|
|
|
169
207
|
```bash
|
|
@@ -365,7 +403,7 @@ exercised against the real library when present and skipped cleanly otherwise, s
|
|
|
365
403
|
the pinned adapter version ranges (which churn) are validated separately from the
|
|
366
404
|
offline core.
|
|
367
405
|
|
|
368
|
-
|
|
406
|
+
Four more adapters ship the same way, each its own optional extra and each
|
|
369
407
|
targeting the framework's actual model-call chokepoint:
|
|
370
408
|
|
|
371
409
|
- **OpenAI Agents SDK** (`pip install 'tracefork[openai-agents]'`) — `bind()` injects
|
|
@@ -384,6 +422,15 @@ targeting the framework's actual model-call chokepoint:
|
|
|
384
422
|
defensive attribute search). Step visibility is a message-level `InterventionHandler`
|
|
385
423
|
(`make_intervention_handler()`) — pass-through only, so it stays an annotation
|
|
386
424
|
layer, never a second capture path.
|
|
425
|
+
- **Google ADK** (`pip install 'tracefork[adk]'`, Agent Development Kit) — ADK's
|
|
426
|
+
model calls go through the `google-genai` SDK, so `bind()` walks a short list of
|
|
427
|
+
candidate attribute paths (the target itself, a `genai.Client`, an ADK `Gemini`
|
|
428
|
+
model wrapper, or an `LlmAgent` whose `.model` already holds one) to find the
|
|
429
|
+
`google.genai` `BaseApiClient` and swap in tracefork's httpx clients — the same
|
|
430
|
+
Gemini `generateContent` wire format `providers/gemini.py` already parses. Step
|
|
431
|
+
visibility is a real `BasePlugin` (`make_plugin()`, installable via
|
|
432
|
+
`Runner(..., plugins=[plugin])`) over agent/model/tool before/after boundaries —
|
|
433
|
+
registered once for the whole run rather than threaded through every `LlmAgent`.
|
|
387
434
|
|
|
388
435
|
Each adapter's real-framework wrapper is import-guarded and validated against a
|
|
389
436
|
synthetic stand-in mimicking the framework's interface (never a live call) in the
|
|
@@ -549,7 +596,7 @@ src/tracefork/ transport, tape, nondet, recorder, matcher, redact, fork, st
|
|
|
549
596
|
interop (OTel GenAI / OpenInference export+ingest),
|
|
550
597
|
observability (opt-in structlog + OTel self-instrumentation),
|
|
551
598
|
adapters/ (opt-in framework seam: LangChain/LangGraph, OpenAI
|
|
552
|
-
Agents SDK, CrewAI, AutoGen),
|
|
599
|
+
Agents SDK, CrewAI, AutoGen, Google ADK),
|
|
553
600
|
bedrock_transport (opt-in botocore before-send record/replay seam),
|
|
554
601
|
eventstream (standalone AWS event-stream binary framing codec),
|
|
555
602
|
proxy (opt-in localhost base-URL record/replay proxy for non-Python clients),
|
|
@@ -63,7 +63,7 @@ makes no network calls.
|
|
|
63
63
|
```bash
|
|
64
64
|
uv sync --extra dev
|
|
65
65
|
|
|
66
|
-
# 1. The full offline test suite (
|
|
66
|
+
# 1. The full offline test suite (672 tests) -- including a cross-module,
|
|
67
67
|
# whole-pipeline suite (tests/test_e2e.py) and an all-CLI-commands smoke
|
|
68
68
|
# test (tests/test_cli_smoke.py), not just per-module unit tests.
|
|
69
69
|
uv run pytest -q
|
|
@@ -104,6 +104,34 @@ bash scripts/e2e.sh
|
|
|
104
104
|
negative control max flip: 0.00 (threshold 0.30)
|
|
105
105
|
```
|
|
106
106
|
|
|
107
|
+
## Optional extras
|
|
108
|
+
|
|
109
|
+
The core install (`pip install tracefork`) is offline/$0 and pulls in no framework or
|
|
110
|
+
provider SDKs. Everything else — providers, Bedrock, MCP, observability, and each
|
|
111
|
+
framework adapter — is opt-in via an extra. A curated bundle of the internally-
|
|
112
|
+
consistent, stable-wire family is available in one shot:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
pip install 'tracefork[all]'
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
| Extra | Installs | Note |
|
|
119
|
+
|---|---|---|
|
|
120
|
+
| `all` | `providers` + `bedrock` + `mcp` + `observability` | Convenience bundle. Deliberately **excludes** the framework stacks (`frameworks`, `openai-agents`, `crewai`, `autogen`, `adk`) so one framework's future version cap can't block installing everything else. |
|
|
121
|
+
| `providers` | `openai`, `google-genai` SDKs | Record/replay against OpenAI or Gemini directly — not the same as `openai-agents` below. |
|
|
122
|
+
| `bedrock` | `boto3` | AWS Bedrock record/replay. |
|
|
123
|
+
| `mcp` | `mcp` | Model Context Protocol client record/replay. |
|
|
124
|
+
| `observability` | `structlog`, `opentelemetry-*` | Self-instrumentation logging/tracing of tracefork itself. |
|
|
125
|
+
| `frameworks` | `langchain-core`, `langchain-openai`, `langchain-anthropic`, `langgraph` | LangChain/LangGraph adapter. |
|
|
126
|
+
| `openai-agents` | `openai-agents` (the OpenAI **Agents SDK**) | Not the plain `openai` SDK — that's `providers`. |
|
|
127
|
+
| `crewai` | `crewai` | CrewAI adapter (routes through LiteLLM). |
|
|
128
|
+
| `autogen` | `autogen-core`, `autogen-ext` | AutoGen adapter. |
|
|
129
|
+
| `adk` | `google-adk` (Google **Agent Development Kit**) | An agent framework, not a provider SDK. |
|
|
130
|
+
| `dev` | `pytest`, `ruff`, `mypy`, ... | Local development/test tooling. |
|
|
131
|
+
|
|
132
|
+
Every bracketed install command in this README is single-quoted — unquoted `[...]`
|
|
133
|
+
gets glob-expanded by zsh (macOS's default shell) into `no matches found`.
|
|
134
|
+
|
|
107
135
|
## The CLI
|
|
108
136
|
|
|
109
137
|
```bash
|
|
@@ -305,7 +333,7 @@ exercised against the real library when present and skipped cleanly otherwise, s
|
|
|
305
333
|
the pinned adapter version ranges (which churn) are validated separately from the
|
|
306
334
|
offline core.
|
|
307
335
|
|
|
308
|
-
|
|
336
|
+
Four more adapters ship the same way, each its own optional extra and each
|
|
309
337
|
targeting the framework's actual model-call chokepoint:
|
|
310
338
|
|
|
311
339
|
- **OpenAI Agents SDK** (`pip install 'tracefork[openai-agents]'`) — `bind()` injects
|
|
@@ -324,6 +352,15 @@ targeting the framework's actual model-call chokepoint:
|
|
|
324
352
|
defensive attribute search). Step visibility is a message-level `InterventionHandler`
|
|
325
353
|
(`make_intervention_handler()`) — pass-through only, so it stays an annotation
|
|
326
354
|
layer, never a second capture path.
|
|
355
|
+
- **Google ADK** (`pip install 'tracefork[adk]'`, Agent Development Kit) — ADK's
|
|
356
|
+
model calls go through the `google-genai` SDK, so `bind()` walks a short list of
|
|
357
|
+
candidate attribute paths (the target itself, a `genai.Client`, an ADK `Gemini`
|
|
358
|
+
model wrapper, or an `LlmAgent` whose `.model` already holds one) to find the
|
|
359
|
+
`google.genai` `BaseApiClient` and swap in tracefork's httpx clients — the same
|
|
360
|
+
Gemini `generateContent` wire format `providers/gemini.py` already parses. Step
|
|
361
|
+
visibility is a real `BasePlugin` (`make_plugin()`, installable via
|
|
362
|
+
`Runner(..., plugins=[plugin])`) over agent/model/tool before/after boundaries —
|
|
363
|
+
registered once for the whole run rather than threaded through every `LlmAgent`.
|
|
327
364
|
|
|
328
365
|
Each adapter's real-framework wrapper is import-guarded and validated against a
|
|
329
366
|
synthetic stand-in mimicking the framework's interface (never a live call) in the
|
|
@@ -489,7 +526,7 @@ src/tracefork/ transport, tape, nondet, recorder, matcher, redact, fork, st
|
|
|
489
526
|
interop (OTel GenAI / OpenInference export+ingest),
|
|
490
527
|
observability (opt-in structlog + OTel self-instrumentation),
|
|
491
528
|
adapters/ (opt-in framework seam: LangChain/LangGraph, OpenAI
|
|
492
|
-
Agents SDK, CrewAI, AutoGen),
|
|
529
|
+
Agents SDK, CrewAI, AutoGen, Google ADK),
|
|
493
530
|
bedrock_transport (opt-in botocore before-send record/replay seam),
|
|
494
531
|
eventstream (standalone AWS event-stream binary framing codec),
|
|
495
532
|
proxy (opt-in localhost base-URL record/replay proxy for non-Python clients),
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# tracefork Packaging: Best Practices for Optional Framework Integrations
|
|
2
|
+
|
|
3
|
+
*Synthesis of six adversarially-vetted research dimensions on Python packaging for optional, extra-gated framework integrations. Every load-bearing claim below is tied to a primary source; where the underlying evidence was **DISPUTED** or **NUANCED**, that is called out inline rather than smoothed over.*
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. TL;DR verdict
|
|
8
|
+
|
|
9
|
+
- **Keep per-integration PEP 621 extras — they are mandatory, not stylistic.** PEP 735 dependency-groups are *structurally unreachable* via `pip install tracefork[adk]` (they are excluded from published wheel/sdist metadata and have no install interface), so extras are the only bracket-installable mechanism for user-facing integrations ([PEP 735, Final](https://peps.python.org/pep-0735/); [Dependency Groups spec](https://packaging.python.org/en/latest/specifications/dependency-groups/)). Do **not** split the ~9 thin, first-party, observer-only adapters into separate `tracefork-*` distributions — tracefork has none of the conditions (scale, decoupled cadence, distributed ownership) that drove the famous splits ([Airflow AIP-8](https://cwiki.apache.org/confluence/display/AIRFLOW/AIP-8+Split+Providers+into+Separate+Packages+for+Airflow+2.0); [LangChain v0.1](https://www.langchain.com/blog/langchain-v0-1-0)).
|
|
10
|
+
- **Add a *curated* `all` extra via a self-reference — but deliberately exclude the mutually-heavy framework stacks.** A single god-`all` over five independently-capped, fast-moving agent frameworks is a fragility and bloat anti-pattern ([PEP 771 bloat guidance](https://peps.python.org/pep-0771/)). Ship `all = ["tracefork[providers,bedrock,mcp,observability]"]` and leave the framework stacks to their own extras. Self-referential extras are well-supported and, under hatchling, flattened into concrete metadata at build time ([Hynek Schlawack](https://hynek.me/articles/python-recursive-optional-dependencies/); [core-metadata thread](https://discuss.python.org/t/core-metadata-for-self-referential-extras/77793)).
|
|
11
|
+
- **Drop the *speculative* `<2`/`<3` caps; keep caps only on genuinely churny/pre-stable deps, each with an inline rationale + revisit note.** In Python's flat graph a library upper-cap cannot be overridden downstream and is permanent once on PyPI ([Schreiner](https://iscinumpy.dev/post/bound-version-constraints/); [PyPA install_requires](https://packaging.python.org/en/latest/discussions/install-requires-vs-requirements/)). Replace the `langchain-core<2` cap with a floor + targeted `!=`; keep single-major caps on `openai-agents` (0.x), `autogen`, `crewai`, and `google-adk` **only** because their adapters inject into private/undocumented internals — Schreiner's blessed exception.
|
|
12
|
+
- **Upgrade the runtime guard to `raise ImportError(HINT) from exc`.** Keep the lazy `X_available()`/`require_X()` pair and the exact-extra-name hint; the one high-confidence, do-it-now change is preserving the chained cause so an *installed-but-broken* dependency's real error survives ([Real Python](https://realpython.com/python-raise-exception/); [Python exception chaining](https://docs.python.org/3/library/exceptions.html)).
|
|
13
|
+
- **Migrate `dev` to `[dependency-groups]` — but treat it as a deliberate, later, breaking change,** not a free win: it removes `uv sync --extra dev` and every call site in `CLAUDE.md`/`README`/`scripts/e2e.sh` must move to `--group dev` in the same commit ([PEP 735](https://peps.python.org/pep-0735/); [pip 25.1 `--group`](https://ichard26.github.io/blog/2025/04/whats-new-in-pip-25.1/)).
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 2. Per-dimension best practice
|
|
18
|
+
|
|
19
|
+
### 2.1 Standards & mechanism (extras vs PEP 735 dependency-groups)
|
|
20
|
+
|
|
21
|
+
**Best practice: user-facing optional integrations MUST be `[project.optional-dependencies]` (extras); development-only tooling belongs in `[dependency-groups]`.** This is not a style preference — it is a structural constraint. The finalized spec states build backends **"MUST NOT include Dependency Group data in built distributions as package metadata"** and that **"there is no syntax or specification-defined interface for installing or referring to dependency groups"** ([Dependency Groups spec](https://packaging.python.org/en/latest/specifications/dependency-groups/); [PEP 735, Status: Final](https://peps.python.org/pep-0735/)). Extras, by contrast, are published to `METADATA` (`Provides-Extra`/`Requires-Dist`) and installable via `pkg[extra]`. pyOpenSci frames the division cleanly: optional-dependencies are for functionality "a user wants to access," dependency-groups "organize development dependencies and are intentionally separate" ([pyOpenSci](https://www.pyopensci.org/python-package-guide/package-structure-code/declare-dependencies.html)).
|
|
22
|
+
|
|
23
|
+
*Verdict: **SUPPORTED**, unqualified.* tracefork's langchain/crewai/adk/provider adapters are exactly the user-facing case, so they must stay extras. Do **not** design around [PEP 771 default-extras](https://peps.python.org/pep-0771/) — still **Draft** (round-2 discussion June 2025), targeting an unshipped Metadata-Version 2.5, with only unreviewed proof-of-concept branches. Keep the base install minimal and every integration strictly opt-in.
|
|
24
|
+
|
|
25
|
+
### 2.2 The `[all]` convenience extra
|
|
26
|
+
|
|
27
|
+
**Best practice: a self-referential meta-extra is a legitimate, well-precedented pattern — but curate it to an internally-consistent family, don't god-`all` heavyweight independent stacks.** The self-reference (`all = ["tracefork[providers,mcp,...]"]`) is an ordinary PEP 508 requirement naming the project itself, not special grammar; setuptools discussion #3627 confirms "that is perfectly OK," and dask ships exactly this in production: `complete = ["dask[array,dataframe,distributed,diagnostics]", ...]` ([Hynek](https://hynek.me/articles/python-recursive-optional-dependencies/); [dask pyproject.toml](https://raw.githubusercontent.com/dask/dask/main/pyproject.toml); [setuptools #3627](https://github.com/pypa/setuptools/discussions/3627)).
|
|
28
|
+
|
|
29
|
+
**NUANCED — mechanism correction:** the common framing that "the backend passes metadata through unchanged; the pip-21.2 resolver does the recursion" is **backwards for hatchling**. Charlie Marsh's demonstration shows hatchling **FLATTENS** the self-reference into concrete `Requires-Dist` at *build* time (setuptools preserves it), so a hatchling wheel's METADATA already contains the concrete union and pip never performs the recursion ([core-metadata thread](https://discuss.python.org/t/core-metadata-for-self-referential-extras/77793)). The end result is equivalent, but declare the self-ref knowing the backend resolves it. Paul Moore's caveat in that thread — "extras are badly specified by the standards" — is why the post-edit resolution check is required, not optional.
|
|
30
|
+
|
|
31
|
+
**NUANCED — the god-`all` warning is directional, not a proven unsolvable build:** the provable downsides of unioning tracefork's five capped, fast-moving frameworks (langchain, crewai, autogen, adk, openai-agents) are (a) **union-fragility** — one future cap collision breaks the whole `all` install though each extra installs fine alone — and (b) **install/vuln bloat**, which PEP 771 explicitly warns to avoid ("carefully consider what is included... to avoid unnecessarily bloating installations") ([PEP 771](https://peps.python.org/pep-0771/)). The claim that these frameworks are "not co-resolvable today" is **unverified** and should not be asserted — modern resolvers (uv) are strong, and running a real solve would violate the offline/$0 invariant. State the fragility mechanism, not a certainty.
|
|
32
|
+
|
|
33
|
+
*Not-triggered caveats worth documenting:* the [hatch #1610](https://github.com/pypa/hatch/issues/1610) self-ref bug is real but scoped to the `hatch env` *manager* — tracefork uses `uv sync`/`uv run`, never `hatch env`, so it cannot bite; and uv's own historical self-ref bug ([astral-sh/uv #1987](https://github.com/astral-sh/uv/issues/1987)) is long fixed. Still, **verify `uv sync --extra all` resolves after the edit** — recursive extras have documented lock/compile edge cases ([pip-tools #2002](https://github.com/jazzband/pip-tools/issues/2002)).
|
|
34
|
+
|
|
35
|
+
### 2.3 Extras vs separate packages
|
|
36
|
+
|
|
37
|
+
**Best practice: keep integrations as extras until an adapter independently needs decoupled release cadence, gains an external co-maintainer, or develops a conflicting hard dependency — never split on integration *count*.** The decisive precondition behind every famous split was **scale + independent cadence + dependency isolation**, not count. Airflow's AIP-8 split providers for release-cadence and distributed-expertise reasons and *explicitly named the cost*: **"in the long-term structure there are many more packages to maintain"** ([AIP-8](https://cwiki.apache.org/confluence/display/AIRFLOW/AIP-8+Split+Providers+into+Separate+Packages+for+Airflow+2.0)). LangChain split at **700+ integrations** to fix install headaches, enable per-package breaking-change versioning, and de-bloat an unstable core with CVE exposure ([LangChain v0.1](https://www.langchain.com/blog/langchain-v0-1-0); [v0.2](https://blog.langchain.com/langchain-v02-leap-to-stability/)).
|
|
38
|
+
|
|
39
|
+
**NUANCED — the load-bearing reason is scale/cadence, not "no external partners."** Airflow's own docs confirm many providers are **community-maintained**, so external ownership is a *common but not necessary* trigger ([apache-airflow-providers](https://airflow.apache.org/docs/apache-airflow-providers/)). tracefork lacks the scale (~9 vs 58–700) and any cadence-decoupling need regardless. At its profile — solo maintainer, guarded imports already containing blast radius to opt-in users — a split's benefits are near-zero and AIP-8's named cost (N repos, N pipelines, an N×N core-vs-adapter compatibility matrix) lands in full.
|
|
40
|
+
|
|
41
|
+
*Future-proofing:* register an `importlib.metadata` entry-point group (e.g. `tracefork.adapters`) resolved by `adapters/base.py`, so a *later* extraction of any single adapter is non-breaking.
|
|
42
|
+
|
|
43
|
+
### 2.4 Naming / normalization
|
|
44
|
+
|
|
45
|
+
**Best practice: lowercase-hyphen extra names — which tracefork already uses — are PEP 685/PEP 503 conformant; no rename is required.** PEP 685 mandates that extras be compared and *written* normalized (`re.sub(r"[-_.]+", "-", name).lower()`) and that generators error if two extras collide ([PEP 685](https://peps.python.org/pep-0685/); [Name normalization](https://packaging.python.org/en/latest/specifications/name-normalization/)). Hatchling normalizes extra names by default ([Hatch metadata docs](https://hatch.pypa.io/latest/config/metadata/)). tracefork's ten names (`dev, providers, bedrock, mcp, frameworks, openai-agents, crewai, autogen, adk, observability`) are all-lowercase, already-normalized, and collision-free — the MUST-error branch cannot fire.
|
|
46
|
+
|
|
47
|
+
**Three framing corrections (none change the KEEP verdict):**
|
|
48
|
+
- The transparent underscore↔hyphen equivalence is a **post-685-tooling property only**. The [huggingface_hub #3029](https://github.com/huggingface/huggingface_hub/issues/3029) bug shows pip ≤ 22.0.2 *silently skipped* a mismatched `hf_transfer` extra; pip 25.0.1+ tolerates it. This "doubly matters" for a self-referential `all`: both the extra name and the bracket must normalize consistently.
|
|
49
|
+
- Hatchling's `allow-ambiguous-features` opt-out is **deprecated-but-still-present**, not removed — soften any "removed" wording. tracefork never sets it, so this is moot.
|
|
50
|
+
- "lowercase kebab-case mirroring the distribution name" is a *consequence of normalization plus community practice*, not a convention the Packaging User Guide prescribes ([writing-pyproject-toml](https://packaging.python.org/en/latest/guides/writing-pyproject-toml/) prescribes none).
|
|
51
|
+
|
|
52
|
+
**The real user-facing gotcha is the shell, not TOML.** Bare TOML keys allow dashes, so `openai-agents = [...]` needs no quotes ([TOML v1.0.0](https://toml.io/en/v1.0.0)) — only a **dot** would force quoting. But zsh (default macOS shell) globs `[...]`, so unquoted `pip install tracefork[dev]` fails with `zsh: no matches found` ([napari #2081](https://github.com/napari/napari/issues/2081); [Berton](https://lucaberton.medium.com/how-to-fix-zsh-no-matches-found-when-installing-python-packages-with-optional-dependencies-40aec463ff09)). **Always quote bracketed install specs in README/CLAUDE.md.**
|
|
53
|
+
|
|
54
|
+
### 2.5 Version caps
|
|
55
|
+
|
|
56
|
+
**Best practice: floor everywhere + targeted `!=` for proven-bad releases; reserve blanket upper caps for genuinely unstable deps that *also* carry a documented lift-the-cap cadence.** The consensus (Brett Cannon via Schreiner, PyPA, pip docs) is robust: "set a floor... but otherwise don't cap the maximum version as you can't predict future compatibility," and crucially **"you can't fix an over-constraint"** — an over-cap is unfixable downstream while an under-constraint is a trivial user workaround ([Schreiner](https://iscinumpy.dev/post/bound-version-constraints/)). PyPA's install_requires guide is author-facing and squarely agrees: preemptive pinning is "overly-restrictive, and prevents the user from gaining the benefit of dependency upgrades" ([PyPA](https://packaging.python.org/en/latest/discussions/install-requires-vs-requirements/)). Because PyPI artifacts are immutable, a bad cap in a shipped release is **permanent** ([prefix.dev](https://prefix.dev/blog/the_python_packaging_debate); [ResolutionImpossible](https://pip.pypa.io/en/stable/ux-research-design/resolution-impossible-example/)).
|
|
57
|
+
|
|
58
|
+
**Schreiner's blessed exceptions apply to tracefork's adapters — this is genuinely NUANCED, and the nuance *sharpens* the recommendation:**
|
|
59
|
+
- Caps are acceptable for "an extension for an ecosystem/framework (pytest/Sphinx/Jupyter extension)... capping on the major version" and when "you depend on private internal details of a library." tracefork's defensive-attribute-injection adapters (langchain/openai-agents/crewai/autogen/adk) fit both.
|
|
60
|
+
- **But a single-major cap gives ZERO protection against the real observed failure mode.** Schreiner's own words: private-internal breakage "can be broken in a minor or patch release, and often is." Verified in the wild: `langchain-core` shipped a 1.x patch regression breaking `ToolsRenderer`/`RetrieverInput` imports, and `langgraph-prebuilt` shipped breaking changes in patch releases **1.0.2** (required a new `runtime` param — [#6363](https://github.com/langchain-ai/langgraph/issues/6363)), **1.0.5** ([#6477](https://github.com/langchain-ai/langgraph/issues/6477)), and **1.0.9**. A `<2` cap catches *none* of these — they live inside 1.x. **Only a targeted `!=` catches them, and CI-against-latest is the actual guard; the cap is a courtesy hint.**
|
|
61
|
+
|
|
62
|
+
**Per-dependency calls:**
|
|
63
|
+
- **`langchain-core >=1.4,<2` → DROP to `>=1.4`.** LangChain 1.0 GA (Oct 22 2025) commits to "no breaking changes until 2.0" ([release policy](https://docs.langchain.com/oss/python/release-policy)), so `<2` is speculative *and* useless against the real intra-1.x breakage — keep `!=` in the toolkit, monitor.
|
|
64
|
+
- **`autogen-* >=0.7,<1` → KEEP.** Genuinely experimental/pre-stable: 0.4 is a ground-up async rewrite, officially "expect changes and bugs," now fracturing into Microsoft's Agent Framework ([migration guide](https://microsoft.github.io/autogen/0.4.8//user-guide/agentchat-user-guide/migration-guide.html)).
|
|
65
|
+
- **`google-adk >=2.3,<3` → KEEP, but correct the rationale.** ADK is **not** pre-1.0 — v1.0.0 shipped May 2025 as "stable, production-ready." The cap rests on **fast major cadence + adapter-internal coupling**, not "pre-1.0 instability," and is only honest paired with a CI item that tests the newest major each release.
|
|
66
|
+
- **`openai-agents >=0.10,<1` → KEEP** (0.x, pre-stable API, defensive internal injection).
|
|
67
|
+
- **`crewai >=1.0,<2` → KEEP defensibly** (adapter hooks LiteLLM's private `client_session` internals) *with* a revisit note.
|
|
68
|
+
- **`providers` (openai, google-genai), `bedrock` (boto3), `mcp`, `observability` (structlog, opentelemetry-*) → floors already; leave uncapped.** Stable-wire-format / self-instrumentation deps, not private-internal framework extensions.
|
|
69
|
+
|
|
70
|
+
*Caveat:* tracefork uses hatchling/PEP 621, so it never *inherits* Poetry-style auto-caps ([poetry-relax](https://pypi.org/project/poetry-relax/); [poetry #2731](https://github.com/python-poetry/poetry/issues/2731)) — these caps are hand-written and deliberate, which is exactly why each needs an inline `# TODO: widen` comment.
|
|
71
|
+
|
|
72
|
+
### 2.6 Runtime UX
|
|
73
|
+
|
|
74
|
+
**Best practice: keep the lazy `X_available()` + `require_X()` guard pair naming the exact project extra; upgrade to chain the cause with `from exc`.** The idiom — import lazily, catch `ImportError`, re-raise a *new* `ImportError` naming the remediation — is confirmed against scikit-learn's `check_matplotlib_support`: `raise ImportError(f"... You can install matplotlib with 'pip install matplotlib'") from e` ([sklearn](https://github.com/scikit-learn/scikit-learn/blob/main/sklearn/utils/_optional_dependencies.py)). **DISPUTED exemplar:** sklearn is *inconsistent* — its sibling `check_pandas_support` raises only `"... requires pandas."` with no remediation. This **strengthens** tracefork's design: naming the exact quoted extra in *every* adapter is strictly better than sklearn's mixed messages.
|
|
75
|
+
|
|
76
|
+
`raise ... from exc` is the documented default for wrapping/transforming exceptions and preserves the original traceback ([Real Python](https://realpython.com/python-raise-exception/); [Python docs](https://docs.python.org/3/library/exceptions.html)). It is the safer default than `from None` because it does **not** hide an *installed-but-broken* dependency's real error — which, given tracefork injects into undocumented framework internals, is the exact confusing `AttributeError` you want surfaced. This is "best practice," not "strictly required" (`from None` is defensible for the pure missing-package case).
|
|
77
|
+
|
|
78
|
+
**Optional (not load-bearing):** a pandas-style `require_extra(module, extra)` DRY helper. Worthwhile only if adapters grow; for ~5 adapters the duplication is small. **Do NOT adopt `lazy_loader`;** PEP 562 `__getattr__` is optional polish, not needed here.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## 3. Concrete recommendation for tracefork
|
|
83
|
+
|
|
84
|
+
**Decisions, sequenced.**
|
|
85
|
+
|
|
86
|
+
### For the next 0.2.1 release (do now — pyproject + docs + one runtime idiom)
|
|
87
|
+
|
|
88
|
+
**1. Keep per-integration extras.** No structural change; they are the only bracket-installable mechanism.
|
|
89
|
+
|
|
90
|
+
**2. Relax the speculative caps to floors; keep only the internal-coupling caps, each with an inline rationale + revisit note.** Example shape:
|
|
91
|
+
|
|
92
|
+
```toml
|
|
93
|
+
[project.optional-dependencies]
|
|
94
|
+
# stable wire-format / self-instrumentation — floors only
|
|
95
|
+
providers = ["openai>=1.0", "google-genai>=0.3"]
|
|
96
|
+
bedrock = ["boto3>=1.34"]
|
|
97
|
+
mcp = ["mcp>=1.0"]
|
|
98
|
+
observability = ["structlog>=24.1", "opentelemetry-api>=1.27", "opentelemetry-sdk>=1.27"]
|
|
99
|
+
|
|
100
|
+
# LangChain 1.0 GA promises no breaks until 2.0 -> floor only
|
|
101
|
+
# (a <2 cap is useless against the real intra-1.x patch regressions)
|
|
102
|
+
frameworks = ["langchain-core>=1.4", "langchain-openai>=1.3", "langchain-anthropic>=1.4", "langgraph>=1.2"]
|
|
103
|
+
|
|
104
|
+
# CAPS: adapter injects into private/undocumented internals (Schreiner's blessed
|
|
105
|
+
# exception). Each cap is a hint, NOT a guard — CI-against-latest is the guard.
|
|
106
|
+
# TODO(caps): test newest major each release and widen.
|
|
107
|
+
openai-agents = ["openai-agents>=0.10,<1"] # 0.x pre-stable API
|
|
108
|
+
autogen = ["autogen-core>=0.7,<1", "autogen-ext>=0.7,<1"] # experimental rewrite
|
|
109
|
+
crewai = ["crewai>=1.0,<2"] # hooks LiteLLM private client_session
|
|
110
|
+
adk = ["google-adk>=2.3,<3"] # stable but fast-major + internal coupling
|
|
111
|
+
|
|
112
|
+
# curated convenience extra — self-referential, deliberately EXCLUDES the
|
|
113
|
+
# mutually-heavy framework stacks and dev
|
|
114
|
+
all = ["tracefork[providers,bedrock,mcp,observability]"]
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**Self-referencing extras are safe here.** Under hatchling the `all` self-reference is flattened into concrete `Requires-Dist` at build time. **Required post-edit step (not a formality):** run `uv sync --extra all` and `uv run pytest -q` to confirm resolution and that the offline suite still passes.
|
|
118
|
+
|
|
119
|
+
*Why exclude the framework stacks from `all`:* unioning five independently-capped, fast-moving frameworks makes `all` a single all-or-nothing failure target — one future cap collision `ResolutionImpossible`s the whole install though each extra installs fine alone. The curated `all` mirrors dask's internally-consistent `complete`, not pandas' god-`all`.
|
|
120
|
+
|
|
121
|
+
**3. Upgrade the runtime guard to chain the cause.** Sketch:
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
# tracefork/adapters/base.py
|
|
125
|
+
def require_extra(module: str, extra: str):
|
|
126
|
+
"""Import `module` or raise an actionable, chained ImportError."""
|
|
127
|
+
try:
|
|
128
|
+
return importlib.import_module(module)
|
|
129
|
+
except ImportError as exc: # keep `from exc` — preserves an installed-but-broken dep's real error
|
|
130
|
+
raise ImportError(
|
|
131
|
+
f"tracefork's {extra!r} integration needs {module!r}. "
|
|
132
|
+
f"Install it with: pip install 'tracefork[{extra}]'"
|
|
133
|
+
) from exc
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**4. Docs:** quote every bracketed install snippet in `README`/`CLAUDE.md` (`pip install 'tracefork[all]'`), and add a small **extras table** disambiguating the generic names.
|
|
137
|
+
|
|
138
|
+
**5. Register an `entry-points` group** (`tracefork.adapters`) resolved by `adapters/base.py`, so a future single-adapter extraction is non-breaking.
|
|
139
|
+
|
|
140
|
+
### Later (deliberate, higher-churn changes)
|
|
141
|
+
|
|
142
|
+
**6. Migrate `dev` to `[dependency-groups]`** — breaking to the dev command surface; update `CLAUDE.md`/`README`/`scripts/e2e.sh` to `uv sync --group dev` in the same commit.
|
|
143
|
+
|
|
144
|
+
**7. Add runtime version/feature checks** to the internal-coupling adapters (the real guard the caps only hint at).
|
|
145
|
+
|
|
146
|
+
### Explicitly decided against
|
|
147
|
+
|
|
148
|
+
- **No `tracefork-*` package split.** Split trigger = an adapter needing decoupled cadence, a co-maintainer, or a conflicting hard dependency — never count.
|
|
149
|
+
- **No god-`all` over the frameworks.**
|
|
150
|
+
- **No rename of extras for conformance** — already PEP 685/503 clean.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## 4. What NOT to do
|
|
155
|
+
|
|
156
|
+
- **Do NOT put user-facing integrations in `[dependency-groups]`.** Excluded from published metadata, no bracket-install interface.
|
|
157
|
+
- **Do NOT design around PEP 771 default-extras.** Still Draft, no shipped tooling.
|
|
158
|
+
- **Do NOT ship a single god-`all` over the five fast-moving frameworks.**
|
|
159
|
+
- **Do NOT keep preemptive `<2`/`<3` caps on stable-wire deps.** A library upper-cap can't be overridden downstream and is permanent once on PyPI.
|
|
160
|
+
- **Do NOT treat a single-major cap as protection against private-internal breakage.** Those ship in minor/patch releases inside the allowed range.
|
|
161
|
+
- **Do NOT let a caught `ImportError` swallow the original.** Re-raise with `from exc`.
|
|
162
|
+
- **Do NOT leave unquoted bracket install commands in docs.** zsh globs them.
|
|
163
|
+
- **Do NOT rename extras to add underscores or rely on old-pip normalization.**
|
|
164
|
+
- **Do NOT split adapters into separate packages "for cleanliness."**
|
|
165
|
+
- **Do NOT migrate `dev` off extras as a "quick additive edit."**
|
|
166
|
+
- **Do NOT reach for `lazy_loader` or PEP 562 `__getattr__`** — unnecessary for ~5 adapters.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
*Method: 13-agent research workflow (6 dimensions → adversarial verification → synthesis), all claims web-sourced against primary references. Generated 2026-07-03.*
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "tracefork"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.1"
|
|
4
4
|
description = "Time-travel debugger for AI agents: bit-exact record/replay, fork any step, causal blame with confidence intervals."
|
|
5
5
|
requires-python = ">=3.12"
|
|
6
6
|
dependencies = [
|
|
@@ -67,20 +67,25 @@ mcp = [
|
|
|
67
67
|
# Optional: LangChain / LangGraph, only needed to bind a *live* framework run to
|
|
68
68
|
# tracefork's transport seam (record) or replay a graph from a tape. The adapter
|
|
69
69
|
# guards every framework import, so `import tracefork` and the whole offline test
|
|
70
|
-
# suite work with none of these installed — they are NOT hard dependencies.
|
|
71
|
-
#
|
|
72
|
-
#
|
|
70
|
+
# suite work with none of these installed — they are NOT hard dependencies.
|
|
71
|
+
# Floors only (no `<2` cap): LangChain 1.0 GA commits to no breaking changes
|
|
72
|
+
# until 2.0, so a `<2` cap is speculative AND useless against the real failure
|
|
73
|
+
# mode — intra-1.x patch regressions (e.g. langgraph-prebuilt 1.0.2/1.0.5/1.0.9)
|
|
74
|
+
# ship *inside* the allowed range, so only CI-against-latest (+ a targeted `!=`
|
|
75
|
+
# if a bad release is ever confirmed) actually catches those, not a major cap.
|
|
73
76
|
frameworks = [
|
|
74
|
-
"langchain-core>=1.4
|
|
75
|
-
"langchain-openai>=1.3
|
|
76
|
-
"langchain-anthropic>=1.4
|
|
77
|
-
"langgraph>=1.2
|
|
77
|
+
"langchain-core>=1.4",
|
|
78
|
+
"langchain-openai>=1.3",
|
|
79
|
+
"langchain-anthropic>=1.4",
|
|
80
|
+
"langgraph>=1.2",
|
|
78
81
|
]
|
|
79
82
|
# Optional: the OpenAI Agents SDK, only needed to bind a *live* Agents SDK run's
|
|
80
83
|
# underlying openai client to tracefork's transport seam (`adapters/openai_agents.py`),
|
|
81
84
|
# or to install its tracing core as a real TracingProcessor. The adapter guards
|
|
82
85
|
# every import, so `import tracefork` and the whole offline test suite work with
|
|
83
|
-
# it NOT installed.
|
|
86
|
+
# it NOT installed. CAP: 0.x pre-stable API, and the adapter injects into
|
|
87
|
+
# private/undocumented internals — the cap is a hint, not a guard; CI-against-
|
|
88
|
+
# latest is the real guard. TODO(caps): test newest major each release and widen.
|
|
84
89
|
"openai-agents" = [
|
|
85
90
|
"openai-agents>=0.10,<1",
|
|
86
91
|
]
|
|
@@ -90,6 +95,9 @@ frameworks = [
|
|
|
90
95
|
# handed), so `import tracefork` and the whole offline test suite work with
|
|
91
96
|
# neither installed. litellm — the actual httpx chokepoint `bind()` targets —
|
|
92
97
|
# is crewai's own transitive dependency, so it is not pinned separately here.
|
|
98
|
+
# CAP: the adapter hooks LiteLLM's private `client_session`/`aclient_session`
|
|
99
|
+
# internals — the cap is a hint, not a guard; CI-against-latest is the real
|
|
100
|
+
# guard. TODO(caps): test newest major each release and widen.
|
|
93
101
|
crewai = [
|
|
94
102
|
"crewai>=1.0,<2",
|
|
95
103
|
]
|
|
@@ -97,11 +105,25 @@ crewai = [
|
|
|
97
105
|
# autogen-ext (the OpenAIChatCompletionClient model wrapper `bind()` targets),
|
|
98
106
|
# only needed to exercise `adapters/autogen.py` against the real framework.
|
|
99
107
|
# Both imports are guarded, so `import tracefork` and the whole offline test
|
|
100
|
-
# suite work with neither installed.
|
|
108
|
+
# suite work with neither installed. CAP: genuinely experimental/pre-stable
|
|
109
|
+
# (ground-up async rewrite) — the cap is a hint, not a guard; CI-against-latest
|
|
110
|
+
# is the real guard. TODO(caps): test newest major each release and widen.
|
|
101
111
|
autogen = [
|
|
102
112
|
"autogen-core>=0.7,<1",
|
|
103
113
|
"autogen-ext>=0.7,<1",
|
|
104
114
|
]
|
|
115
|
+
# Optional: google-adk (the Google Agent Development Kit), only needed to bind a
|
|
116
|
+
# *live* ADK LlmAgent/Runner's underlying google-genai client to tracefork's
|
|
117
|
+
# transport seam (`adapters/adk.py`), or to install its BasePlugin as a real
|
|
118
|
+
# ADK plugin. google-adk pulls in google-genai transitively (it already pins
|
|
119
|
+
# `google-genai>=2.8,<3`), so it is not pinned separately here. Both imports
|
|
120
|
+
# are guarded, so `import tracefork` and the whole offline test suite work
|
|
121
|
+
# with neither installed. CAP: stable (not pre-1.0) but fast major cadence +
|
|
122
|
+
# adapter-internal coupling — the cap is a hint, not a guard; CI-against-latest
|
|
123
|
+
# is the real guard. TODO(caps): test newest major each release and widen.
|
|
124
|
+
adk = [
|
|
125
|
+
"google-adk>=2.3,<3",
|
|
126
|
+
]
|
|
105
127
|
# Optional: structlog JSON logging + OTel self-instrumentation of
|
|
106
128
|
# record/replay/fork/blame (see observability.py). Export/ingest of OTel
|
|
107
129
|
# GenAI / OpenInference data (interop.py) needs NEITHER of these — it produces
|
|
@@ -111,6 +133,12 @@ observability = [
|
|
|
111
133
|
"opentelemetry-api>=1.27",
|
|
112
134
|
"opentelemetry-sdk>=1.27",
|
|
113
135
|
]
|
|
136
|
+
# Curated convenience extra: the stable-wire / self-instrumentation family in one
|
|
137
|
+
# shot. DELIBERATELY excludes the fast-moving framework stacks (frameworks,
|
|
138
|
+
# openai-agents, crewai, autogen, adk) and dev — unioning five independently-capped
|
|
139
|
+
# frameworks would make `all` a single all-or-nothing resolution target. Self-
|
|
140
|
+
# referential extra (hatchling flattens it into concrete Requires-Dist at build).
|
|
141
|
+
all = ["tracefork[providers,bedrock,mcp,observability]"]
|
|
114
142
|
|
|
115
143
|
[project.scripts]
|
|
116
144
|
tracefork = "tracefork.cli:app"
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"""tracefork — time-travel debugger for AI agents."""
|
|
2
2
|
|
|
3
3
|
from .adapters import (
|
|
4
|
+
ADK_IMPORT_HINT,
|
|
5
|
+
AdkAdapter,
|
|
4
6
|
AutoGenAdapter,
|
|
5
7
|
BaseFrameworkAdapter,
|
|
6
8
|
BindResult,
|
|
@@ -11,10 +13,12 @@ from .adapters import (
|
|
|
11
13
|
Step,
|
|
12
14
|
StepDAG,
|
|
13
15
|
TapeBackedCheckpointStore,
|
|
16
|
+
TraceforkAdkCore,
|
|
14
17
|
TraceforkCallbackCore,
|
|
15
18
|
TraceforkCrewEventCore,
|
|
16
19
|
TraceforkInterventionCore,
|
|
17
20
|
TraceforkTracingCore,
|
|
21
|
+
adk_available,
|
|
18
22
|
autogen_available,
|
|
19
23
|
bind_default_client,
|
|
20
24
|
crewai_available,
|
|
@@ -24,11 +28,13 @@ from .adapters import (
|
|
|
24
28
|
make_callback_handler,
|
|
25
29
|
make_event_listener,
|
|
26
30
|
make_intervention_handler,
|
|
31
|
+
make_plugin,
|
|
27
32
|
make_tape_backed_checkpointer,
|
|
28
33
|
make_tracing_processor,
|
|
29
34
|
openai_agents_available,
|
|
30
35
|
register_framework_adapter,
|
|
31
36
|
registered_framework_adapters,
|
|
37
|
+
require_adk,
|
|
32
38
|
require_autogen,
|
|
33
39
|
require_crewai,
|
|
34
40
|
require_langchain,
|
|
@@ -131,4 +137,10 @@ __all__ = [
|
|
|
131
137
|
"autogen_available",
|
|
132
138
|
"make_intervention_handler",
|
|
133
139
|
"require_autogen",
|
|
140
|
+
"ADK_IMPORT_HINT",
|
|
141
|
+
"AdkAdapter",
|
|
142
|
+
"TraceforkAdkCore",
|
|
143
|
+
"adk_available",
|
|
144
|
+
"make_plugin",
|
|
145
|
+
"require_adk",
|
|
134
146
|
]
|