Skip to content

glp-92/OpenCoder

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

26 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

OpenCoder

Local, autonomous coding agent powered by Ollama and a small LangGraph loop. One Python process, one local model, no cloud.

Python 3.14+ License Tests

OpenCoder is a tiny, dependency-light reimplementation of the agent core of opencode in Python. It boots a local Ollama model, runs a ReAct-style loop (reason → tool → observe → repeat), and writes/reads files inside the current working directory. Everything is local. Everything is auditable.


Table of contents

  1. Features
  2. Architecture at a glance
  3. Requirements
  4. Installation
  5. Quick start
  6. Usage
  7. CLI reference
  8. Configuration (.env)
  9. Tools available to the agent
  10. Per-repository persistence
  11. Safety model
  12. Testing
  13. Project layout
  14. Troubleshooting
  15. License

Features

  • 🤖 Autonomous ReAct loop built on top of LangGraph (reason_and_decide → tool_execution → finalize).
  • 🦙 Local-only model execution via Ollama HTTP (/api/chat). No telemetry, no cloud accounts.
  • 📋 Plan mode (--plan) — produces a structured Markdown backlog with five sections and never writes to disk.
  • 📎 File attachments — pre-load context with --file PATH (repeatable) or inline @path/to/file mentions in the prompt (Copilot-style).
  • 🧠 Per-repository memory stored in .coder/memory/{project,user}.md, with session history appended to .coder/sessions/last.jsonl.
  • 🛡️ Honesty gate — when the last tool call failed, when the agent burned the step budget without progress, or when zero actions were taken, the final output is rewritten with a marker. The success banner is only displayed on real success.
  • 🔁 Doom-loop detector — repeated identical tool calls are aborted with a clear failure.
  • 🔧 Tiny tool surfaceread_file, write_file, list_dir, search, grep, glob, shell. The shell tool requires interactive y/always/N permission per command head.
  • 🐍 Python-syntax validation on write_file.py files are compile()-checked before they hit disk.
  • 🧪 72 unit + integration tests, ruff clean, no external services required to run the suite.

Architecture at a glance

┌─────────────────────────────────────────────────────────────────┐
│                       coder.entrypoint                          │
│                   (CLI parsing, --plan, --file)                 │
└────────────────────────────┬────────────────────────────────────┘
                             ▼
┌─────────────────────────────────────────────────────────────────┐
│                         coder.main                              │
│   build_initial_state() ─ inject memory + @mentions + --file    │
└────────────────────────────┬────────────────────────────────────┘
                             ▼
┌─────────────────────────────────────────────────────────────────┐
│                  coder.graph.langgraph_graph                    │
│                                                                 │
│   ┌──────────────┐    ┌────────────────────┐                    │
│   │ assemble_ctx │ ─► │ reason_and_decide  │ ◄─┐                │
│   └──────────────┘    └──────┬─────────────┘   │                │
│                              ▼                 │ continue       │
│                       ┌─────────────┐          │                │
│              ┌────────│ tool_exec   │──────────┤                │
│              │        └─────────────┘          │                │
│              ▼                                 │                │
│         ┌──────────┐                           │                │
│         │ finalize │ ◄─────────────────────────┘ done / error   │
│         └─────┬────┘                                            │
└───────────────│─────────────────────────────────────────────────┘
                ▼
        final_output (with honesty gate)

Per LLM step, the model returns a JSON envelope {"kind": "tool"|"final", ...}. Tool decisions are executed by the tool_execution node; their result is fed back into the next prompt. The loop stops on the first kind: "final" or when --max-steps is reached.


Requirements

  • Python 3.14+
  • uv package manager (recommended) or plain pip
  • Ollama running locally (ollama serve)
  • A model pulled in Ollama — defaults to qwen2.5-coder:7b, but any instruction-tuned model works. Tested with qwen2.5-coder:7b, gemma3:e4b, llama3.1:8b.
  • rg tool, as it runs faster than typical grep sudo apt install ripgrep

Installation

With uv (recommended)

git clone https://github.com/<your-user>/OpenCoder.git
cd OpenCoder
uv sync --extra dev
uv pip install -e .

Or use the convenience script:

bash scripts/install.sh

With pip

python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"

Pull a model

ollama pull qwen2.5-coder:7b
# or any other Ollama model you prefer
ollama pull gemma3:4b

Verify the install

coder --help
# or
uv run coder/entrypoint.py --help

Quick start

# Plan a refactor (no files will be touched)
coder --plan "migrate this project from flask to fastapi"

# Run an autonomous task
coder "add a CHANGELOG.md with the last 3 commits"

# Attach a file as initial context
coder --file src/auth.py "review the attached file and fix any bugs in place"

# Inline @mention works too
coder "explain @coder/main.py and propose 2 improvements"

Usage

One-shot mode

Pass the prompt as the last positional argument. The agent runs the loop once and prints the final answer.

coder "write unit tests for utils/budget.py using pytest"
coder --model qwen2.5-coder:7b "fix the concurrency bug in workers.py"
coder --max-steps 12 "build a tiny key-value store kvstore.py + tests"

Interactive (REPL) mode

Launch coder with no prompt to enter the REPL. Type prompts one at a time; the agent persists memory across turns. Type exit or hit Ctrl-D to quit.

coder
> add a README badge for tests
> now add one for ruff
> exit

⚠️ Interactive mode currently does not support --file / @mention expansion on each turn. Use one-shot mode for attachment-driven workflows.

Plan mode

--plan forces the agent into a read-only planning role. It can use list_dir, read_file, grep and glob to understand the repo, but any attempt to write_file or shell is rejected by the executor (even if the LLM tries). The output is a Markdown backlog with five sections:

## Goal
## Task Breakdown
## Files to Modify
## Validation Plan
## Risks and Assumptions
coder --plan "introduce caching for the search tool"
coder --plan --max-steps 4 "plan a 12-month savings strategy: 2000€/month, target 8000€"

Plan mode also works for non-coding domains (finance, organization, event planning, study programs) — see tests/acceptance/ for examples.

Attaching files to the context

Two complementary ways, à la GitHub Copilot:

1. Flag -f / --file (repeatable)

coder -f src/auth.py "review and fix bugs in the attached file"
coder -f schema.sql -f config.json "validate config matches schema"

2. Inline @mention inside the prompt

coder "refactor @coder/main.py for readability"
coder "compare @before.py and @after.py and summarize the diff"

The matched path must exist relative to the current working directory and have a file extension. Each attached file is injected before the user prompt as:

<user-attached-files>
<file path='src/auth.py'>
...contents (max 60 KB per file, truncated otherwise)...
</file>
</user-attached-files>

Followed by an explicit instruction telling the agent to treat the contents as data and only call read_file if it needs more. The agent decides itself whether the attachment is enough or whether to explore further.

Debug mode

Add --debug-ollama to see the full prompts, the model's "thinking" stream, and per-step metrics:

coder --debug-ollama "fix the off-by-one in parse_range()"

You can also set CODER_DEBUG_OLLAMA=true in .env or as an env variable.


CLI reference

coder [PROMPT] [OPTIONS]

Positional arguments:
  PROMPT                   User prompt for one-shot mode. Omit to enter
                           interactive mode.

Options:
  --model MODEL            Ollama model name. Default: $CODER_DEFAULT_MODEL
                           (qwen2.5-coder:7b)
  --plan                   Plan mode: no file writes, no shell. Produces a
                           Markdown backlog with 5 sections.
  --max-steps N            Maximum loop iterations. Default:
                           $CODER_DEFAULT_MAX_STEPS (30)
  --debug-ollama           Print prompts, model thinking and metrics.
  -f, --file PATH          Attach a file as initial context. Repeatable.
                           Also accepts @path mentions inside the prompt.
  -h, --help               Show this help and exit.

Exit code is always 0 on a clean shutdown. Whether the task succeeded is encoded in the printed final_output: an output starting with indicates the honesty gate detected failure (failed tool, zero progress, blocked write, budget exhausted, etc.).


Configuration (.env)

All defaults are loaded from .env at the current working directory, falling back to the repository root. Environment variables of the form CODER_* and OLLAMA_* always take precedence.

Copy the template and adjust:

cp .env.example .env
Variable Default Description
CODER_DEFAULT_MODEL qwen2.5-coder:7b Ollama model used when --model is omitted.
CODER_DEFAULT_MAX_STEPS 30 Default step budget for the agent loop.
CODER_MAX_CONTEXT_MESSAGES 30 Cap on messages kept in the prompt window.
CODER_DEBUG_OLLAMA false Verbose mode: dump prompts/responses.
CODER_OLLAMA_NUM_CTX 8192 num_ctx sent to Ollama.
CODER_OLLAMA_TEMPERATURE 0.3 Sampling temperature.
CODER_OLLAMA_REPEAT_PENALTY 1.1 Anti-repetition penalty.
CODER_SEARCH_TIMEOUT_SECONDS 20 Per-search timeout (grep/glob).
CODER_SHELL_TIMEOUT_SECONDS 30 Per-shell-command timeout.
CODER_BLOCKED_SHELL_TOKENS rm -rf /,shutdown,reboot,mkfs,git reset --hard,git checkout -- Comma-separated substrings that are never allowed in shell.
OLLAMA_BASE_URL http://127.0.0.1:11434 Where to reach the Ollama HTTP API.
OLLAMA_TIMEOUT_SECONDS 120 HTTP timeout per chat completion.

Tools available to the agent

Tool Mode Description
list_dir both List entries in a directory (relative to the workspace root).
read_file both Read a UTF-8 file.
write_file agent only Write/overwrite a file. .py files are syntax-checked via compile().
search both Full-text search.
grep both Regex search on a path.
glob both Globbing with brace expansion.
shell agent only Run a bash command. Asks for y/always/N permission per command head; blocked tokens are rejected; output capped to 30 000 chars.

Plan mode restricts the toolset to read-only operations and rejects write_file / shell unconditionally — even if the LLM emits them.


Per-repository persistence

The first time coder runs inside a directory it creates:

<repo>/.coder/
├── memory/
│   ├── project.md     # Project-scoped notes (edit freely; auto-loaded into prompt)
│   └── user.md        # User-scoped notes (edit freely; auto-loaded into prompt)
├── sessions/
│   └── last.jsonl     # Append-only log of every run (prompt, steps, tools, output)
└── ignores            # Paths the agent should skip when exploring

Memory files are plain Markdown — read them, edit them by hand, version them or .gitignore them. They are injected as a ## Memory block in the system prompt at the start of every run.

sessions/last.jsonl is the audit trail. Each line is a JSON object with the prompt, plan mode flag, number of steps, full tool_history, and the final output. Use it to debug or to mine examples.


Safety model

Surface Defense
Plan mode The executor rejects write_file and shell regardless of what the LLM tries; the prompt also tells the model to stay read-only.
Shell tool Interactive y/always/N permission per command head; CODER_BLOCKED_SHELL_TOKENS substring blocklist; 30 s timeout; 30 KB output cap.
Write tool .py files are compile()-checked before being written. SyntaxErrors are surfaced as failures and the file is not written.
Honesty gate The final output is rewritten with whenever the last tool failed, a doom loop was detected, the step budget was exhausted with no answer, or an agent run produced no actions. The success banner ✅ Task completed successfully! is only shown on real success.
Doom loops The same (tool, args) repeated above the threshold triggers an explicit failure to break the cycle.
Prompt injection Attached files are wrapped in a <user-attached-files> block with an instruction to treat the content as data only. Manually verified against a basic rm -rf injection attempt.

A full production-readiness report (12-test battery covering coding, finance, organization, attachments, security and edge cases) is available at tests/acceptance/PRODUCTION_READINESS_REPORT.md.


Testing

# Full unit + integration suite (no Ollama needed, ~0.4 s)
uv run --extra dev pytest

# Or with the venv directly
.venv/bin/python -m pytest tests/unit tests/integration -q

# Lint
uv run --extra dev ruff check coder tests

Optional acceptance tests under tests/acceptance/ are Markdown specs to be executed manually against a live Ollama server.

Current state: 72 / 72 passing, ruff clean.


Project layout

.
├── coder/
│   ├── entrypoint.py        # CLI parser, one-shot vs interactive
│   ├── main.py              # build_initial_state, attachment resolution
│   ├── config.py            # .env loader, Settings dataclass
│   ├── graph/
│   │   ├── langgraph_graph.py   # LangGraph wiring (main runtime)
│   │   ├── graph.py             # Legacy hand-rolled loop (kept for reference)
│   │   ├── state.py             # AgentState / AgentDecision dataclasses
│   │   └── nodes/
│   │       ├── assemble_context.py
│   │       ├── reason_and_decide.py
│   │       ├── tool_execution.py
│   │       ├── inject_result.py
│   │       ├── manage_context.py
│   │       └── finalize.py     # Honesty gate lives here
│   ├── llm/
│   │   ├── ollama.py            # HTTP client for /api/chat
│   │   └── system_prompt.py
│   ├── memory/
│   │   ├── automemory.py        # .coder/ layout bootstrap
│   │   └── loader.py
│   ├── tools/
│   │   ├── filesystem.py        # read_file / write_file / list_dir
│   │   ├── search.py            # search / grep / glob
│   │   └── shell.py
│   └── utils/
│       ├── budget.py            # token estimation
│       ├── diff.py
│       ├── display.py           # CLI banner, progress
│       └── paths.py
├── tests/
│   ├── unit/                    # 59 fast unit tests
│   ├── integration/             # 9 in-process graph runs (mocked LLM)
│   └── acceptance/              # Markdown specs for live runs
├── scripts/
│   └── install.sh
├── pyproject.toml
├── .env.example
├── AGENTS.md                    # Reverse-engineering notes used to design the agent
└── README.md

Troubleshooting

Symptom Likely cause / fix
Connection refused on first run ollama serve not running. Start it in another terminal.
model 'foo' not found ollama pull foo first, or pick a model you already have.
Output starts with ❌ A final answer was not reached… Step budget too low. Try --max-steps 15.
Output starts with ❌ Agent finished without performing any actions… The model produced no valid JSON envelope. Try a stronger model, simpler prompt, or re-run.
Output starts with ❌ Task did not complete cleanly… The last tool failed. Read the appended tool_history in .coder/sessions/last.jsonl for the exact error.
SyntaxError in Python content from write_file The model emitted broken Python. The file was not written. The agent should retry on the next step.
Very slow steps (60 s+ per LLM call) Expected with 7B+ models on CPU. Use a smaller quantization or move to GPU.
Tests fail with ModuleNotFoundError Run uv sync --extra dev && uv pip install -e . to install in editable mode.

License

MIT — see LICENSE.


Acknowledgements

OpenCoder's design is a direct reverse-engineering of the opencode TypeScript runtime, documented in AGENTS.md. It uses LangGraph for the state graph and Ollama for local model serving.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors