loopgain 0.4.1__tar.gz → 0.4.2__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.
Files changed (34) hide show
  1. {loopgain-0.4.1 → loopgain-0.4.2}/PKG-INFO +2 -2
  2. {loopgain-0.4.1 → loopgain-0.4.2}/README.md +1 -1
  3. {loopgain-0.4.1 → loopgain-0.4.2}/loopgain/_version.py +1 -1
  4. {loopgain-0.4.1 → loopgain-0.4.2}/loopgain/classifier.py +8 -3
  5. {loopgain-0.4.1 → loopgain-0.4.2}/loopgain/telemetry.py +5 -0
  6. {loopgain-0.4.1 → loopgain-0.4.2}/loopgain.egg-info/PKG-INFO +2 -2
  7. {loopgain-0.4.1 → loopgain-0.4.2}/tests/test_classifier_mock_validation.py +7 -5
  8. {loopgain-0.4.1 → loopgain-0.4.2}/tests/test_classifier_synthetic.py +36 -1
  9. {loopgain-0.4.1 → loopgain-0.4.2}/LICENSE +0 -0
  10. {loopgain-0.4.1 → loopgain-0.4.2}/loopgain/__init__.py +0 -0
  11. {loopgain-0.4.1 → loopgain-0.4.2}/loopgain/__main__.py +0 -0
  12. {loopgain-0.4.1 → loopgain-0.4.2}/loopgain/cli.py +0 -0
  13. {loopgain-0.4.1 → loopgain-0.4.2}/loopgain/core.py +0 -0
  14. {loopgain-0.4.1 → loopgain-0.4.2}/loopgain/funnel.py +0 -0
  15. {loopgain-0.4.1 → loopgain-0.4.2}/loopgain/integrations/__init__.py +0 -0
  16. {loopgain-0.4.1 → loopgain-0.4.2}/loopgain/integrations/autogen.py +0 -0
  17. {loopgain-0.4.1 → loopgain-0.4.2}/loopgain/integrations/claude_agent_sdk.py +0 -0
  18. {loopgain-0.4.1 → loopgain-0.4.2}/loopgain/integrations/crewai.py +0 -0
  19. {loopgain-0.4.1 → loopgain-0.4.2}/loopgain/integrations/langchain.py +0 -0
  20. {loopgain-0.4.1 → loopgain-0.4.2}/loopgain/integrations/langgraph.py +0 -0
  21. {loopgain-0.4.1 → loopgain-0.4.2}/loopgain/integrations/openai_agents.py +0 -0
  22. {loopgain-0.4.1 → loopgain-0.4.2}/loopgain.egg-info/SOURCES.txt +0 -0
  23. {loopgain-0.4.1 → loopgain-0.4.2}/loopgain.egg-info/dependency_links.txt +0 -0
  24. {loopgain-0.4.1 → loopgain-0.4.2}/loopgain.egg-info/entry_points.txt +0 -0
  25. {loopgain-0.4.1 → loopgain-0.4.2}/loopgain.egg-info/requires.txt +0 -0
  26. {loopgain-0.4.1 → loopgain-0.4.2}/loopgain.egg-info/top_level.txt +0 -0
  27. {loopgain-0.4.1 → loopgain-0.4.2}/pyproject.toml +0 -0
  28. {loopgain-0.4.1 → loopgain-0.4.2}/setup.cfg +0 -0
  29. {loopgain-0.4.1 → loopgain-0.4.2}/tests/test_core.py +0 -0
  30. {loopgain-0.4.1 → loopgain-0.4.2}/tests/test_funnel.py +0 -0
  31. {loopgain-0.4.1 → loopgain-0.4.2}/tests/test_integrations.py +0 -0
  32. {loopgain-0.4.1 → loopgain-0.4.2}/tests/test_stress.py +0 -0
  33. {loopgain-0.4.1 → loopgain-0.4.2}/tests/test_telemetry.py +0 -0
  34. {loopgain-0.4.1 → loopgain-0.4.2}/tests/test_termination_safety.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: loopgain
3
- Version: 0.4.1
3
+ Version: 0.4.2
4
4
  Summary: An open-source cost controller for AI agent loops. Stops a loop when it has actually converged and rolls back before it degrades — replacing the max_iterations guess with a real-time loop-gain (Aβ) monitor with five named threshold bands and best-so-far rollback.
5
5
  Author-email: Dave Fitzsimmons <hello@loopgain.ai>
6
6
  License: Apache-2.0
@@ -58,7 +58,7 @@ AI agent loops waste time and money when they don't know when to stop. LoopGain
58
58
  [![PyPI](https://img.shields.io/pypi/v/loopgain.svg)](https://pypi.org/project/loopgain/)
59
59
  [![Python](https://img.shields.io/pypi/pyversions/loopgain.svg)](https://pypi.org/project/loopgain/)
60
60
  [![License](https://img.shields.io/badge/license-Apache_2.0-blue.svg)](LICENSE)
61
- [![Tests](https://img.shields.io/badge/tests-202_passing-brightgreen.svg)](tests/)
61
+ [![Tests](https://img.shields.io/badge/tests-200%2B_passing-brightgreen.svg)](tests/)
62
62
 
63
63
  **Home:** [loopgain.ai](https://loopgain.ai)
64
64
 
@@ -9,7 +9,7 @@ AI agent loops waste time and money when they don't know when to stop. LoopGain
9
9
  [![PyPI](https://img.shields.io/pypi/v/loopgain.svg)](https://pypi.org/project/loopgain/)
10
10
  [![Python](https://img.shields.io/pypi/pyversions/loopgain.svg)](https://pypi.org/project/loopgain/)
11
11
  [![License](https://img.shields.io/badge/license-Apache_2.0-blue.svg)](LICENSE)
12
- [![Tests](https://img.shields.io/badge/tests-202_passing-brightgreen.svg)](tests/)
12
+ [![Tests](https://img.shields.io/badge/tests-200%2B_passing-brightgreen.svg)](tests/)
13
13
 
14
14
  **Home:** [loopgain.ai](https://loopgain.ai)
15
15
 
@@ -7,4 +7,4 @@ from here so the value never drifts between ``__version__`` and the
7
7
  ``pyproject.toml``) for each release.
8
8
  """
9
9
 
10
- __version__ = "0.4.1"
10
+ __version__ = "0.4.2"
@@ -184,9 +184,14 @@ def _two_sided_t_p(t_abs: float, df: int) -> float:
184
184
  # exact: cdf_t(t,1) = 0.5 + arctan(t)/pi
185
185
  return 2.0 * (0.5 - math.atan(t_abs) / math.pi)
186
186
  if df == 2:
187
- # exact one-sided survival: 1 - (1 + t²/2)^(-1) doubled
188
- return min(1.0, 2.0 * (1.0 - t_abs / math.sqrt(2.0 + t_abs * t_abs) / 1.0) * 0.5
189
- + 2.0 * (0.5 - 0.5 * t_abs / math.sqrt(2.0 + t_abs * t_abs)))
187
+ # Exact two-sided p-value for Student-t with df=2. The df=2 CDF is
188
+ # F(t) = 1/2 + t / (2·√(2 + t²)), so the one-sided survival is
189
+ # P(T > t) = 1/2 t / (2·√(2 + t²)) and the two-sided p is
190
+ # 2·P(T > |t|) = 1 − |t| / √(2 + t²).
191
+ # (The previous implementation returned twice this — it required
192
+ # |t| > 6.21 for p<0.05 instead of the correct |t| > 4.30, making
193
+ # the n=4 classifier far too conservative. See test_classifier.)
194
+ return max(0.0, 1.0 - t_abs / math.sqrt(2.0 + t_abs * t_abs))
190
195
  # Wilson-Hilferty: transform t² ~ F(1, df), then F → chi-square via
191
196
  # cube-root approximation. For our purposes the simpler normal-approx
192
197
  # to the t with the Hill / Abramowitz adjustment is enough.
@@ -178,6 +178,11 @@ def build_payload(
178
178
  "savings_vs_fixed_cap": result.savings_vs_fixed_cap,
179
179
  "convergence_profile_summary": profile_summary,
180
180
  "rollback_triggered": result.outcome in ("oscillating", "diverged"),
181
+ # Index (0-based) of the lowest-error iteration. Lets the receiver
182
+ # derive iterations-to-best (best_index+1) and iterations-past-best
183
+ # (iterations_used-1-best_index) — the "Iteration Waste" view.
184
+ # Privacy-safe: an integer position, no output/error content.
185
+ "best_index": result.best_index,
181
186
  # v2: first computable eta snapshot, for ETA calibration dashboard.
182
187
  # Predicted total iterations = first_eta_at_iteration +
183
188
  # first_eta_prediction; compare to iterations_used to compute the
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: loopgain
3
- Version: 0.4.1
3
+ Version: 0.4.2
4
4
  Summary: An open-source cost controller for AI agent loops. Stops a loop when it has actually converged and rolls back before it degrades — replacing the max_iterations guess with a real-time loop-gain (Aβ) monitor with five named threshold bands and best-so-far rollback.
5
5
  Author-email: Dave Fitzsimmons <hello@loopgain.ai>
6
6
  License: Apache-2.0
@@ -58,7 +58,7 @@ AI agent loops waste time and money when they don't know when to stop. LoopGain
58
58
  [![PyPI](https://img.shields.io/pypi/v/loopgain.svg)](https://pypi.org/project/loopgain/)
59
59
  [![Python](https://img.shields.io/pypi/pyversions/loopgain.svg)](https://pypi.org/project/loopgain/)
60
60
  [![License](https://img.shields.io/badge/license-Apache_2.0-blue.svg)](LICENSE)
61
- [![Tests](https://img.shields.io/badge/tests-202_passing-brightgreen.svg)](tests/)
61
+ [![Tests](https://img.shields.io/badge/tests-200%2B_passing-brightgreen.svg)](tests/)
62
62
 
63
63
  **Home:** [loopgain.ai](https://loopgain.ai)
64
64
 
@@ -223,11 +223,13 @@ def test_loop_length_robustness():
223
223
  - n=8 (df=6): ≥ 90% (the default real-loop length)
224
224
  - n=12 (df=10): ≥ 95%
225
225
  """
226
- # n=4 is intentionally excluded: with df=2 the t-test requires |t|>4.3
227
- # for p<0.05, which is a fundamental statistical-power floor. The
228
- # classifier correctly falls back to STALLING (insufficient evidence)
229
- # for most convergent trajectories at n=4. Documented as a
230
- # min-recommended-iterations limit, not a bug.
226
+ # n=4 is intentionally excluded from the high-accuracy thresholds below:
227
+ # with df=2 the t-test correctly requires |t|>4.30 for p<0.05 (see
228
+ # test_two_sided_t_p_df2_exact), a fundamental statistical-power floor at
229
+ # this length. The classifier falls back to cumulative E_ratio when the
230
+ # slope test is underpowered. This is a min-recommended-iterations limit,
231
+ # not a bug. (Historically the df=2 p-value was computed at 2x its true
232
+ # value, requiring |t|>6.21 and worsening this floor — now fixed.)
231
233
  LEN_THRESHOLDS = {6: 0.80, 8: 0.90, 12: 0.95}
232
234
  for n, threshold in LEN_THRESHOLDS.items():
233
235
  for gen, expected in [
@@ -33,7 +33,42 @@ from loopgain import (
33
33
  classify_trajectory,
34
34
  extract_features,
35
35
  )
36
- from loopgain.classifier import _ols_slope_and_p
36
+ from loopgain.classifier import _ols_slope_and_p, _two_sided_t_p
37
+
38
+
39
+ # ----- Two-sided t p-value closed forms -----
40
+
41
+
42
+ def test_two_sided_t_p_df1_exact():
43
+ """df=1 is the Cauchy distribution: two-sided p = 1 - 2·atan(t)/pi."""
44
+ for t in (0.0, 0.5, 1.0, 2.0, 5.0, 12.706):
45
+ expected = 1.0 - 2.0 * math.atan(t) / math.pi
46
+ assert _two_sided_t_p(t, 1) == pytest.approx(expected, abs=1e-9)
47
+ # t=1 is the median of |T| for df=1 → two-sided p = 0.5.
48
+ assert _two_sided_t_p(1.0, 1) == pytest.approx(0.5, abs=1e-9)
49
+
50
+
51
+ def test_two_sided_t_p_df2_exact():
52
+ """df=2 closed form: two-sided p = 1 - |t|/sqrt(2 + t^2).
53
+
54
+ Regression guard for the doubled-p bug: the critical value for p=0.05
55
+ at df=2 is t=4.302653. The previous implementation returned ~0.10 here
56
+ (2x too large), which forced |t|>6.21 for significance and made the n=4
57
+ classifier far too conservative.
58
+ """
59
+ for t in (0.0, 0.5, 1.0, 2.0, 5.0):
60
+ expected = 1.0 - t / math.sqrt(2.0 + t * t)
61
+ assert _two_sided_t_p(t, 2) == pytest.approx(expected, abs=1e-9)
62
+ # The exact 5% two-sided critical value for df=2.
63
+ assert _two_sided_t_p(4.302653, 2) == pytest.approx(0.05, abs=1e-4)
64
+ # p is a probability: monotone non-increasing in t, bounded to [0, 1].
65
+ assert _two_sided_t_p(0.0, 2) == pytest.approx(1.0, abs=1e-9)
66
+ prev = 1.1
67
+ for t in (0.0, 0.5, 1.0, 2.0, 4.0, 8.0, 50.0):
68
+ p = _two_sided_t_p(t, 2)
69
+ assert 0.0 <= p <= 1.0
70
+ assert p <= prev + 1e-12
71
+ prev = p
37
72
 
38
73
 
39
74
  # ----- OLS slope / p-value building blocks -----
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes