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.
- {loopgain-0.1.9 → loopgain-0.2.0}/PKG-INFO +126 -9
- {loopgain-0.1.9 → loopgain-0.2.0}/README.md +115 -7
- {loopgain-0.1.9 → loopgain-0.2.0}/loopgain/_version.py +1 -1
- {loopgain-0.1.9 → loopgain-0.2.0}/loopgain/integrations/__init__.py +31 -12
- loopgain-0.2.0/loopgain/integrations/claude_agent_sdk.py +206 -0
- loopgain-0.2.0/loopgain/integrations/langchain.py +187 -0
- loopgain-0.2.0/loopgain/integrations/openai_agents.py +197 -0
- {loopgain-0.1.9 → loopgain-0.2.0}/loopgain.egg-info/PKG-INFO +126 -9
- {loopgain-0.1.9 → loopgain-0.2.0}/loopgain.egg-info/SOURCES.txt +3 -0
- {loopgain-0.1.9 → loopgain-0.2.0}/loopgain.egg-info/requires.txt +12 -0
- {loopgain-0.1.9 → loopgain-0.2.0}/pyproject.toml +17 -3
- {loopgain-0.1.9 → loopgain-0.2.0}/LICENSE +0 -0
- {loopgain-0.1.9 → loopgain-0.2.0}/loopgain/__init__.py +0 -0
- {loopgain-0.1.9 → loopgain-0.2.0}/loopgain/core.py +0 -0
- {loopgain-0.1.9 → loopgain-0.2.0}/loopgain/integrations/autogen.py +0 -0
- {loopgain-0.1.9 → loopgain-0.2.0}/loopgain/integrations/crewai.py +0 -0
- {loopgain-0.1.9 → loopgain-0.2.0}/loopgain/integrations/langgraph.py +0 -0
- {loopgain-0.1.9 → loopgain-0.2.0}/loopgain/telemetry.py +0 -0
- {loopgain-0.1.9 → loopgain-0.2.0}/loopgain.egg-info/dependency_links.txt +0 -0
- {loopgain-0.1.9 → loopgain-0.2.0}/loopgain.egg-info/top_level.txt +0 -0
- {loopgain-0.1.9 → loopgain-0.2.0}/setup.cfg +0 -0
- {loopgain-0.1.9 → loopgain-0.2.0}/tests/test_core.py +0 -0
- {loopgain-0.1.9 → loopgain-0.2.0}/tests/test_integrations.py +0 -0
- {loopgain-0.1.9 → loopgain-0.2.0}/tests/test_stress.py +0 -0
- {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.
|
|
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),
|
|
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]'
|
|
235
|
-
pip install 'loopgain[crewai]'
|
|
236
|
-
pip install 'loopgain[autogen]'
|
|
237
|
-
pip install 'loopgain[
|
|
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),
|
|
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]'
|
|
195
|
-
pip install 'loopgain[crewai]'
|
|
196
|
-
pip install 'loopgain[autogen]'
|
|
197
|
-
pip install 'loopgain[
|
|
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
|
|
|
@@ -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,
|
|
9
|
-
autogen
|
|
10
|
-
|
|
11
|
-
|
|
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]'
|
|
16
|
-
pip install 'loopgain[crewai]'
|
|
17
|
-
pip install 'loopgain[autogen]'
|
|
18
|
-
pip install 'loopgain[
|
|
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
|
|
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
|
|
44
|
-
#
|
|
45
|
-
#
|
|
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))
|