loopgain 0.1.2__tar.gz → 0.1.4__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.
- {loopgain-0.1.2 → loopgain-0.1.4}/PKG-INFO +2 -4
- {loopgain-0.1.2 → loopgain-0.1.4}/README.md +1 -3
- {loopgain-0.1.2 → loopgain-0.1.4}/loopgain/__init__.py +1 -1
- {loopgain-0.1.2 → loopgain-0.1.4}/loopgain/core.py +29 -1
- {loopgain-0.1.2 → loopgain-0.1.4}/loopgain/telemetry.py +12 -2
- {loopgain-0.1.2 → loopgain-0.1.4}/loopgain.egg-info/PKG-INFO +2 -4
- {loopgain-0.1.2 → loopgain-0.1.4}/pyproject.toml +1 -1
- {loopgain-0.1.2 → loopgain-0.1.4}/tests/test_core.py +64 -0
- {loopgain-0.1.2 → loopgain-0.1.4}/tests/test_telemetry.py +46 -0
- {loopgain-0.1.2 → loopgain-0.1.4}/LICENSE +0 -0
- {loopgain-0.1.2 → loopgain-0.1.4}/loopgain.egg-info/SOURCES.txt +0 -0
- {loopgain-0.1.2 → loopgain-0.1.4}/loopgain.egg-info/dependency_links.txt +0 -0
- {loopgain-0.1.2 → loopgain-0.1.4}/loopgain.egg-info/requires.txt +0 -0
- {loopgain-0.1.2 → loopgain-0.1.4}/loopgain.egg-info/top_level.txt +0 -0
- {loopgain-0.1.2 → loopgain-0.1.4}/setup.cfg +0 -0
- {loopgain-0.1.2 → loopgain-0.1.4}/tests/test_stress.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: loopgain
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Barkhausen stability monitor for AI agent loops. Real-time loop-gain (Aβ) monitoring with five named threshold bands, best-so-far rollback, and ETA prediction.
|
|
5
5
|
Author-email: Dave Fitzsimmons <dave_fitz@icloud.com>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -101,7 +101,7 @@ Plus a short-circuit: if observed error drops at or below `target_error`, the lo
|
|
|
101
101
|
|
|
102
102
|
The `±0.05` noise band around `Aβ=1` absorbs stochastic jitter from agent outputs without triggering false-positive aborts. The `0.85` `STALLING` boundary is an early warning — by the time `Aβ` crosses `1.0`, you've already wasted iterations.
|
|
103
103
|
|
|
104
|
-
These threshold defaults
|
|
104
|
+
These threshold defaults are derived from the Barkhausen-stability analysis and serve as reasonable starting points. Tune them per domain (via the `ThresholdBands` argument) once you have production traces.
|
|
105
105
|
|
|
106
106
|
---
|
|
107
107
|
|
|
@@ -234,5 +234,3 @@ Loop types this applies to in practice:
|
|
|
234
234
|
- **Code generation with linter/test feedback** — generate, run tests/linter, fix, repeat. Error = failing test count or linter violation count.
|
|
235
235
|
- **Multi-step reasoning loops** — ReAct-style think/act/observe iterations. Error = whatever the agent's quality assessor returns.
|
|
236
236
|
- **Custom feedback loops** — anything where you can produce a number that should drop toward zero as the loop succeeds.
|
|
237
|
-
|
|
238
|
-
See [loopgain.ai](https://loopgain.ai) for the longer write-up.
|
|
@@ -73,7 +73,7 @@ Plus a short-circuit: if observed error drops at or below `target_error`, the lo
|
|
|
73
73
|
|
|
74
74
|
The `±0.05` noise band around `Aβ=1` absorbs stochastic jitter from agent outputs without triggering false-positive aborts. The `0.85` `STALLING` boundary is an early warning — by the time `Aβ` crosses `1.0`, you've already wasted iterations.
|
|
75
75
|
|
|
76
|
-
These threshold defaults
|
|
76
|
+
These threshold defaults are derived from the Barkhausen-stability analysis and serve as reasonable starting points. Tune them per domain (via the `ThresholdBands` argument) once you have production traces.
|
|
77
77
|
|
|
78
78
|
---
|
|
79
79
|
|
|
@@ -206,5 +206,3 @@ Loop types this applies to in practice:
|
|
|
206
206
|
- **Code generation with linter/test feedback** — generate, run tests/linter, fix, repeat. Error = failing test count or linter violation count.
|
|
207
207
|
- **Multi-step reasoning loops** — ReAct-style think/act/observe iterations. Error = whatever the agent's quality assessor returns.
|
|
208
208
|
- **Custom feedback loops** — anything where you can produce a number that should drop toward zero as the loop succeeds.
|
|
209
|
-
|
|
210
|
-
See [loopgain.ai](https://loopgain.ai) for the longer write-up.
|
|
@@ -10,7 +10,7 @@ named states and decides whether to continue, stop, or roll back.
|
|
|
10
10
|
|
|
11
11
|
The math is foundational EE control theory. The product layer is the
|
|
12
12
|
threshold bands, the best-so-far buffer, the ETA prediction, and the
|
|
13
|
-
clean Python API.
|
|
13
|
+
clean Python API.
|
|
14
14
|
|
|
15
15
|
License: Apache-2.0
|
|
16
16
|
"""
|
|
@@ -109,6 +109,19 @@ class LoopGainResult:
|
|
|
109
109
|
"""Iterations saved versus the assumed fixed cap (default 10).
|
|
110
110
|
Zero if the loop hit ``max_iterations``; otherwise non-negative."""
|
|
111
111
|
|
|
112
|
+
first_eta_prediction: Optional[int] = None
|
|
113
|
+
"""First non-None ``eta`` snapshot captured during the loop —
|
|
114
|
+
the predicted iterations-remaining at the moment the prediction
|
|
115
|
+
became computable. ``None`` if no prediction was ever made
|
|
116
|
+
(e.g., ``target_error == 0``, loop never converged toward target,
|
|
117
|
+
or the loop terminated before two observations)."""
|
|
118
|
+
|
|
119
|
+
first_eta_at_iteration: Optional[int] = None
|
|
120
|
+
"""Iteration count when ``first_eta_prediction`` was captured.
|
|
121
|
+
``None`` if no prediction was ever made. Predicted *total*
|
|
122
|
+
iterations = ``first_eta_at_iteration + first_eta_prediction``,
|
|
123
|
+
comparable to ``iterations_used`` for calibration."""
|
|
124
|
+
|
|
112
125
|
|
|
113
126
|
class LoopGain:
|
|
114
127
|
"""Barkhausen stability monitor for AI agent loops.
|
|
@@ -173,6 +186,8 @@ class LoopGain:
|
|
|
173
186
|
self._outputs: list[Any] = []
|
|
174
187
|
self._state: str = INIT
|
|
175
188
|
self._terminal: bool = False
|
|
189
|
+
self._first_eta_prediction: Optional[int] = None
|
|
190
|
+
self._first_eta_at_iteration: Optional[int] = None
|
|
176
191
|
|
|
177
192
|
# ----- Public observation API -----
|
|
178
193
|
|
|
@@ -237,6 +252,17 @@ class LoopGain:
|
|
|
237
252
|
self._state = MAX_ITERATIONS
|
|
238
253
|
self._terminal = True
|
|
239
254
|
|
|
255
|
+
# Snapshot the first computable eta prediction for calibration.
|
|
256
|
+
# eta is None until smoothing settles and the loop looks convergent;
|
|
257
|
+
# we capture the *first* value it produces and the iteration it was
|
|
258
|
+
# produced at, so predicted_total = at_iter + eta is comparable to
|
|
259
|
+
# iterations_used.
|
|
260
|
+
if self._first_eta_prediction is None:
|
|
261
|
+
eta_now = self.eta
|
|
262
|
+
if eta_now is not None and eta_now > 0:
|
|
263
|
+
self._first_eta_prediction = eta_now
|
|
264
|
+
self._first_eta_at_iteration = len(self._error_history)
|
|
265
|
+
|
|
240
266
|
return self._state
|
|
241
267
|
|
|
242
268
|
def should_continue(self) -> bool:
|
|
@@ -333,6 +359,8 @@ class LoopGain:
|
|
|
333
359
|
error_history=list(self._error_history),
|
|
334
360
|
gain_margin=self.gain_margin,
|
|
335
361
|
savings_vs_fixed_cap=savings,
|
|
362
|
+
first_eta_prediction=self._first_eta_prediction,
|
|
363
|
+
first_eta_at_iteration=self._first_eta_at_iteration,
|
|
336
364
|
)
|
|
337
365
|
|
|
338
366
|
# ----- Internal helpers -----
|
|
@@ -25,10 +25,13 @@ if TYPE_CHECKING:
|
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
# Schema version is incremented when the payload format breaks compatibility.
|
|
28
|
-
|
|
28
|
+
# v2 (2026-05-13) adds first_eta_prediction + first_eta_at_iteration for the
|
|
29
|
+
# ETA Accuracy dashboard panel. Receiver remains backward-compatible: v1
|
|
30
|
+
# payloads are still accepted (new fields default to None).
|
|
31
|
+
SCHEMA_VERSION = 2
|
|
29
32
|
|
|
30
33
|
# Library version (kept in sync with __init__.py).
|
|
31
|
-
LIBRARY_VERSION = "0.1.
|
|
34
|
+
LIBRARY_VERSION = "0.1.4"
|
|
32
35
|
|
|
33
36
|
|
|
34
37
|
def build_payload(
|
|
@@ -83,6 +86,13 @@ def build_payload(
|
|
|
83
86
|
"savings_vs_fixed_cap": result.savings_vs_fixed_cap,
|
|
84
87
|
"convergence_profile_summary": profile_summary,
|
|
85
88
|
"rollback_triggered": result.outcome in ("oscillating", "diverged"),
|
|
89
|
+
# v2: first computable eta snapshot, for ETA calibration dashboard.
|
|
90
|
+
# Predicted total iterations = first_eta_at_iteration +
|
|
91
|
+
# first_eta_prediction; compare to iterations_used to compute the
|
|
92
|
+
# calibration error. Both are None when no prediction was made
|
|
93
|
+
# (target_error=0, loop never looked convergent, etc.).
|
|
94
|
+
"first_eta_prediction": result.first_eta_prediction,
|
|
95
|
+
"first_eta_at_iteration": result.first_eta_at_iteration,
|
|
86
96
|
},
|
|
87
97
|
"thresholds": {
|
|
88
98
|
"fast_converge": lg.thresholds.fast_converge,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: loopgain
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Barkhausen stability monitor for AI agent loops. Real-time loop-gain (Aβ) monitoring with five named threshold bands, best-so-far rollback, and ETA prediction.
|
|
5
5
|
Author-email: Dave Fitzsimmons <dave_fitz@icloud.com>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -101,7 +101,7 @@ Plus a short-circuit: if observed error drops at or below `target_error`, the lo
|
|
|
101
101
|
|
|
102
102
|
The `±0.05` noise band around `Aβ=1` absorbs stochastic jitter from agent outputs without triggering false-positive aborts. The `0.85` `STALLING` boundary is an early warning — by the time `Aβ` crosses `1.0`, you've already wasted iterations.
|
|
103
103
|
|
|
104
|
-
These threshold defaults
|
|
104
|
+
These threshold defaults are derived from the Barkhausen-stability analysis and serve as reasonable starting points. Tune them per domain (via the `ThresholdBands` argument) once you have production traces.
|
|
105
105
|
|
|
106
106
|
---
|
|
107
107
|
|
|
@@ -234,5 +234,3 @@ Loop types this applies to in practice:
|
|
|
234
234
|
- **Code generation with linter/test feedback** — generate, run tests/linter, fix, repeat. Error = failing test count or linter violation count.
|
|
235
235
|
- **Multi-step reasoning loops** — ReAct-style think/act/observe iterations. Error = whatever the agent's quality assessor returns.
|
|
236
236
|
- **Custom feedback loops** — anything where you can produce a number that should drop toward zero as the loop succeeds.
|
|
237
|
-
|
|
238
|
-
See [loopgain.ai](https://loopgain.ai) for the longer write-up.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "loopgain"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.4"
|
|
8
8
|
description = "Barkhausen stability monitor for AI agent loops. Real-time loop-gain (Aβ) monitoring with five named threshold bands, best-so-far rollback, and ETA prediction."
|
|
9
9
|
authors = [{name = "Dave Fitzsimmons", email = "dave_fitz@icloud.com"}]
|
|
10
10
|
readme = "README.md"
|
|
@@ -199,6 +199,70 @@ def test_eta_none_when_target_is_zero():
|
|
|
199
199
|
assert lg.eta is None
|
|
200
200
|
|
|
201
201
|
|
|
202
|
+
# ----- First-eta snapshot (for ETA Accuracy dashboard panel) -----
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def test_first_eta_snapshot_captured_during_converging_run():
|
|
206
|
+
"""Result carries the first non-None eta and the iter it was made at."""
|
|
207
|
+
target = 0.1
|
|
208
|
+
errors = _decay(0.5, e0=100.0)
|
|
209
|
+
lg = LoopGain(target_error=target, max_iterations=100)
|
|
210
|
+
for e in errors:
|
|
211
|
+
lg.observe(e)
|
|
212
|
+
if not lg.should_continue():
|
|
213
|
+
break
|
|
214
|
+
|
|
215
|
+
result = lg.result
|
|
216
|
+
# First eta becomes computable on the 2nd observation (smoothed_history
|
|
217
|
+
# exists, target > 0, current > target, Aβ_smooth < 1).
|
|
218
|
+
assert result.first_eta_at_iteration == 2
|
|
219
|
+
assert result.first_eta_prediction is not None
|
|
220
|
+
assert result.first_eta_prediction > 0
|
|
221
|
+
# Predicted total iterations should be within ±2 of actual (the prediction
|
|
222
|
+
# is made early, before smoothing has fully settled).
|
|
223
|
+
predicted_total = result.first_eta_at_iteration + result.first_eta_prediction
|
|
224
|
+
assert abs(predicted_total - result.iterations_used) <= 2
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def test_first_eta_snapshot_none_when_target_is_zero():
|
|
228
|
+
"""No prediction is captured when target_error=0 (eta is always None)."""
|
|
229
|
+
lg = LoopGain(target_error=0.0, max_iterations=5)
|
|
230
|
+
for _ in range(5):
|
|
231
|
+
lg.observe(10.0)
|
|
232
|
+
result = lg.result
|
|
233
|
+
assert result.first_eta_prediction is None
|
|
234
|
+
assert result.first_eta_at_iteration is None
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def test_first_eta_snapshot_none_when_loop_never_converges():
|
|
238
|
+
"""Oscillating loop (Aβ ≈ 1) never produces a positive eta."""
|
|
239
|
+
lg = LoopGain(target_error=0.5)
|
|
240
|
+
for _ in range(5):
|
|
241
|
+
lg.observe(10.0)
|
|
242
|
+
if not lg.should_continue():
|
|
243
|
+
break
|
|
244
|
+
result = lg.result
|
|
245
|
+
assert result.first_eta_prediction is None
|
|
246
|
+
assert result.first_eta_at_iteration is None
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def test_first_eta_snapshot_is_idempotent():
|
|
250
|
+
"""Subsequent observations don't overwrite the first prediction."""
|
|
251
|
+
target = 0.1
|
|
252
|
+
errors = _decay(0.5, e0=100.0)
|
|
253
|
+
lg = LoopGain(target_error=target, max_iterations=100)
|
|
254
|
+
lg.observe(errors[0])
|
|
255
|
+
lg.observe(errors[1])
|
|
256
|
+
first = lg._first_eta_prediction
|
|
257
|
+
first_iter = lg._first_eta_at_iteration
|
|
258
|
+
assert first is not None
|
|
259
|
+
# Run a few more iterations; the snapshot should not change.
|
|
260
|
+
for e in errors[2:6]:
|
|
261
|
+
lg.observe(e)
|
|
262
|
+
assert lg._first_eta_prediction == first
|
|
263
|
+
assert lg._first_eta_at_iteration == first_iter
|
|
264
|
+
|
|
265
|
+
|
|
202
266
|
# ----- observe() input coercion -----
|
|
203
267
|
|
|
204
268
|
|
|
@@ -172,6 +172,52 @@ def test_payload_for_not_started_loop():
|
|
|
172
172
|
assert p["loop"]["convergence_profile_summary"]["samples"] == 0
|
|
173
173
|
|
|
174
174
|
|
|
175
|
+
# ----- v2 schema: ETA calibration fields -----
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def test_payload_schema_version_is_v2():
|
|
179
|
+
"""Schema bumped to v2 with the addition of first_eta_* fields."""
|
|
180
|
+
assert SCHEMA_VERSION == 2
|
|
181
|
+
lg = _make_terminated_loop()
|
|
182
|
+
p = build_payload(lg)
|
|
183
|
+
assert p["schema_version"] == 2
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def test_payload_includes_first_eta_fields_when_loop_converged():
|
|
187
|
+
"""A converging loop produces a captured eta snapshot."""
|
|
188
|
+
lg = _make_terminated_loop()
|
|
189
|
+
p = build_payload(lg)
|
|
190
|
+
loop = p["loop"]
|
|
191
|
+
assert "first_eta_prediction" in loop
|
|
192
|
+
assert "first_eta_at_iteration" in loop
|
|
193
|
+
assert loop["first_eta_prediction"] is not None
|
|
194
|
+
assert loop["first_eta_at_iteration"] is not None
|
|
195
|
+
assert loop["first_eta_prediction"] > 0
|
|
196
|
+
assert loop["first_eta_at_iteration"] >= 2
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def test_payload_first_eta_none_for_target_zero():
|
|
200
|
+
"""target_error=0 means eta is never computable; both fields are None."""
|
|
201
|
+
lg = LoopGain(target_error=0.0, max_iterations=4)
|
|
202
|
+
for _ in range(4):
|
|
203
|
+
lg.observe(10.0)
|
|
204
|
+
p = build_payload(lg)
|
|
205
|
+
assert p["loop"]["first_eta_prediction"] is None
|
|
206
|
+
assert p["loop"]["first_eta_at_iteration"] is None
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def test_payload_first_eta_none_for_diverging_loop():
|
|
210
|
+
"""A divergent loop never produces a positive eta."""
|
|
211
|
+
lg = LoopGain(target_error=0.5, max_iterations=20)
|
|
212
|
+
for e in [10.0, 12.0, 15.0, 20.0, 30.0]:
|
|
213
|
+
if not lg.should_continue():
|
|
214
|
+
break
|
|
215
|
+
lg.observe(e)
|
|
216
|
+
p = build_payload(lg)
|
|
217
|
+
assert p["loop"]["first_eta_prediction"] is None
|
|
218
|
+
assert p["loop"]["first_eta_at_iteration"] is None
|
|
219
|
+
|
|
220
|
+
|
|
175
221
|
# ----- send_payload behavior -----
|
|
176
222
|
|
|
177
223
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|