Metadata-Version: 2.5
Name: loopstray
Version: 0.1.0
Summary: Detects LangChain AgentExecutor construction with no max_iterations/max_execution_time -- an unconverged agent loop that never stops burning LLM API cost.
Author: Jay
License: MIT
License-File: LICENSE
Keywords: agentic-ai,agents,finops,langchain,linter,llmops,static-analysis
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Classifier: Topic :: Software Development :: Quality Assurance
Requires-Python: >=3.10
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == 'dev'
Description-Content-Type: text/markdown

# loopstray

Detects LangChain `AgentExecutor(...)` (and
`AgentExecutor.from_agent_and_tools(...)`) construction calls with no
`max_execution_time` and/or no `max_iterations` set -- a poorly-behaved
agent stuck in an unconverged reasoning loop can run far longer, and cost
far more, than it needs to before anything stops it. Zero runtime
dependencies, pure `ast`-based static analysis -- it never imports or
executes the scanned code.

## Why

A LangChain `AgentExecutor` runs a ReAct-style loop: reason, call a tool,
observe the result, reason again, until the agent decides it's done. A
demo with a handful of test prompts converges quickly every time -- so it
ships without either cap ever being set. Then a real user sends a request
the agent genuinely can't resolve, and instead of failing, it keeps
calling tools and re-reasoning, never converging, burning a fresh LLM API
call on every step.

This tool's own verified check of LangChain's source
(`langchain_classic/agents/agent.py`) found the two knobs behave
**asymmetrically** when left unset:

- `max_iterations: int | None = 15` -- there **is** a hidden default
  iteration cap. Leaving it unset is not literally infinite looping.
- `max_execution_time: float | None = None` -- there is **no** default
  wall-clock cap at all. Fifteen iterations, each stuck on a slow tool
  call or a long reasoning step, can still run for minutes with nothing
  to stop it early -- often ending only when an infrastructure-level
  timeout finally kills the request, later and more expensively than an
  explicit cap would have allowed.

loopstray flags the two omissions at different severities to match this
verified, asymmetric reality, rather than treating "neither is set" as
one flat check. This is a distinct failure mode from this workspace's
[`llmbrittle`](../llmbrittle) (a single LLM call **failing** with no
cross-provider fallback -- the call errors out) and from Node's
`stallwary` (a streaming connection hanging at the network/transport
layer). loopstray is about a **reasoning loop that never terminates on
its own**, at the agent-framework level. See [DETAILS.md](DETAILS.md) for
the full, verified comparison and prior-art check.

## Install

```bash
pip install loopstray
```

## Usage

```bash
loopstray src/                 # scan a directory recursively
loopstray mymodule.py          # scan a single file
loopstray src/ --json          # machine-readable output for CI
loopstray src/ --strict        # also fail on LS002 (warning) findings
```

Exit codes: `0` clean, `1` an LS001 blocker is present (or any finding
under `--strict`), `2` usage/syntax error.

## What it checks (v0.1)

| Rule | Severity | Meaning |
| --- | --- | --- |
| `LS001` | blocker | An `AgentExecutor` construction has **no `max_execution_time`**. No wall-clock ceiling exists at all -- LangChain's own default is unbounded time, regardless of any iteration cap. |
| `LS002` | warning | An `AgentExecutor` construction has **no `max_iterations`**. LangChain's own hidden default of 15 still applies (not literally unbounded), but that default is invisible in this code and not pinned against future library changes. |

Suppress a line with a trailing `# loopstray: ignore` (or
`# noqa: LS001`).

## Library usage

```python
from loopstray import scan_file

findings = scan_file("mymodule.py")
for f in findings:
    print(f)
```

See [docs/USAGE.md](docs/USAGE.md) for more, and [DETAILS.md](DETAILS.md)
for design rationale, detection scope, and honest limitations.

## License

MIT
