loopgain 0.1.8__tar.gz → 0.2.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.
Files changed (26) hide show
  1. {loopgain-0.1.8 → loopgain-0.2.0}/PKG-INFO +130 -11
  2. {loopgain-0.1.8 → loopgain-0.2.0}/README.md +117 -9
  3. {loopgain-0.1.8 → loopgain-0.2.0}/loopgain/_version.py +1 -1
  4. {loopgain-0.1.8 → loopgain-0.2.0}/loopgain/core.py +21 -11
  5. {loopgain-0.1.8 → loopgain-0.2.0}/loopgain/integrations/__init__.py +31 -12
  6. loopgain-0.2.0/loopgain/integrations/claude_agent_sdk.py +206 -0
  7. loopgain-0.2.0/loopgain/integrations/langchain.py +187 -0
  8. loopgain-0.2.0/loopgain/integrations/openai_agents.py +197 -0
  9. {loopgain-0.1.8 → loopgain-0.2.0}/loopgain/telemetry.py +24 -6
  10. {loopgain-0.1.8 → loopgain-0.2.0}/loopgain.egg-info/PKG-INFO +130 -11
  11. {loopgain-0.1.8 → loopgain-0.2.0}/loopgain.egg-info/SOURCES.txt +3 -0
  12. loopgain-0.2.0/loopgain.egg-info/requires.txt +32 -0
  13. {loopgain-0.1.8 → loopgain-0.2.0}/pyproject.toml +21 -3
  14. {loopgain-0.1.8 → loopgain-0.2.0}/tests/test_core.py +16 -5
  15. {loopgain-0.1.8 → loopgain-0.2.0}/tests/test_stress.py +9 -7
  16. {loopgain-0.1.8 → loopgain-0.2.0}/tests/test_telemetry.py +26 -3
  17. loopgain-0.1.8/loopgain.egg-info/requires.txt +0 -17
  18. {loopgain-0.1.8 → loopgain-0.2.0}/LICENSE +0 -0
  19. {loopgain-0.1.8 → loopgain-0.2.0}/loopgain/__init__.py +0 -0
  20. {loopgain-0.1.8 → loopgain-0.2.0}/loopgain/integrations/autogen.py +0 -0
  21. {loopgain-0.1.8 → loopgain-0.2.0}/loopgain/integrations/crewai.py +0 -0
  22. {loopgain-0.1.8 → loopgain-0.2.0}/loopgain/integrations/langgraph.py +0 -0
  23. {loopgain-0.1.8 → loopgain-0.2.0}/loopgain.egg-info/dependency_links.txt +0 -0
  24. {loopgain-0.1.8 → loopgain-0.2.0}/loopgain.egg-info/top_level.txt +0 -0
  25. {loopgain-0.1.8 → loopgain-0.2.0}/setup.cfg +0 -0
  26. {loopgain-0.1.8 → loopgain-0.2.0}/tests/test_integrations.py +0 -0
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: loopgain
3
- Version: 0.1.8
3
+ Version: 0.2.0
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 <hello@loopgain.ai>
6
6
  License: Apache-2.0
7
7
  Project-URL: Homepage, https://loopgain.ai
8
8
  Project-URL: Repository, https://github.com/loopgain-ai/loopgain
9
9
  Project-URL: Issues, https://github.com/loopgain-ai/loopgain/issues
10
- Keywords: ai,agent,ai-agent,ai-agents,agentic,agentic-ai,llm,llm-agent,llm-orchestration,agent-orchestration,agent-loop,verify-revise,verify-revise-loop,gvr,generator-verifier-reviser,convergence,divergence-detection,infinite-loop,infinite-loop-detection,loop-detection,loop-stability,stability-monitor,early-stopping,max-iterations,barkhausen,barkhausen-criterion,control-theory,feedback-loop,feedback-loop-stability,loop-gain,rollback,best-so-far,langgraph,crewai,autogen,claude,anthropic,openai
10
+ Keywords: ai,agent,ai-agent,ai-agents,agentic,agentic-ai,llm,llm-agent,llm-orchestration,agent-orchestration,agent-loop,verify-revise,verify-revise-loop,gvr,generator-verifier-reviser,convergence,divergence-detection,infinite-loop,infinite-loop-detection,loop-detection,loop-stability,stability-monitor,early-stopping,max-iterations,barkhausen,barkhausen-criterion,control-theory,feedback-loop,feedback-loop-stability,loop-gain,rollback,best-so-far,langgraph,crewai,autogen,langchain,openai-agents,openai-agents-sdk,claude-agent-sdk,claude,anthropic,openai
11
11
  Classifier: Development Status :: 3 - Alpha
12
12
  Classifier: Intended Audience :: Developers
13
13
  Classifier: License :: OSI Approved :: Apache Software License
@@ -30,10 +30,21 @@ Provides-Extra: crewai
30
30
  Requires-Dist: crewai>=0.30; extra == "crewai"
31
31
  Provides-Extra: autogen
32
32
  Requires-Dist: autogen-agentchat>=0.4; extra == "autogen"
33
+ Provides-Extra: langchain
34
+ Requires-Dist: langchain>=1.0; extra == "langchain"
35
+ Provides-Extra: openai-agents
36
+ Requires-Dist: openai-agents>=0.1; extra == "openai-agents"
37
+ Provides-Extra: claude-agent-sdk
38
+ Requires-Dist: claude-agent-sdk>=0.2; extra == "claude-agent-sdk"
33
39
  Provides-Extra: all
34
40
  Requires-Dist: langgraph>=0.2; extra == "all"
35
41
  Requires-Dist: crewai>=0.30; extra == "all"
36
42
  Requires-Dist: autogen-agentchat>=0.4; extra == "all"
43
+ Requires-Dist: langchain>=1.0; extra == "all"
44
+ Requires-Dist: openai-agents>=0.1; extra == "all"
45
+ Requires-Dist: claude-agent-sdk>=0.2; extra == "all"
46
+ Provides-Extra: examples
47
+ Requires-Dist: anthropic>=0.40.0; extra == "examples"
37
48
  Dynamic: license-file
38
49
 
39
50
  # LoopGain
@@ -49,7 +60,7 @@ Replace `max_iterations=5` with a real-time loop-gain (`Aβ`) monitor that knows
49
60
 
50
61
  **Home:** [loopgain.ai](https://loopgain.ai)
51
62
 
52
- Works for **any iterative AI workflow with a measurable error signal** — verify-revise loops, refinement passes, tool-use retry chains, RAG with self-correction, code-gen with linter feedback, multi-step reasoning loops. **Pre-built adapters for [LangGraph](#langgraph), [CrewAI](#crewai), and [AutoGen](#autogen-v04)**; drop-in via the raw API for **Claude Agent SDK** and any custom stack. Pure Python, no runtime dependencies.
63
+ Works for **any iterative AI workflow with a measurable error signal** — verify-revise loops, refinement passes, tool-use retry chains, RAG with self-correction, code-gen with linter feedback, multi-step reasoning loops. **Pre-built adapters for [LangGraph](#langgraph), [CrewAI](#crewai), [AutoGen](#autogen-v04), [LangChain](#langchain), [OpenAI Agents SDK](#openai-agents-sdk), and [Claude Agent SDK](#claude-agent-sdk)**; drop-in via the raw API for any custom stack. Pure Python, no runtime dependencies.
53
64
 
54
65
  **Keywords:** AI agent loops · agentic AI · infinite loop detection · divergence detection · early stopping · convergence · agent orchestration · LLM stability · generator-verifier-reviser · feedback-loop control.
55
66
 
@@ -116,7 +127,7 @@ It classifies `Aβ_smooth` into five named bands:
116
127
  | `0.95 ≤ Aβ ≤ 1.05` | `OSCILLATING` | Break — return best-so-far |
117
128
  | `> 1.05` | `DIVERGING` | Abort — roll back to best-so-far |
118
129
 
119
- Plus a short-circuit: if observed error drops at or below `target_error`, the loop stops immediately with state `TARGET_MET`.
130
+ Plus a short-circuit: if observed error drops at or below `target_error`, the loop stops immediately with state `TARGET_MET`. The default `target_error=0.0` short-circuits on exactly zero error — the natural completion signal for verifier-driven loops. Pass `target_error=None` to disable the short-circuit and rely on stability detection alone.
120
131
 
121
132
  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.
122
133
 
@@ -156,7 +167,7 @@ This transforms divergence detection from "abort with garbage" into "abort with
156
167
 
157
168
  Construct the monitor.
158
169
 
159
- - `target_error` — Stop when an observed error drops at or below this. Default `0.0` means "never short-circuit on target met."
170
+ - `target_error` — Stop when an observed error drops at or below this. Default `0.0` short-circuits on exactly zero error (the natural completion signal for verifier-driven loops). Pass `None` to disable the short-circuit entirely.
160
171
  - `max_iterations` — Hard safety cap. Default `None` (rely on stability detection). Recommended ~20–50 for production.
161
172
  - `thresholds` — Custom `ThresholdBands` if defaults don't fit your domain.
162
173
  - `smoothing_window` — EMA window for the smoothed Aβ. Default 3.
@@ -229,10 +240,13 @@ The hosted endpoint at `telemetry.loopgain.ai` is one acceptable destination. Th
229
240
  Thin wrappers under `loopgain.integrations` drive each major agent framework's iteration with a `LoopGain` monitor and auto-stamp `framework="<name>"` on telemetry. The frameworks themselves are **optional dependencies** — install the extra you need:
230
241
 
231
242
  ```bash
232
- pip install 'loopgain[langgraph]' # LangGraph
233
- pip install 'loopgain[crewai]' # CrewAI
234
- pip install 'loopgain[autogen]' # AutoGen v0.4+
235
- pip install 'loopgain[all]' # all three
243
+ pip install 'loopgain[langgraph]' # LangGraph
244
+ pip install 'loopgain[crewai]' # CrewAI
245
+ pip install 'loopgain[autogen]' # AutoGen v0.4+
246
+ pip install 'loopgain[langchain]' # LangChain (create_agent / AgentExecutor)
247
+ pip install 'loopgain[openai-agents]' # OpenAI Agents SDK
248
+ pip install 'loopgain[claude-agent-sdk]' # Anthropic Claude Agent SDK
249
+ pip install 'loopgain[all]' # all six
236
250
  ```
237
251
 
238
252
  All adapters take a `LoopGain` instance plus an `error_fn` you provide — the framework doesn't know what your error signal is, so the adapter doesn't either. `error_fn` returns a non-negative number (or `None` to skip an iteration).
@@ -319,15 +333,120 @@ lg.send_telemetry(
319
333
 
320
334
  Pass a `cancellation_token` to `adapter.run(...)` and the adapter will cancel it when LoopGain reaches a terminal state (target met, oscillation, divergence). The legacy v0.2 `ConversableAgent.initiate_chat` API is **not** supported — use the v0.4 event-driven runtime.
321
335
 
336
+ ### LangChain
337
+
338
+ Duck-types against any LangChain agent that exposes `.stream(input, **kwargs)` / `.astream(input, **kwargs)` — both the current `langchain.agents.create_agent()` (v1+) and the legacy `AgentExecutor`. The adapter forwards `**stream_kwargs` verbatim, so the chunk shape your `error_fn` sees is the one your agent emits.
339
+
340
+ ```python
341
+ from langchain.agents import create_agent
342
+ from loopgain import LoopGain
343
+ from loopgain.integrations import LangChainAdapter
344
+
345
+ agent = create_agent(model="gpt-5-nano", tools=[get_weather])
346
+ lg = LoopGain(target_error=0.0, max_iterations=20)
347
+
348
+ def error_fn(chunk):
349
+ if chunk.get("type") != "updates":
350
+ return None
351
+ # Count unresolved tool calls; drops to 0 once the agent stops calling tools.
352
+ return sum(
353
+ 1 for _, update in chunk["data"].items()
354
+ if getattr(update.get("messages", [None])[-1], "tool_calls", None)
355
+ )
356
+
357
+ adapter = LangChainAdapter(lg=lg, error_fn=error_fn)
358
+ final = adapter.run(
359
+ agent,
360
+ {"messages": [{"role": "user", "content": "What's the weather?"}]},
361
+ stream_mode="updates",
362
+ version="v2",
363
+ )
364
+
365
+ lg.send_telemetry(
366
+ endpoint=...,
367
+ token=...,
368
+ framework=adapter.framework_name, # "langchain"
369
+ )
370
+ ```
371
+
372
+ For legacy `AgentExecutor`: just drop the `stream_mode` / `version` kwargs; each yielded chunk is an `AddableDict` per step (parse `intermediate_steps` or the terminal `output` key in your `error_fn`).
373
+
374
+ ### OpenAI Agents SDK
375
+
376
+ Wraps `Runner.run_streamed(agent, input).stream_events()`. The SDK is async-first; the adapter mirrors that. A `run_sync` helper wraps the async path with `asyncio.run` for synchronous callers.
377
+
378
+ ```python
379
+ from agents import Agent, function_tool
380
+ from loopgain import LoopGain
381
+ from loopgain.integrations import OpenAIAgentsAdapter
382
+
383
+ agent = Agent(name="Reviser", instructions="...", tools=[...])
384
+
385
+ lg = LoopGain(target_error=0.0, max_iterations=20)
386
+
387
+ def error_fn(event):
388
+ # Default observes only run_item_stream_event; pull the verifier's
389
+ # reported failure count off tool outputs.
390
+ if event.item.type == "tool_call_output_item":
391
+ return float(event.item.output.get("failures", 0))
392
+ return None
393
+
394
+ adapter = OpenAIAgentsAdapter(lg=lg, error_fn=error_fn)
395
+ result = await adapter.run(agent, input="Fix the bug.")
396
+ print(result.final_output)
397
+
398
+ lg.send_telemetry(
399
+ endpoint=...,
400
+ token=...,
401
+ framework=adapter.framework_name, # "openai-agents"
402
+ )
403
+ ```
404
+
405
+ By default the adapter only forwards `run_item_stream_event` to `error_fn` — pass `observe_event_types=None` to see every event (including raw token deltas and agent-handoff notifications). When LoopGain reaches a terminal state, the adapter best-effort calls `.cancel()` on the underlying `RunResultStreaming`.
406
+
407
+ ### Claude Agent SDK
408
+
409
+ Wraps Anthropic's `claude_agent_sdk.query(prompt=..., options=...)` async iterator. By default observes only `AssistantMessage` (skips `UserMessage` / `SystemMessage` / `ResultMessage`); override with `observe_message_types=None` or a custom tuple.
410
+
411
+ ```python
412
+ from claude_agent_sdk import ClaudeAgentOptions, TextBlock
413
+ from loopgain import LoopGain
414
+ from loopgain.integrations import ClaudeAgentSDKAdapter
415
+
416
+ def error_fn(message):
417
+ # Count `FAIL:` markers a self-verifying persona emits.
418
+ for block in getattr(message, "content", []):
419
+ if isinstance(block, TextBlock):
420
+ return float(block.text.count("FAIL:"))
421
+ return None
422
+
423
+ lg = LoopGain(target_error=0.0, max_iterations=20)
424
+ adapter = ClaudeAgentSDKAdapter(lg=lg, error_fn=error_fn)
425
+
426
+ options = ClaudeAgentOptions(system_prompt="Self-verify each draft.")
427
+ result = await adapter.run(
428
+ prompt="Write a haiku about feedback loops.",
429
+ options=options,
430
+ )
431
+
432
+ lg.send_telemetry(
433
+ endpoint=...,
434
+ token=...,
435
+ framework=adapter.framework_name, # "claude-agent-sdk"
436
+ )
437
+ ```
438
+
439
+ For the bidirectional `ClaudeSDKClient` use case, pass `message_iterator=client.receive_messages()` instead of `prompt=...`.
440
+
322
441
  ### Custom integrations
323
442
 
324
- For frameworks without an adapter, the raw `LoopGain.observe()` API works against any iterable. The adapters are 100-200 lines each — copy one of `loopgain/integrations/{langgraph,crewai,autogen}.py` as a starting point.
443
+ For frameworks without an adapter, the raw `LoopGain.observe()` API works against any iterable. The adapters are 100-200 lines each — copy one of `loopgain/integrations/{langgraph,crewai,autogen,langchain,openai_agents,claude_agent_sdk}.py` as a starting point.
325
444
 
326
445
  ---
327
446
 
328
447
  ## Status
329
448
 
330
- **Initial public release.** Core library shipped (current version: see the PyPI badge at the top). Framework adapters (LangGraph, CrewAI, AutoGen) are installable as optional extras. The cloud-aggregator [telemetry receiver](https://github.com/loopgain-ai/telemetry-receiver) and [dashboard](https://github.com/loopgain-ai/dashboard) are live as separate open-source repos. The math and the API surface are stable.
449
+ **Initial public release.** Core library shipped (current version: see the PyPI badge at the top). Framework adapters (LangGraph, CrewAI, AutoGen, LangChain, OpenAI Agents SDK, Claude Agent SDK) are installable as optional extras. The cloud-aggregator [telemetry receiver](https://github.com/loopgain-ai/telemetry-receiver) and [dashboard](https://github.com/loopgain-ai/dashboard) are live as separate open-source repos. The math and the API surface are stable.
331
450
 
332
451
  This is alpha software. The API may break before 1.0 if production usage surfaces design issues; pin the version.
333
452
 
@@ -11,7 +11,7 @@ Replace `max_iterations=5` with a real-time loop-gain (`Aβ`) monitor that knows
11
11
 
12
12
  **Home:** [loopgain.ai](https://loopgain.ai)
13
13
 
14
- Works for **any iterative AI workflow with a measurable error signal** — verify-revise loops, refinement passes, tool-use retry chains, RAG with self-correction, code-gen with linter feedback, multi-step reasoning loops. **Pre-built adapters for [LangGraph](#langgraph), [CrewAI](#crewai), and [AutoGen](#autogen-v04)**; drop-in via the raw API for **Claude Agent SDK** and any custom stack. Pure Python, no runtime dependencies.
14
+ Works for **any iterative AI workflow with a measurable error signal** — verify-revise loops, refinement passes, tool-use retry chains, RAG with self-correction, code-gen with linter feedback, multi-step reasoning loops. **Pre-built adapters for [LangGraph](#langgraph), [CrewAI](#crewai), [AutoGen](#autogen-v04), [LangChain](#langchain), [OpenAI Agents SDK](#openai-agents-sdk), and [Claude Agent SDK](#claude-agent-sdk)**; drop-in via the raw API for any custom stack. Pure Python, no runtime dependencies.
15
15
 
16
16
  **Keywords:** AI agent loops · agentic AI · infinite loop detection · divergence detection · early stopping · convergence · agent orchestration · LLM stability · generator-verifier-reviser · feedback-loop control.
17
17
 
@@ -78,7 +78,7 @@ It classifies `Aβ_smooth` into five named bands:
78
78
  | `0.95 ≤ Aβ ≤ 1.05` | `OSCILLATING` | Break — return best-so-far |
79
79
  | `> 1.05` | `DIVERGING` | Abort — roll back to best-so-far |
80
80
 
81
- Plus a short-circuit: if observed error drops at or below `target_error`, the loop stops immediately with state `TARGET_MET`.
81
+ Plus a short-circuit: if observed error drops at or below `target_error`, the loop stops immediately with state `TARGET_MET`. The default `target_error=0.0` short-circuits on exactly zero error — the natural completion signal for verifier-driven loops. Pass `target_error=None` to disable the short-circuit and rely on stability detection alone.
82
82
 
83
83
  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.
84
84
 
@@ -118,7 +118,7 @@ This transforms divergence detection from "abort with garbage" into "abort with
118
118
 
119
119
  Construct the monitor.
120
120
 
121
- - `target_error` — Stop when an observed error drops at or below this. Default `0.0` means "never short-circuit on target met."
121
+ - `target_error` — Stop when an observed error drops at or below this. Default `0.0` short-circuits on exactly zero error (the natural completion signal for verifier-driven loops). Pass `None` to disable the short-circuit entirely.
122
122
  - `max_iterations` — Hard safety cap. Default `None` (rely on stability detection). Recommended ~20–50 for production.
123
123
  - `thresholds` — Custom `ThresholdBands` if defaults don't fit your domain.
124
124
  - `smoothing_window` — EMA window for the smoothed Aβ. Default 3.
@@ -191,10 +191,13 @@ The hosted endpoint at `telemetry.loopgain.ai` is one acceptable destination. Th
191
191
  Thin wrappers under `loopgain.integrations` drive each major agent framework's iteration with a `LoopGain` monitor and auto-stamp `framework="<name>"` on telemetry. The frameworks themselves are **optional dependencies** — install the extra you need:
192
192
 
193
193
  ```bash
194
- pip install 'loopgain[langgraph]' # LangGraph
195
- pip install 'loopgain[crewai]' # CrewAI
196
- pip install 'loopgain[autogen]' # AutoGen v0.4+
197
- pip install 'loopgain[all]' # all three
194
+ pip install 'loopgain[langgraph]' # LangGraph
195
+ pip install 'loopgain[crewai]' # CrewAI
196
+ pip install 'loopgain[autogen]' # AutoGen v0.4+
197
+ pip install 'loopgain[langchain]' # LangChain (create_agent / AgentExecutor)
198
+ pip install 'loopgain[openai-agents]' # OpenAI Agents SDK
199
+ pip install 'loopgain[claude-agent-sdk]' # Anthropic Claude Agent SDK
200
+ pip install 'loopgain[all]' # all six
198
201
  ```
199
202
 
200
203
  All adapters take a `LoopGain` instance plus an `error_fn` you provide — the framework doesn't know what your error signal is, so the adapter doesn't either. `error_fn` returns a non-negative number (or `None` to skip an iteration).
@@ -281,15 +284,120 @@ lg.send_telemetry(
281
284
 
282
285
  Pass a `cancellation_token` to `adapter.run(...)` and the adapter will cancel it when LoopGain reaches a terminal state (target met, oscillation, divergence). The legacy v0.2 `ConversableAgent.initiate_chat` API is **not** supported — use the v0.4 event-driven runtime.
283
286
 
287
+ ### LangChain
288
+
289
+ Duck-types against any LangChain agent that exposes `.stream(input, **kwargs)` / `.astream(input, **kwargs)` — both the current `langchain.agents.create_agent()` (v1+) and the legacy `AgentExecutor`. The adapter forwards `**stream_kwargs` verbatim, so the chunk shape your `error_fn` sees is the one your agent emits.
290
+
291
+ ```python
292
+ from langchain.agents import create_agent
293
+ from loopgain import LoopGain
294
+ from loopgain.integrations import LangChainAdapter
295
+
296
+ agent = create_agent(model="gpt-5-nano", tools=[get_weather])
297
+ lg = LoopGain(target_error=0.0, max_iterations=20)
298
+
299
+ def error_fn(chunk):
300
+ if chunk.get("type") != "updates":
301
+ return None
302
+ # Count unresolved tool calls; drops to 0 once the agent stops calling tools.
303
+ return sum(
304
+ 1 for _, update in chunk["data"].items()
305
+ if getattr(update.get("messages", [None])[-1], "tool_calls", None)
306
+ )
307
+
308
+ adapter = LangChainAdapter(lg=lg, error_fn=error_fn)
309
+ final = adapter.run(
310
+ agent,
311
+ {"messages": [{"role": "user", "content": "What's the weather?"}]},
312
+ stream_mode="updates",
313
+ version="v2",
314
+ )
315
+
316
+ lg.send_telemetry(
317
+ endpoint=...,
318
+ token=...,
319
+ framework=adapter.framework_name, # "langchain"
320
+ )
321
+ ```
322
+
323
+ For legacy `AgentExecutor`: just drop the `stream_mode` / `version` kwargs; each yielded chunk is an `AddableDict` per step (parse `intermediate_steps` or the terminal `output` key in your `error_fn`).
324
+
325
+ ### OpenAI Agents SDK
326
+
327
+ Wraps `Runner.run_streamed(agent, input).stream_events()`. The SDK is async-first; the adapter mirrors that. A `run_sync` helper wraps the async path with `asyncio.run` for synchronous callers.
328
+
329
+ ```python
330
+ from agents import Agent, function_tool
331
+ from loopgain import LoopGain
332
+ from loopgain.integrations import OpenAIAgentsAdapter
333
+
334
+ agent = Agent(name="Reviser", instructions="...", tools=[...])
335
+
336
+ lg = LoopGain(target_error=0.0, max_iterations=20)
337
+
338
+ def error_fn(event):
339
+ # Default observes only run_item_stream_event; pull the verifier's
340
+ # reported failure count off tool outputs.
341
+ if event.item.type == "tool_call_output_item":
342
+ return float(event.item.output.get("failures", 0))
343
+ return None
344
+
345
+ adapter = OpenAIAgentsAdapter(lg=lg, error_fn=error_fn)
346
+ result = await adapter.run(agent, input="Fix the bug.")
347
+ print(result.final_output)
348
+
349
+ lg.send_telemetry(
350
+ endpoint=...,
351
+ token=...,
352
+ framework=adapter.framework_name, # "openai-agents"
353
+ )
354
+ ```
355
+
356
+ By default the adapter only forwards `run_item_stream_event` to `error_fn` — pass `observe_event_types=None` to see every event (including raw token deltas and agent-handoff notifications). When LoopGain reaches a terminal state, the adapter best-effort calls `.cancel()` on the underlying `RunResultStreaming`.
357
+
358
+ ### Claude Agent SDK
359
+
360
+ Wraps Anthropic's `claude_agent_sdk.query(prompt=..., options=...)` async iterator. By default observes only `AssistantMessage` (skips `UserMessage` / `SystemMessage` / `ResultMessage`); override with `observe_message_types=None` or a custom tuple.
361
+
362
+ ```python
363
+ from claude_agent_sdk import ClaudeAgentOptions, TextBlock
364
+ from loopgain import LoopGain
365
+ from loopgain.integrations import ClaudeAgentSDKAdapter
366
+
367
+ def error_fn(message):
368
+ # Count `FAIL:` markers a self-verifying persona emits.
369
+ for block in getattr(message, "content", []):
370
+ if isinstance(block, TextBlock):
371
+ return float(block.text.count("FAIL:"))
372
+ return None
373
+
374
+ lg = LoopGain(target_error=0.0, max_iterations=20)
375
+ adapter = ClaudeAgentSDKAdapter(lg=lg, error_fn=error_fn)
376
+
377
+ options = ClaudeAgentOptions(system_prompt="Self-verify each draft.")
378
+ result = await adapter.run(
379
+ prompt="Write a haiku about feedback loops.",
380
+ options=options,
381
+ )
382
+
383
+ lg.send_telemetry(
384
+ endpoint=...,
385
+ token=...,
386
+ framework=adapter.framework_name, # "claude-agent-sdk"
387
+ )
388
+ ```
389
+
390
+ For the bidirectional `ClaudeSDKClient` use case, pass `message_iterator=client.receive_messages()` instead of `prompt=...`.
391
+
284
392
  ### Custom integrations
285
393
 
286
- For frameworks without an adapter, the raw `LoopGain.observe()` API works against any iterable. The adapters are 100-200 lines each — copy one of `loopgain/integrations/{langgraph,crewai,autogen}.py` as a starting point.
394
+ For frameworks without an adapter, the raw `LoopGain.observe()` API works against any iterable. The adapters are 100-200 lines each — copy one of `loopgain/integrations/{langgraph,crewai,autogen,langchain,openai_agents,claude_agent_sdk}.py` as a starting point.
287
395
 
288
396
  ---
289
397
 
290
398
  ## Status
291
399
 
292
- **Initial public release.** Core library shipped (current version: see the PyPI badge at the top). Framework adapters (LangGraph, CrewAI, AutoGen) are installable as optional extras. The cloud-aggregator [telemetry receiver](https://github.com/loopgain-ai/telemetry-receiver) and [dashboard](https://github.com/loopgain-ai/dashboard) are live as separate open-source repos. The math and the API surface are stable.
400
+ **Initial public release.** Core library shipped (current version: see the PyPI badge at the top). Framework adapters (LangGraph, CrewAI, AutoGen, LangChain, OpenAI Agents SDK, Claude Agent SDK) are installable as optional extras. The cloud-aggregator [telemetry receiver](https://github.com/loopgain-ai/telemetry-receiver) and [dashboard](https://github.com/loopgain-ai/dashboard) are live as separate open-source repos. The math and the API surface are stable.
293
401
 
294
402
  This is alpha software. The API may break before 1.0 if production usage surfaces design issues; pin the version.
295
403
 
@@ -6,4 +6,4 @@ Both ``loopgain/__init__.py`` and ``loopgain/telemetry.py`` import
6
6
  Update this file (and ``pyproject.toml``) for each release.
7
7
  """
8
8
 
9
- __version__ = "0.1.8"
9
+ __version__ = "0.2.0"
@@ -113,8 +113,9 @@ class LoopGainResult:
113
113
  """First non-None ``eta`` snapshot captured during the loop —
114
114
  the predicted iterations-remaining at the moment the prediction
115
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)."""
116
+ (e.g., ``target_error`` is ``None`` or ``0`` so the prediction
117
+ formula is undefined, loop never converged toward target, or the
118
+ loop terminated before two observations)."""
118
119
 
119
120
  first_eta_at_iteration: Optional[int] = None
120
121
  """Iteration count when ``first_eta_prediction`` was captured.
@@ -149,7 +150,11 @@ class LoopGain:
149
150
 
150
151
  Args:
151
152
  target_error: Stop when an observed error drops at or below this.
152
- Default ``0.0`` means "never stop early on target met."
153
+ Default ``0.0`` short-circuits on exactly zero error the
154
+ natural completion signal for most verifiers (no failing
155
+ tests, no validation errors, etc.). Pass ``None`` to disable
156
+ the short-circuit entirely and rely only on stability
157
+ detection and ``max_iterations``.
153
158
  max_iterations: Hard safety cap. Default ``None`` (rely on
154
159
  stability detection). Recommended ~20-50 for production.
155
160
  thresholds: Custom ``ThresholdBands``. Default is the canonical
@@ -161,7 +166,7 @@ class LoopGain:
161
166
 
162
167
  def __init__(
163
168
  self,
164
- target_error: float = 0.0,
169
+ target_error: Optional[float] = 0.0,
165
170
  max_iterations: Optional[int] = None,
166
171
  thresholds: Optional[ThresholdBands] = None,
167
172
  smoothing_window: int = 3,
@@ -169,12 +174,14 @@ class LoopGain:
169
174
  ) -> None:
170
175
  if smoothing_window < 1:
171
176
  raise ValueError("smoothing_window must be >= 1")
172
- if target_error < 0:
173
- raise ValueError("target_error must be non-negative")
177
+ if target_error is not None and target_error < 0:
178
+ raise ValueError("target_error must be non-negative or None")
174
179
  if max_iterations is not None and max_iterations < 1:
175
180
  raise ValueError("max_iterations must be >= 1 or None")
176
181
 
177
- self.target_error = float(target_error)
182
+ self.target_error: Optional[float] = (
183
+ float(target_error) if target_error is not None else None
184
+ )
178
185
  self.max_iterations = max_iterations
179
186
  self.thresholds = thresholds or ThresholdBands()
180
187
  self.smoothing_window = smoothing_window
@@ -217,7 +224,9 @@ class LoopGain:
217
224
  self._outputs.append(output)
218
225
 
219
226
  # TARGET_MET short-circuit takes precedence over band classification.
220
- if magnitude <= self.target_error and self.target_error > 0:
227
+ # target_error=None disables the short-circuit entirely; any non-None
228
+ # value (including 0.0) fires TARGET_MET when magnitude <= target.
229
+ if self.target_error is not None and magnitude <= self.target_error:
221
230
  self._state = TARGET_MET
222
231
  self._terminal = True
223
232
  return self._state
@@ -289,12 +298,13 @@ class LoopGain:
289
298
  n_remaining = log(E_target / E_current) / log(Aβ_smooth)
290
299
 
291
300
  Returns ``None`` when the prediction isn't well-defined:
292
- no Aβ yet, ``target_error`` is zero, target already met, or
293
- ``Aβ_smooth >= 1`` (non-converging gain).
301
+ no Aβ yet, ``target_error`` is ``None`` (no target to predict
302
+ against) or ``0`` (the formula has a log-of-zero singularity),
303
+ target already met, or ``Aβ_smooth >= 1`` (non-converging gain).
294
304
  """
295
305
  if not self._smoothed_history or not self._error_history:
296
306
  return None
297
- if self.target_error <= 0:
307
+ if self.target_error is None or self.target_error <= 0:
298
308
  return None
299
309
  e_current = self._error_history[-1]
300
310
  if e_current <= self.target_error:
@@ -5,22 +5,26 @@ calls ``LoopGain.observe()`` on each step with an error magnitude derived
5
5
  from a user-provided ``error_fn``, and (optionally) sends telemetry on
6
6
  completion with ``framework="<name>"`` auto-stamped.
7
7
 
8
- Adapters are isolated submodules so the host frameworks (langgraph, crewai,
9
- autogen) remain *optional* dependencies. Importing this package does not
10
- import any framework each adapter only imports its framework when its
11
- class is instantiated, and surfaces a clear ``ImportError`` if missing.
8
+ Adapters are isolated submodules so the host frameworks (langgraph,
9
+ crewai, autogen, langchain, openai-agents, claude-agent-sdk) remain
10
+ *optional* dependencies. Importing this package does not import any
11
+ framework each adapter only imports its framework when its class is
12
+ instantiated, and surfaces a clear ``ImportError`` if missing.
12
13
 
13
14
  Install adapter extras::
14
15
 
15
- pip install 'loopgain[langgraph]' # LangGraph
16
- pip install 'loopgain[crewai]' # CrewAI
17
- pip install 'loopgain[autogen]' # AutoGen v0.4+
18
- pip install 'loopgain[all]' # all of the above
16
+ pip install 'loopgain[langgraph]' # LangGraph
17
+ pip install 'loopgain[crewai]' # CrewAI
18
+ pip install 'loopgain[autogen]' # AutoGen v0.4+
19
+ pip install 'loopgain[langchain]' # LangChain (create_agent or AgentExecutor)
20
+ pip install 'loopgain[openai-agents]' # OpenAI Agents SDK
21
+ pip install 'loopgain[claude-agent-sdk]' # Anthropic Claude Agent SDK
22
+ pip install 'loopgain[all]' # all of the above
19
23
 
20
24
  Common pattern::
21
25
 
22
26
  from loopgain import LoopGain
23
- from loopgain.integrations import LangGraphAdapter # or CrewAIAdapter, AutoGenAdapter
27
+ from loopgain.integrations import LangGraphAdapter # or any other adapter
24
28
 
25
29
  lg = LoopGain(target_error=0.1, max_iterations=20)
26
30
  adapter = LangGraphAdapter(
@@ -40,13 +44,16 @@ Common pattern::
40
44
 
41
45
  from __future__ import annotations
42
46
 
43
- # Adapters are imported lazily so importing this package does NOT pull in
44
- # langgraph / crewai / autogen. Each name resolves on first attribute access
45
- # and surfaces a clear ImportError if its host framework isn't installed.
47
+ # Adapters are imported lazily so importing this package does NOT pull
48
+ # in any framework. Each name resolves on first attribute access and
49
+ # surfaces a clear ImportError if its host framework isn't installed.
46
50
  __all__ = [
47
51
  "LangGraphAdapter",
48
52
  "CrewAIAdapter",
49
53
  "AutoGenAdapter",
54
+ "LangChainAdapter",
55
+ "OpenAIAgentsAdapter",
56
+ "ClaudeAgentSDKAdapter",
50
57
  ]
51
58
 
52
59
 
@@ -63,4 +70,16 @@ def __getattr__(name: str):
63
70
  from loopgain.integrations.autogen import AutoGenAdapter
64
71
 
65
72
  return AutoGenAdapter
73
+ if name == "LangChainAdapter":
74
+ from loopgain.integrations.langchain import LangChainAdapter
75
+
76
+ return LangChainAdapter
77
+ if name == "OpenAIAgentsAdapter":
78
+ from loopgain.integrations.openai_agents import OpenAIAgentsAdapter
79
+
80
+ return OpenAIAgentsAdapter
81
+ if name == "ClaudeAgentSDKAdapter":
82
+ from loopgain.integrations.claude_agent_sdk import ClaudeAgentSDKAdapter
83
+
84
+ return ClaudeAgentSDKAdapter
66
85
  raise AttributeError(f"module 'loopgain.integrations' has no attribute {name!r}")