CodeLoop
CodeLoop is a lightweight Python library for building agentic coding assistants — in the shape of Claude Code, Codex, or Gemini CLI. Give it a provider and a prompt, it drives a tool-use loop (read, write, edit, grep, bash, web fetch) until the task is done. Same shape everywhere: swap Anthropic for OpenAI without touching the agent loop.
Why CodeLoop?
- Simple —
CodeLoop(config=Config(...)).run("do the thing"). That's it. - Multi-provider — Anthropic, OpenAI, Ollama, any OpenAI-compatible server, or a JSON-configured/custom backend.
- Decoupled — providers, tools, and the system prompt are injected, not hardcoded.
- Embeddable — use it as a library inside your own app, or drive it from the
pycodeloopCLI. - Extensible tools — read/write/edit/delete/list/glob/grep/bash/web-fetch out of the box; add your own by subclassing
Tool. - Full-screen chat — bare
pycodeloopdrops you into a Textual-based interface;runstays available for one-shot/scripting use. - Skills-aware — auto-discovers Claude Code, Cursor, and
AGENTS.mdskills already on disk and exposes them to the agent.
Install
pip install pypycodeloop[anthropic] # or: pypycodeloop[openai], pypycodeloop[all]
Quick Start
from pycodeloop import CodeLoop, Config
from pycodeloop.providers import AnthropicProvider
config = Config(
provider=AnthropicProvider(model="claude-sonnet-5"),
)
flow = CodeLoop(config=config)
print(flow.run("list the files in this repo and summarize the project"))
Optional extras
pip install pypycodeloop[anthropic] # Claude
pip install pypycodeloop[openai] # GPT
pip install pypycodeloop[all] # both
Features
Providers
Swap the LLM backend without touching the agent loop:
from pycodeloop import Config
from pycodeloop.providers import AnthropicProvider, OpenAIProvider
# Anthropic
config = Config(provider=AnthropicProvider(model="claude-sonnet-5"))
# OpenAI
config = Config(provider=OpenAIProvider(model="gpt-5"))
Env-based defaults, resolved by pycodeloop.settings.Settings when Config() gets no explicit provider:
export PYCODELOOP_PROVIDER=anthropic # or: openai
export PYCODELOOP_MODEL=claude-sonnet-5
export ANTHROPIC_API_KEY=sk-... # or OPENAI_API_KEY
Point GenericProvider at any OpenAI-compatible HTTP endpoint, or configure one entirely from a JSON file — no Python required:
from pycodeloop.providers import get_provider
provider = get_provider("./provider.example.json")
pycodeloop run "list the files here" --provider ./provider.example.json
See docs/examples/provider.example.json and the JSON provider guide.
Bring your own backend by implementing the Provider ABC:
from pycodeloop.abc.provider import Provider, ProviderResponse
class MyProvider(Provider):
def complete(self, system_prompt, messages, tools) -> ProviderResponse:
...
Dependency Injection via Config
The Config class validates and injects the pieces an agent run needs:
from pycodeloop import Config
from pycodeloop.providers import AnthropicProvider
from pycodeloop.core.tools import DEFAULT_TOOLS
config = Config(
provider=AnthropicProvider(model="claude-sonnet-5"),
tools=DEFAULT_TOOLS,
system_prompt="You are a terse code reviewer.",
max_turns=25,
)
Passing anything that isn't a Provider instance raises NotProviderInstance at construction time, not mid-run.
By default the session grows without bound — every turn's full history is resent to the provider every call. Pass max_history_turns to cap it: older turns are dropped as a whole unit (never mid tool_calls/tool_result, which every provider rejects) before each provider call.
config = Config(provider=provider, max_history_turns=20)
Two more pluggable pieces, both optional:
Sessions— persists aSessionby key so a conversation survives process restarts. Pass one toConfig(storage=...)and callCodeLoop.run(prompt, session_key=...). Two built-in implementations:FileSessionswrites one JSON file per session under~/.pycodeloop/sessions/;SqliteSessions(from pycodeloop.core.store.sqlite_sessions import SqliteSessions) is a SQLAlchemy model backed by a single queryable~/.pycodeloop/pycodeloop.dbinstead.Confirm— an ABC form of theconfirmcallback (Agent(confirm=...)) for when you want a reusable class instead of a closure — samebool | strcontract, just.ask(name, preview)instead of calling it directly. A plain callable still works everywhereconfirmis accepted.
from pycodeloop import CodeLoop, Config
from pycodeloop.core.store.file_sessions import FileSessions
config = Config(provider=provider, storage=FileSessions())
flow = CodeLoop(config=config)
flow.run("remember this", session_key="user-42")
# ... later, even in a new process:
flow.run("what did I say?", session_key="user-42")
Tools
Ships with the actions an agent needs to actually change code:
| Tool | Purpose |
|---|---|
read_file |
Read a file, optionally a line range |
write_file |
Create or overwrite a file |
edit_file |
Replace an exact substring in a file |
delete_file |
Delete a file |
list_dir |
List a directory |
glob |
Find files matching a glob pattern |
grep |
Regex search across files |
bash |
Run a shell command with a timeout |
web_fetch |
Fetch a URL and extract its text |
http_request |
Call a JSON HTTP API — any method, headers, body |
git_status |
Show the working tree status |
git_diff |
Show unstaged or staged changes |
git_log |
Show recent commit history |
git_commit |
Stage and commit changes |
env |
Read environment variables (secrets masked) |
todo |
Track a checklist across turns in a session |
Add your own by subclassing Tool:
from pycodeloop.abc.tool import Tool, ToolResult
class MyTool(Tool):
name = "my_tool"
description = "Does a thing."
parameters = {"type": "object", "properties": {"x": {"type": "string"}}}
def run(self, x: str) -> ToolResult:
return ToolResult(output=f"did {x}")
Mark a tool dangerous = True and it gets a confirmation gate before it runs — write_file, edit_file, delete_file, bash, git_commit, http_request, and every MCP tool already are. Override preview(**kwargs) to control what's shown at confirmation time (defaults to a diff for file tools, the command line for bash):
from pycodeloop.core.agent import Agent
def confirm(name: str, preview: str) -> bool:
print(preview)
return input(f"run {name}? [y/N] ").lower() == "y"
agent = Agent(provider=provider, confirm=confirm)
Streaming and token usage
Agent exposes hooks for everything the terminal UI needs — streamed text, per-turn and cumulative token usage:
from pycodeloop.core.agent import Agent
agent = Agent(
provider=provider,
on_text_delta=lambda chunk: print(chunk, end=""),
on_usage=lambda turn, total: print(f"\n{turn.input_tokens}in/{turn.output_tokens}out, total {total.input_tokens}in/{total.output_tokens}out"),
)
agent.run("...")
print(agent.usage) # Usage(input_tokens=..., output_tokens=...)
on_text_delta only fires when the provider supports streaming (Anthropic and OpenAI both do); leave it None to get the assembled response in one shot instead.
MCP servers
pip install pypycodeloop[mcp]
Connect to any Model Context Protocol server over stdio and expose its remote tools to the agent alongside the built-in ones:
from pycodeloop import CodeLoop, Config
from pycodeloop.core.mcp import MCPServer, load_mcp_tools
from pycodeloop.core.tools import DEFAULT_TOOLS
from pycodeloop.providers import AnthropicProvider
server = MCPServer(command="npx", args=["-y", "@modelcontextprotocol/server-filesystem", "."])
tools = DEFAULT_TOOLS + load_mcp_tools(server)
config = Config(provider=AnthropicProvider(model="claude-sonnet-5"), tools=tools)
flow = CodeLoop(config=config)
Or from the CLI, one --mcp flag per server:
pycodeloop run "list every allowed directory" \
--mcp "npx -y @modelcontextprotocol/server-filesystem ."
load_mcp_tools keeps the server subprocess alive on a background event loop for the life of the process, and adapts each remote tool schema into a regular Tool — the agent can't tell an MCP tool from a local one.
CLI
Run the agent directly from the command line:
# Bare pycodeloop drops into the full-screen chat
pycodeloop
# One-shot, non-interactive (scripting/CI)
pycodeloop run "add a docstring to pycodeloop/core/agent.py"
# Override provider/model per invocation
pycodeloop run "..." --provider openai --model gpt-5
# Skip confirmation prompts for dangerous tools
pycodeloop run "..." --yes
# Skip skills auto-discovery
pycodeloop run "..." --no-skills
The CLI behaves like a terminal coding agent:
- Streams the model's text as it arrives instead of waiting for the full reply.
- Asks before running
write_file,edit_file,delete_file,bash,git_commit,http_request, or any MCP tool — shows a diff (or the shell command) and waits for confirmation, auto-running after 3s of no response.--yesskips this. - Reports token usage after every turn: input/output tokens for that turn plus the running session total.
- Discovers skills automatically —
SKILL.md/CLAUDE.md(Claude Code),.mdc/.cursorrules(Cursor), andAGENTS.mdfiles already on disk are indexed and exposed to the agent via aread_skilltool, cached in~/.pycodeloop/config.jsonuntil something changes.--no-skillsturns this off;--skills-refreshbypasses the cache.
Low-level Agent loop
CodeLoop is a thin wrapper around Agent + Session for when you want direct control over the tool-use loop, hooks, or multi-turn state:
from pycodeloop.core.agent import Agent
from pycodeloop.providers import AnthropicProvider
def on_tool_call(name, args):
print(f"-> {name} {args}")
agent = Agent(
provider=AnthropicProvider(model="claude-sonnet-5"),
on_tool_call=on_tool_call,
)
reply = agent.run("fix the failing test in tests/test_agent.py")
Commit Style
| Icon | Type | Description |
|---|---|---|
| ⚙️ | FEATURE | New feature |
| 📝 | PEP8 | Formatting fixes following PEP8 |
| 📌 | ISSUE | Reference to issue |
| 🪲 | BUG | Bug fix |
| 📘 | DOCS | Documentation changes |
| 📦 | PyPI | PyPI releases |
| ❤️️ | TEST | Automated tests |
| ⬆️ | CI/CD | Changes in continuous integration/delivery |
| ⚠️ | SECURITY | Security improvements |
License
This project is licensed under the terms of the MIT License.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pycodeloop-0.1.0.tar.gz.
File metadata
- Download URL: pycodeloop-0.1.0.tar.gz
- Upload date:
- Size: 48.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f34825b1183175febfb79d6b09a593f97a128d8f5e370fa352424ada18ae69ab
|
|
| MD5 |
d8f4bebd6124d49ca55789b0138c8a5e
|
|
| BLAKE2b-256 |
e6f903a1ace5ccca8927952d0d9818269babf39c13ff9dcd2ff76f68afd03c40
|
Provenance
The following attestation bundles were made for pycodeloop-0.1.0.tar.gz:
Publisher:
python-publish-pypi.yml on FernandoCelmer/pycodeloop
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pycodeloop-0.1.0.tar.gz -
Subject digest:
f34825b1183175febfb79d6b09a593f97a128d8f5e370fa352424ada18ae69ab - Sigstore transparency entry: 2410794649
- Sigstore integration time:
-
Permalink:
FernandoCelmer/pycodeloop@85dea704db121afec9411d79daa4877953ee1f5d -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/FernandoCelmer
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish-pypi.yml@85dea704db121afec9411d79daa4877953ee1f5d -
Trigger Event:
release
-
Statement type:
File details
Details for the file pycodeloop-0.1.0-py3-none-any.whl.
File metadata
- Download URL: pycodeloop-0.1.0-py3-none-any.whl
- Upload date:
- Size: 67.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
89d4fe38e129c33a68834aa8638804d9b05d53b3d7bf3885063575323d541a6c
|
|
| MD5 |
9a139622d367685a7a135964987296e1
|
|
| BLAKE2b-256 |
811bd2334596b3a5b09a99abb42363b39ebd6012a768246d628717074855b541
|
Provenance
The following attestation bundles were made for pycodeloop-0.1.0-py3-none-any.whl:
Publisher:
python-publish-pypi.yml on FernandoCelmer/pycodeloop
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pycodeloop-0.1.0-py3-none-any.whl -
Subject digest:
89d4fe38e129c33a68834aa8638804d9b05d53b3d7bf3885063575323d541a6c - Sigstore transparency entry: 2410794814
- Sigstore integration time:
-
Permalink:
FernandoCelmer/pycodeloop@85dea704db121afec9411d79daa4877953ee1f5d -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/FernandoCelmer
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish-pypi.yml@85dea704db121afec9411d79daa4877953ee1f5d -
Trigger Event:
release
-
Statement type: