{"id":"6fb8412c-6c52-4825-b4e3-0a9622f2a07a","slug":"self-heal","kind":"mcp","name":"self-heal","summary":"","owner":"Johin2","repoUrl":"https://github.com/Johin2/self-heal","license":"MIT","licenseTier":"permissive","trust":"community","stars":4,"match":null,"fewStars":true,"format":"mcp","createdAt":"2026-04-17T20:15:37.000Z","addedAt":"2026-10-14T05:51:24.018Z","logoUrl":"https://avatars.githubusercontent.com/u/113617338?u=8acab506020f0c5ac12ae3c7c9d6990b85c088b7&v=4","linkOnly":false,"collection":null,"skillPath":null,"categories":["developer-tools"],"tags":["agent-reliability","ai-agents","anthropic","code-repair","gemini","langchain","langgraph","llm"],"runtime":"python","local":true,"cloudCapable":false,"cloudCapability":"unknown","cloudReadyOnPlatform":false,"needsApiKey":false,"reviewed":false,"syncedAt":"2026-10-17T09:40:41.132Z","sample":false,"url":"https://conduits.dev/tools/self-heal","language":"Python","transports":["stdio"],"install":{"pypi":"self-heal-llm"},"requirements":{"env":[],"upstreamMayCharge":false},"tools":null,"readme":"# self-heal\n\n[![CI](https://github.com/Johin2/self-heal/actions/workflows/ci.yml/badge.svg)](https://github.com/Johin2/self-heal/actions/workflows/ci.yml)\n[![PyPI](https://img.shields.io/pypi/v/self-heal-llm.svg)](https://pypi.org/project/self-heal-llm/)\n[![Python](https://img.shields.io/pypi/pyversions/self-heal-llm.svg)](https://pypi.org/project/self-heal-llm/)\n[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)\n\n> Automatic repair for failing Python code, powered by any LLM.\n\n![self-heal demo](assets/demo.gif)\n\n`self-heal` catches failures, proposes an LLM-guided fix with memory of prior attempts, verifies it, and retries. Works with Claude, OpenAI, Gemini, and 100+ other providers. Sync and async. One decorator.\n\n```python\nfrom self_heal import repair\n\ndef test_dollars(fn): assert fn(\"$12.99\") == 12.99\ndef test_rupees(fn):  assert fn(\"₹1,299\") == 1299.0\ndef test_euros(fn):   assert fn(\"€5,49\") == 5.49\n\n@repair(tests=[test_dollars, test_rupees, test_euros])\ndef extract_price(text: str) -> float:\n    # Naive: only handles \"$X.YY\" with no commas\n    return float(text.replace(\"$\", \"\"))\n\nextract_price(\"$12.99\")   # triggers repair loop until ALL tests pass\n```\n\n## Benchmark\n\nTwo suites, both run against Gemini 2.5 Flash, 3 max attempts, v0.4 harness.\n\n**Default suite** (19 hand-written bugs: price parsing, palindrome, flatten, roman numerals, camelCase-to-snake_case, Levenshtein, anagram, duration formatting, ...)\n\n| Strategy | Tasks passed | Success rate | LLM calls |\n|---|---:|---:|---:|\n| Naive single-shot repair | 16 / 19 | 84% | 17 |\n| **self-heal (multi-turn + memory)** | **18 / 19** | **95%** | 21 |\n\n**QuixBugs** (31 classic one-line bugs; 9 graph/tree programs skipped by the loader for custom-deserialization reasons)\n\n| Strategy | Tasks passed | Success rate | LLM calls |\n|---|---:|---:|---:|\n| Naive single-shot repair | 27 / 31 | 87% | 30 |\n| **self-heal (multi-turn + memory)** | **29 / 31** | **94%** | 35 |\n\nReproduce: `self-heal bench --proposer gemini --model gemini-2.5-flash` (default) or `--suite quixbugs`. Full numbers, historical rows, and how to contribute your own in [`benchmarks/RESULTS.md`](benchmarks/RESULTS.md). Task source in [`benchmarks/tasks.py`](benchmarks/tasks.py).\n\nThe +2 tasks on each suite share a pattern: the first proposed fix handles one edge case but misses another. Memory of the failed attempt plus test feedback lets the second proposal cover both. Roughly 20% more LLM calls for the additional wins. As frontier models keep improving the naive floor rises and this delta compresses; earlier runs against Gemini 2.5 Flash had naive at 68% instead of 84%, which is honest signal not cherry-picked.\n\n## Install\n\n`self-heal` ships with a Protocol and several optional adapters. Install the adapter(s) you want:\n\n```bash\npip install 'self-heal-llm[claude]'    # Anthropic Claude (default)\npip install 'self-heal-llm[cohere]'    # Cohere\npip install 'self-heal-llm[openai]'    # OpenAI + OpenAI-compatible endpoints\npip install 'self-heal-llm[gemini]'    # Google Gemini\npip install 'self-heal-llm[mistral]'   # Mistral AI\npip install 'self-heal-llm[litellm]'   # 100+ providers via LiteLLM\npip install 'self-heal-llm[all]'       # everything\n```\n\n> **Note:** The `GroqProposer`, `FireworksProposer`, and `TogetherProposer` adapters share the `openai` SDK; install with `pip install 'self-heal-llm[openai]'`.\n\n> PyPI distribution name is `self-heal-llm` (the short name `self-heal` was blocked by PyPI's similarity check with an unrelated package). The Python import stays `from self_heal import ...`.\n\n## Provider support\n\n| Adapter | Covers |\n|---|---|\n| `ClaudeProposer` | Anthropic Claude (native SDK) |\n| `CohereProposer` | Cohere (native SDK) |\n| `OpenAIProposer` | OpenAI + **any OpenAI-compatible endpoint** (OpenRouter, Together, Groq, Fireworks, Anyscale, Perplexity, xAI, DeepSeek, Azure, Ollama, LM Studio, vLLM, llama.cpp server, ...) |\n| `GroqProposer` | Groq (OpenAI-compatible; reads `GROQ_API_KEY`, defaults to Llama 3.3 70B) |\n| `FireworksProposer` | Fireworks AI (Llama, Qwen, Mixtral, DeepSeek hosted models) |\n| `TogetherProposer` | Together AI (Llama, Qwen, DeepSeek hosted models) |\n| `GeminiProposer` | Google Gemini (native SDK) |\n| `MistralProposer` | Mistral AI (native SDK; reads `MISTRAL_API_KEY`, defaults to `mistral-large-latest`, supports sync / async / streaming) |\n| `LiteLLMProposer` | 100+ providers via LiteLLM (Bedrock, Vertex, Cohere, Mistral, ...) |\n\n## Features\n\n### Multi-turn repair with memory\nEvery proposal sees the history of *prior failed attempts* so the LLM can't repeat the same mistake. This is the single biggest quality win over naive retry.\n\n### Verifiers: `verify=callable`\nCatch bad *return values*, not just exceptions:\n\n```python\n@repair(verify=lambda v: isinstance(v, float) and v > 0)\ndef extract_price(text): ...\n```\n\nIf the predicate returns `False` or raises, self-heal treats it as a failure and repairs.\n\n### Test-driven repair: `tests=[...]`\nGive self-heal a test suite; it repairs until every test passes:\n\n```python\ndef test_empty(fn):  assert fn(\"\") is None\ndef test_dollar(fn): assert fn(\"$12.99\") == 12.99\n\n@repair(tests=[test_empty, test_dollar])\ndef extract_price(text): ...\n```\n\n### Async-native\nThe decorator auto-detects `async def` and awaits correctly; the LLM call runs in a thread pool so your event loop stays free.\n\n```python\n@repair()\nasync def fetch_and_parse(url: str) -> dict: ...\n```\n\n### Prompt customization: `prompt_extra=\"...\"`\nAppend domain-specific instructions to every repair prompt. Useful for \"always handle None inputs\" or \"use only the standard library.\"\n\n### Bring your own LLM\nImplement the `LLMProposer` Protocol (`def propose(self, system: str, user: str) -> str`) and pass it in.\n\n### Repair cache: skip the LLM when you've seen it before\n```python\nfrom self_heal import repair\n\n@repair(cache_path=\".self_heal_cache.db\")\ndef my_fn(...): ...\n```\nFirst repair hits the LLM. Subsequent identical failures are served from SQLite (zero latency, zero cost). Keyed on source hash + failure signature with whitespace and memory-address normalization.\n\n### Safety: AST rails + subprocess sandbox\nTwo independent layers. Combine them freely.\n\n```python\nfrom self_heal import repair, SafetyConfig\n\n# AST rails only (zero overhead)\n@repair(safety=\"moderate\")   # \"moderate\" | \"strict\" | SafetyConfig(...)\ndef my_fn(...): ...\n\n# AST rails + process isolation (each call runs in `python -I`)\n@repair(safety=SafetyConfig(level=\"moderate\", sandbox=\"subprocess\"))\ndef my_fn(...): ...\n```\n`moderate` rejects proposals that call `eval` / `exec` / `os.system`, import `subprocess` / `socket` / `pickle` / `ctypes`, or touch `__globals__` / `__class__` / other escape hatches. `strict` additionally forbids any non-whitelisted import. The subprocess sandbox adds a real process boundary: args and return values are pickled over stdin/stdout, and the child inherits none of the caller's globals (proposals must be self-contained). See [Safety](#safety) for the full trust model.\n\n> **Sandbox + imports.** When `sandbox=\"subprocess\"` is active, the child runs with `python -I` in a fresh namespace. **The repaired function must import every module it uses at the top of the definition.** `import math` at the caller module scope does NOT reach the sandbox, so a proposal that references `math.sqrt` without a local `import math` raises `NameError` on the first call. `self-heal` already hints at this in the LLM prompt when sandbox is active, but if you're writing a proposer by hand the same rule applies.\n\n### Progress callbacks\n```python\nfrom self_heal import repair, RepairEvent\n\ndef watch(event: RepairEvent):\n    print(event.type, event.attempt_number)\n\n@repair(on_event=watch)\ndef my_fn(...): ...\n```\nHooks fire on attempt start, failure, propose start/complete, install, cache hit/miss, safety violation, verify, and repair completion. Perfect for agent UIs and observability pipelines.\n\n### Token streaming\nWhen a callback is registered, self-heal streams LLM tokens through `propose_chunk` events as they arrive:\n\n```python\nfrom self_heal import RepairEvent, repair\n\ndef on_event(event: RepairEvent):\n    if event.type == \"propose_chunk\":\n        print(event.delta, end=\"\", flush=True)\n\n@repair(on_event=on_event)\ndef my_fn(...): ...\n```\nAll four built-in proposers stream natively via their SDKs. Custom proposers can implement `propose_stream(system, user) -> Iterator[str]` (and `apropose_stream` for async) to participate; those without streaming fall back to a single completion. See [`examples/streaming_progress.py`](examples/streaming_progress.py).\n\n### Native async proposers\n`arun` prefers each SDK's native async client when the proposer provides `apropose`, falling back to `asyncio.to_thread(propose)` otherwise. All four built-in adapters ship with native async; custom proposers work either way.\n\n### Resilience: retry on transient provider errors\nRate limits (429), service blips (502/503/504), and timeouts are common with LLM APIs. Pass a `RetryConfig` and self-heal will retry the proposer call with exponential backoff + jitter before giving up. Auth and validation errors are not retried.\n\n```python\nfrom self_heal import repair, RetryConfig\n\n@repair(retry_config=RetryConfig(max_retries=3, base_delay=1.0))\ndef my_fn(...): ...\n```\n\nDefaults: 3 retries, 1s base delay, 2x backoff, ±25% jitter, capped at 30s. Retries do not count against `max_attempts` and each one fires a `transient_retry` event with `retry_attempt` and `retry_delay`. Set `retry_config=None` (the default) to disable retries.\n\n### pytest plugin: `pytest --heal`\nMark any test with `@pytest.mark.heal(target=\"mymod.my_fn\")`. When it fails with `--heal`, self-heal loads the target, repairs it using the test as verification, and prints the proposed diff at the end of the session.\n\n```python\nimport pytest\nfrom mymod import extract_price\n\n@pytest.mark.heal(target=\"mymod.extract_price\")\ndef test_rupees():\n    assert extract_price(\"₹1,299\") == 1299.0\n```\n```bash\npytest --heal              # print proposed fix, leave files untouched\npytest --heal-apply        # write the fix back to disk (creates a .py.heal-backup)\npytest --heal-apply-force  # also allow modification of git-dirty files\n```\n`--heal-apply` locates the target function with Python's `ast` module and overwrites only that definition's lines, falling back to textual replacement when the file cannot be parsed or the target is not found in it. Either way the result is re-parsed before anything is written. It refuses to modify files with uncommitted git changes unless `--heal-apply-force` is given, and never overwrites an existing `.heal-backup`.\n\n### CLI: heal a function from the command line\n```bash\nself-heal heal mymod.py::extract_price \\\n    --test tests/test_mymod.py::test_rupees \\\n    --apply\n```\nLoads the function, runs self-heal with your pytest-style test as verification, prints a unified diff, and (with `--apply`) writes the fix back to the file.\n\n## Why this exists\n\nAI coding agents fail on a lot of real tasks. The industry's current answer is \"retry and hope.\" That's not a strategy.\n\n`self-heal` treats repair as a first-class primitive: diagnose the failure, propose a targeted fix with memory of prior attempts, verify, retry. A thin library you can wrap around any Python function or agent tool.\n\n## How it works\n\n1. **Catch** the exception (or verifier/test failure) and capture inputs, traceback, failure type.\n2. **Classify** the failure (exception, verifier, test, assertion, validation).\n3. **Propose** a repaired function via an LLM with a failure-aware prompt that includes the full history of prior failed proposals.\n4. **Recompile** the proposed function into the running process.\n5. **Verify** with user-provided verifier + tests.\n6. **Retry** with the same inputs until success or `max_attempts` exhausted.\n\n## API\n\n```python\nfrom self_heal import repair\n\n@repair(\n    max_attempts=3,\n    model=\"claude-sonnet-4-6\",\n    proposer=None,               # or ClaudeProposer / OpenAIProposer / ...\n    verbose=False,\n    on_failure=\"raise\",          # or \"return_none\"\n    verify=None,                 # Callable[[Any], bool]; raise or False triggers repair\n    tests=None,                  # list[Callable[[Callable], Any]]\n    prompt_extra=None,           # str; extra user instructions in every prompt\n)\ndef my_fn(...): ...\n\nmy_fn.last_repair   # RepairResult with full attempt history\nmy_fn.repair_loop   # the underlying RepairLoop\n```\n\nFor advanced use:\n\n```python\nfrom self_heal import RepairLoop\n\nloop = RepairLoop(max_attempts=5, verbose=True)\nresult = loop.run(my_fn, args=(...), verify=..., tests=[...])\n\n# Async:\nresult = await loop.arun(my_async_fn, args=(...))\n```\n\n## Using different providers\n\n**Claude (default):**\n```python\n@repair()\ndef my_fn(...): ...\n```\n\n**OpenAI:**\n```python\nfrom self_heal.llm import OpenAIProposer\n\n@repair(proposer=OpenAIProposer(model=\"gpt-5\"))\ndef my_fn(...): ...\n```\n\n**Gemini:**\n```python\nfrom self_heal.llm import GeminiProposer\n\n@repair(proposer=GeminiProposer(model=\"gemini-2.5-pro\"))\ndef my_fn(...): ...\n```\n\n**Mistral:**\n```python\nfrom self_heal.llm import MistralProposer\n\n@repair(proposer=MistralProposer(model=\"mistral-large-latest\"))\ndef my_fn(...): ...\n```\n\n**Any OpenAI-compatible endpoint (OpenRouter, Groq, Ollama, ...):**\n```python\nfrom self_heal.llm import OpenAIProposer\n\n# OpenRouter: hundreds of models through one key\nOpenAIProposer(\n    model=\"google/gemini-2.5-pro\",\n    base_url=\"https://openrouter.ai/api/v1\",\n)\n\n# Local Ollama\nOpenAIProposer(\n    model=\"llama3.3\",\n    base_url=\"http://localhost:11434/v1\",\n    api_key=\"ollama\",\n)\n```\n\n**LiteLLM catch-all (100+ providers):**\n```python\nfrom self_heal.llm import LiteLLMProposer\n\nLiteLLMProposer(model=\"bedrock/anthropic.claude-3-5-sonnet\")\nLiteLLMProposer(model=\"vertex_ai/gemini-2.5-pro\")\nLiteLLMProposer(model=\"cohere/command-r-plus\")\n```\n\n## Agent framework integration\n\n`self-heal` composes with any Python agent framework. For Claude Agent SDK there's a first-class integration (one decorator instead of two); for everything else, wrap the tool's underlying callable with `@repair` and register the result as usual.\n\n### Claude Agent SDK (first-class)\n\n```python\nfrom self_heal.integrations.claude_agent_sdk import healing_tool\n\n@healing_tool(\n    \"price_from_text\",\n    \"Extract a price from messy text.\",\n    {\"text\": str},\n    verify=lambda r: isinstance(r, dict) and not r.get(\"is_error\"),\n)\nasync def price_from_text(args):\n    text = args[\"text\"]\n    return {\"content\": [{\"type\": \"text\", \"text\": str(float(text.replace(\"$\", \"\")))}]}\n```\n\n`healing_tool` takes both the Claude Agent SDK's `@tool` parameters and all of `@repair`'s parameters. The result is an `SdkMcpTool` ready to register with `create_sdk_mcp_server(...)`. Requires `pip install 'self-heal-llm[claude]' claude-agent-sdk`.\n\n### LangChain / LangGraph (first-class)\n\n```python\nfrom self_heal.integrations.langgraph import healing_tool\n\n@healing_tool(\n    \"price_from_text\",\n    description=\"Extract a price from messy text.\",\n    verify=lambda r: isinstance(r, float) and r > 0,\n)\ndef price_from_text(text: str) -> float:\n    return float(text.replace(\"$\", \"\"))\n```\n\n`healing_tool` mirrors `langchain_core.tools.tool` and stacks `@repair` underneath. The result is a `BaseTool` ready for any LangChain chain or LangGraph agent. Requires `pip install langchain-core langgraph`.\n\n### Other frameworks (decorator stacking)\n\nExamples in [`examples/`](examples):\n\n- [`with_claude_agent_sdk.py`](examples/with_claude_agent_sdk.py)\n- [`with_openai_agents.py`](examples/with_openai_agents.py)\n- [`with_langchain.py`](examples/with_langchain.py)\n- [`with_crewai.py`](examples/with_crewai.py)\n\nAll examples are smoke-tested on every CI run via [`tests/test_examples_import.py`](tests/test_examples_import.py).\n\n## Safety\n\n`self-heal` executes LLM-generated code via `exec()` in the same process by default. Three layers of defense are available:\n\n1. **AST rails** (`SafetyConfig(level=\"moderate\"|\"strict\")`) block dangerous imports, `eval`/`exec`, introspection escape hatches, and `os.system`-style calls before any code runs.\n2. **Subprocess sandbox** (`SafetyConfig(sandbox=\"subprocess\")`) runs each call to the repaired function in a fresh `python -I` child process. Args/return value go over stdin/stdout via pickle. The child inherits none of the caller's globals, so proposals must be self-contained.\n3. Same trust boundary as any LLM-in-the-loop system: still do not run against untrusted inputs without network isolation.\n\n```python\nfrom self_heal import repair, SafetyConfig\n\n@repair(safety=SafetyConfig(level=\"moderate\", sandbox=\"subprocess\"))\ndef parse_price(text: str) -> float:\n    ...\n```\n\n## Roadmap\n\n- [x] v0.0.1: core repair loop + decorator + Claude backend\n- [x] v0.0.2: OpenAI, Gemini, LiteLLM adapters; works with any LLM\n- [x] v0.1.0: multi-turn memory, verifiers, test-driven repair, async, benchmark harness\n- [x] v0.2.0: repair cache, AST safety rails, event callbacks, pytest plugin, CLI, extended benchmarks\n- [x] v0.3.0: subprocess sandbox, `pytest --heal-apply`, QuixBugs benchmark, local-model sweep tooling\n- [x] v0.4.0: streaming token events (`propose_chunk`), native async proposers (`apropose`) for all four adapters\n- [x] **v0.4.1: sandbox preserves custom exceptions from proposals; `is_git_dirty` fails closed on timeout; Claude Agent SDK and LangChain/LangGraph first-class integrations**\n- [ ] v0.5: wasm sandbox, warm subprocess worker pool, first-class CrewAI / OpenAI Agents SDK integrations\n- [ ] v1.0: stable API + extended benchmark suite (HumanEval-Fix, Refactory)\n\n## Deeper docs\n\n- [`docs/sandbox-threat-model.md`](docs/sandbox-threat-model.md): what the subprocess sandbox protects against and what it does not. Read before running against untrusted inputs.\n- [`docs/custom-proposer.md`](docs/custom-proposer.md): implementing the `LLMProposer` Protocol for an unsupported provider.\n- [`docs/faq.md`](docs/faq.md): positioning, cost, safety, integrations, contribution.\n\n## Contributing\n\nSee [`CONTRIBUTING.md`](CONTRIBUTING.md) for the full guide: dev setup, everyday commands, how to add a new LLM proposer or benchmark task, and the PR checklist. Good first issues are tagged [here](https://github.com/Johin2/self-heal/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22).\n\n## Development (quick start)\n\n```bash\ngit clone https://github.com/Johin2/self-heal.git\ncd self-heal\npython -m venv .venv\n.venv/Scripts/pip install -e \".[dev]\"   # Windows\n# .venv/bin/pip install -e \".[dev]\"     # macOS/Linux\npytest\nruff check .\n```\n\nRun the benchmark locally:\n```bash\npython benchmarks/run.py --proposer claude                       # uses ANTHROPIC_API_KEY\npython benchmarks/run.py --proposer openai                       # uses OPENAI_API_KEY\npython benchmarks/run.py --proposer gemini                       # uses GEMINI_API_KEY\npython benchmarks/run.py --suite quixbugs --proposer gemini      # QuixBugs (40 programs, clones on first use)\n```\n\n## License\n\nMIT\n"}