loopgain 0.1.9__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 (25) hide show
  1. {loopgain-0.1.9 → loopgain-0.2.0}/PKG-INFO +126 -9
  2. {loopgain-0.1.9 → loopgain-0.2.0}/README.md +115 -7
  3. {loopgain-0.1.9 → loopgain-0.2.0}/loopgain/_version.py +1 -1
  4. {loopgain-0.1.9 → loopgain-0.2.0}/loopgain/integrations/__init__.py +31 -12
  5. loopgain-0.2.0/loopgain/integrations/claude_agent_sdk.py +206 -0
  6. loopgain-0.2.0/loopgain/integrations/langchain.py +187 -0
  7. loopgain-0.2.0/loopgain/integrations/openai_agents.py +197 -0
  8. {loopgain-0.1.9 → loopgain-0.2.0}/loopgain.egg-info/PKG-INFO +126 -9
  9. {loopgain-0.1.9 → loopgain-0.2.0}/loopgain.egg-info/SOURCES.txt +3 -0
  10. {loopgain-0.1.9 → loopgain-0.2.0}/loopgain.egg-info/requires.txt +12 -0
  11. {loopgain-0.1.9 → loopgain-0.2.0}/pyproject.toml +17 -3
  12. {loopgain-0.1.9 → loopgain-0.2.0}/LICENSE +0 -0
  13. {loopgain-0.1.9 → loopgain-0.2.0}/loopgain/__init__.py +0 -0
  14. {loopgain-0.1.9 → loopgain-0.2.0}/loopgain/core.py +0 -0
  15. {loopgain-0.1.9 → loopgain-0.2.0}/loopgain/integrations/autogen.py +0 -0
  16. {loopgain-0.1.9 → loopgain-0.2.0}/loopgain/integrations/crewai.py +0 -0
  17. {loopgain-0.1.9 → loopgain-0.2.0}/loopgain/integrations/langgraph.py +0 -0
  18. {loopgain-0.1.9 → loopgain-0.2.0}/loopgain/telemetry.py +0 -0
  19. {loopgain-0.1.9 → loopgain-0.2.0}/loopgain.egg-info/dependency_links.txt +0 -0
  20. {loopgain-0.1.9 → loopgain-0.2.0}/loopgain.egg-info/top_level.txt +0 -0
  21. {loopgain-0.1.9 → loopgain-0.2.0}/setup.cfg +0 -0
  22. {loopgain-0.1.9 → loopgain-0.2.0}/tests/test_core.py +0 -0
  23. {loopgain-0.1.9 → loopgain-0.2.0}/tests/test_integrations.py +0 -0
  24. {loopgain-0.1.9 → loopgain-0.2.0}/tests/test_stress.py +0 -0
  25. {loopgain-0.1.9 → loopgain-0.2.0}/tests/test_telemetry.py +0 -0
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: loopgain
3
- Version: 0.1.9
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,19 @@ 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"
37
46
  Provides-Extra: examples
38
47
  Requires-Dist: anthropic>=0.40.0; extra == "examples"
39
48
  Dynamic: license-file
@@ -51,7 +60,7 @@ Replace `max_iterations=5` with a real-time loop-gain (`Aβ`) monitor that knows
51
60
 
52
61
  **Home:** [loopgain.ai](https://loopgain.ai)
53
62
 
54
- 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.
55
64
 
56
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.
57
66
 
@@ -231,10 +240,13 @@ The hosted endpoint at `telemetry.loopgain.ai` is one acceptable destination. Th
231
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:
232
241
 
233
242
  ```bash
234
- pip install 'loopgain[langgraph]' # LangGraph
235
- pip install 'loopgain[crewai]' # CrewAI
236
- pip install 'loopgain[autogen]' # AutoGen v0.4+
237
- 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
238
250
  ```
239
251
 
240
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).
@@ -321,15 +333,120 @@ lg.send_telemetry(
321
333
 
322
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.
323
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
+
324
441
  ### Custom integrations
325
442
 
326
- 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.
327
444
 
328
445
  ---
329
446
 
330
447
  ## Status
331
448
 
332
- **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.
333
450
 
334
451
  This is alpha software. The API may break before 1.0 if production usage surfaces design issues; pin the version.
335
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
 
@@ -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.9"
9
+ __version__ = "0.2.0"
@@ -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}")
@@ -0,0 +1,206 @@
1
+ """Claude Agent SDK adapter for LoopGain.
2
+
3
+ Wraps Anthropic's ``claude_agent_sdk`` with a LoopGain monitor. The SDK
4
+ is async-only: ``query(prompt=..., options=...)`` returns an async
5
+ iterator of messages, each of which is one of:
6
+
7
+ - ``UserMessage`` — the user-supplied prompt echoed back
8
+ - ``AssistantMessage`` — model output with content blocks (``TextBlock``,
9
+ ``ToolUseBlock``)
10
+ - ``SystemMessage`` — system events
11
+ - ``ResultMessage`` — terminal message with summary fields (cost, usage)
12
+
13
+ The natural iteration unit for an agent loop is one ``AssistantMessage``
14
+ (or one full tool-call → tool-result round-trip). The user's
15
+ ``error_fn`` decides which messages carry an error signal — typically
16
+ by inspecting ``AssistantMessage.content`` for self-reported state or
17
+ counting unresolved ``ToolUseBlock`` entries.
18
+
19
+ The adapter accepts either:
20
+
21
+ - a ``prompt`` (string) and optional ``options`` — the adapter
22
+ constructs the ``query(...)`` iterator itself; or
23
+ - a pre-constructed ``message_iterator`` (e.g. from
24
+ ``ClaudeSDKClient.receive_messages()`` or ``receive_response()``) —
25
+ the adapter just drives it.
26
+
27
+ By default the adapter only forwards ``AssistantMessage`` instances to
28
+ ``error_fn`` (since user/system messages don't typically carry an error
29
+ signal). Override with ``observe_message_types=None`` to observe every
30
+ message, or pass a tuple of types to widen the filter.
31
+
32
+ Reference: https://github.com/anthropics/claude-agent-sdk-python
33
+ """
34
+
35
+ from __future__ import annotations
36
+
37
+ import asyncio
38
+ from typing import TYPE_CHECKING, Any, AsyncIterator, Awaitable, Callable, Optional, Tuple
39
+
40
+ if TYPE_CHECKING:
41
+ from loopgain.core import LoopGain
42
+
43
+
44
+ MessageErrorFn = Callable[[Any], Optional[float]]
45
+ AsyncMessageErrorFn = Callable[[Any], Awaitable[Optional[float]]]
46
+
47
+
48
+ def _default_observe_types() -> Tuple[type, ...]:
49
+ """Default observation filter: ``AssistantMessage`` only.
50
+
51
+ Imported lazily so the adapter module itself stays importable
52
+ without ``claude_agent_sdk`` installed (importing the package is
53
+ what raises ``ImportError`` from ``run()`` if it's missing).
54
+ """
55
+ from claude_agent_sdk import AssistantMessage
56
+
57
+ return (AssistantMessage,)
58
+
59
+
60
+ class ClaudeAgentSDKAdapter:
61
+ """Drive a Claude Agent SDK ``query`` or ``ClaudeSDKClient`` message
62
+ stream with a LoopGain monitor.
63
+
64
+ Args:
65
+ lg: A ``LoopGain`` instance to drive.
66
+ error_fn: Maps one yielded message to an error magnitude. Both
67
+ sync and async callables are accepted. Return ``None`` to
68
+ skip a message. Only messages of a type in
69
+ ``observe_message_types`` are forwarded to ``error_fn``;
70
+ others are yielded but not observed.
71
+ observe_message_types: Tuple of message classes to forward to
72
+ ``error_fn``. Defaults to ``(AssistantMessage,)``. Pass
73
+ ``None`` to observe every message regardless of type. The
74
+ tuple is resolved lazily on first stream so the module
75
+ stays importable without ``claude_agent_sdk`` installed.
76
+
77
+ Example::
78
+
79
+ from claude_agent_sdk import ClaudeAgentOptions, AssistantMessage, TextBlock
80
+ from loopgain import LoopGain
81
+ from loopgain.integrations import ClaudeAgentSDKAdapter
82
+
83
+ def error_fn(message):
84
+ # Count `FAIL:` markers the verifier-persona emits.
85
+ for block in getattr(message, "content", []):
86
+ if isinstance(block, TextBlock):
87
+ return float(block.text.count("FAIL:"))
88
+ return None
89
+
90
+ lg = LoopGain(target_error=0.0, max_iterations=20)
91
+ adapter = ClaudeAgentSDKAdapter(lg=lg, error_fn=error_fn)
92
+
93
+ options = ClaudeAgentOptions(system_prompt="Self-verify each draft.")
94
+ result = await adapter.run(
95
+ prompt="Write a haiku about feedback loops.",
96
+ options=options,
97
+ )
98
+ """
99
+
100
+ framework_name = "claude-agent-sdk"
101
+
102
+ def __init__(
103
+ self,
104
+ lg: "LoopGain",
105
+ error_fn: MessageErrorFn,
106
+ observe_message_types: Optional[Tuple[type, ...]] = (),
107
+ ) -> None:
108
+ self.lg = lg
109
+ self.error_fn = error_fn
110
+ # Sentinel: empty tuple → use defaults on first stream.
111
+ # ``None`` → observe everything. Otherwise the user-supplied
112
+ # tuple is honored verbatim.
113
+ self._observe_types_arg = observe_message_types
114
+ self._resolved_observe_types: Optional[Tuple[type, ...]] = None
115
+
116
+ def _resolve_observe_types(self) -> Optional[Tuple[type, ...]]:
117
+ if self._observe_types_arg is None:
118
+ return None
119
+ if self._resolved_observe_types is None:
120
+ if self._observe_types_arg == ():
121
+ self._resolved_observe_types = _default_observe_types()
122
+ else:
123
+ self._resolved_observe_types = tuple(self._observe_types_arg)
124
+ return self._resolved_observe_types
125
+
126
+ async def run(
127
+ self,
128
+ prompt: Optional[Any] = None,
129
+ *,
130
+ options: Optional[Any] = None,
131
+ message_iterator: Optional[AsyncIterator[Any]] = None,
132
+ ) -> list:
133
+ """Drive a message stream to completion, returning the full
134
+ list of yielded messages.
135
+
136
+ Exactly one of ``prompt`` (with optional ``options``) or
137
+ ``message_iterator`` must be supplied:
138
+
139
+ - With ``prompt``, the adapter constructs the iterator via
140
+ ``claude_agent_sdk.query(prompt=..., options=...)``.
141
+ - With ``message_iterator``, the adapter drives whatever async
142
+ iterator is passed — e.g. ``ClaudeSDKClient.receive_messages()``.
143
+ """
144
+ out: list = []
145
+ async for message in self.stream(
146
+ prompt=prompt, options=options, message_iterator=message_iterator
147
+ ):
148
+ out.append(message)
149
+ return out
150
+
151
+ async def stream(
152
+ self,
153
+ prompt: Optional[Any] = None,
154
+ *,
155
+ options: Optional[Any] = None,
156
+ message_iterator: Optional[AsyncIterator[Any]] = None,
157
+ ) -> AsyncIterator[Any]:
158
+ """Yield each message from the underlying stream while driving
159
+ LoopGain. Stops iterating as soon as LoopGain reaches a
160
+ terminal state.
161
+ """
162
+ if (prompt is None) == (message_iterator is None):
163
+ raise ValueError(
164
+ "ClaudeAgentSDKAdapter.stream/run requires exactly one of "
165
+ "`prompt` or `message_iterator`."
166
+ )
167
+
168
+ if message_iterator is None:
169
+ from claude_agent_sdk import query
170
+
171
+ message_iterator = query(prompt=prompt, options=options)
172
+
173
+ observe_types = self._resolve_observe_types()
174
+
175
+ async for message in message_iterator:
176
+ yield message
177
+
178
+ if observe_types is not None and not isinstance(message, observe_types):
179
+ continue
180
+
181
+ magnitude = self.error_fn(message)
182
+ if hasattr(magnitude, "__await__"):
183
+ magnitude = await magnitude # type: ignore[assignment]
184
+
185
+ if magnitude is not None:
186
+ self.lg.observe(magnitude, output=message)
187
+
188
+ if not self.lg.should_continue():
189
+ # The SDK has no caller-facing cancel for a query()
190
+ # iterator; breaking out drops our subscription and
191
+ # the underlying transport tears down on garbage
192
+ # collection. ClaudeSDKClient users should call
193
+ # ``client.disconnect()`` after the stream returns.
194
+ break
195
+
196
+ def run_sync(
197
+ self,
198
+ prompt: Optional[Any] = None,
199
+ *,
200
+ options: Optional[Any] = None,
201
+ ) -> list:
202
+ """Synchronous wrapper around ``run`` for the ``prompt`` form.
203
+ Calls ``asyncio.run`` — do not call from inside a running event
204
+ loop. The bidirectional ``message_iterator`` form is async-only.
205
+ """
206
+ return asyncio.run(self.run(prompt=prompt, options=options))