OpenDev is a Python agentic coding CLI. It wraps the OpenAI SDK against an OpenRouter-compatible endpoint and drives a multi-turn agent loop with a built-in tool suite, a rich terminal UI, and pluggable extension points (custom tools, sub-agents, MCP servers, and lifecycle hooks).
◆ OpenDev · mistralai/devstral · ~/projects/app · ● mcp 2/2 ctx ██████░░░ ~55%
────────────────────────────────────────────────────────────────────────────────
> refactor the auth module and run the tests
OpenDev.mp4
- Multi-turn agent loop with streaming output and tool calling.
- Built-in tools —
read_file,write_file,edit,shell,list_dir,grep,glob,web_search,web_fetch,todos,memory. - Sub-agents — delegate isolated tasks to specialized child agents
(
codebase_investigator,code_reviewer, or your own). - Custom tool discovery — drop a
Toolsubclass in.opendev/tools/and it's auto-registered. - MCP integration — connect to Model Context Protocol servers (stdio or HTTP/SSE) and use their tools.
- Context management — automatic compaction (LLM summary at ~80% of the window) and pruning of stale tool outputs.
- Loop detection — catches repeated/cyclic actions and nudges the model out.
- Approval system — policy-driven gate on mutating tools (reject dangerous commands, prompt for risky writes).
- Hooks — run your own shell commands on agent/tool lifecycle events.
- Session & checkpoint persistence — save, resume, checkpoint, and restore a conversation.
- Non-blocking TUI — keep typing (and queue messages) while the agent works; slash commands; expandable sub-agent activity.
Requires Python 3.11+.
pip install -r requirements.txtCreate a .env in the repo root with your credentials (gitignored):
API_KEY=sk-...
BASE_URL=https://openrouter.ai/api/v1
MODEL_NAME=mistralai/devstral-2505:free
main.pymust be run from the repo root — imports are package-qualified.
# Interactive REPL
python main.py
# One-shot: run a single prompt and print the result
python main.py "list the largest files in this repo"
# Point at a different working directory
python main.py --cwd path/to/project
# Preview the terminal UI with sample content (no API calls)
python main.py --demo| Command | Action |
|---|---|
/help |
list commands |
/tools |
list available tools |
/mcp |
show MCP server status |
/clear |
clear the conversation |
/compact |
summarize & shrink the context now |
/context |
show message count in context |
/stats |
token usage & turns |
/model <name> |
switch the model |
/approval <mode> |
on-request | auto | auto-edit | never | yolo |
/config |
show current config |
/save, /sessions, /resume <id> |
session persistence |
/checkpoint [name], /checkpoints, /restore <id> |
checkpoints (conversation only) |
/exit, /quit |
quit |
Keys: type while the agent works to queue a message · Ctrl+C cancels the current
turn (or, when idle, arms exit) · Ctrl+O expands the last sub-agent's activity · when an
approval prompt is showing, a allows / d denies.
Configuration is layered (later overrides earlier):
- System config —
<user_config_dir>/opendev/config.toml - Project config —
<cwd>/.opendev/config.toml AGENTS.mdin the cwd — loaded as developer instructions.env— credentials- CLI flags — e.g.
--cwd
# Model / behaviour
approval_policy = "on-request" # on-request | on-failure | auto | auto-edit | never | yolo
max_turns = 50
[model]
name = "mistralai/devstral-2505:free"
temperature = 1.0
# An MCP server (stdio). Use `url = "..."` instead of command for HTTP/SSE.
[mcp_servers.filesystem]
enabled = true
command = "npx"
args = ["-y", "@modelcontextprotocol/server-filesystem", "."]
# Lifecycle hooks
hooks_enabled = true
[[hooks]]
name = "format-on-write"
trigger = "after_tool" # before_agent | after_agent | before_tool | after_tool | on_error
command = "ruff format ."
timeout_sec = 30There are four extension seams — none require touching the core.
Custom tool — .opendev/tools/my_tool.py:
from pydantic import BaseModel, Field
from tools.base import Tool, ToolInvocation, ToolKind, ToolResult
class MyParams(BaseModel):
name: str = Field(..., description="who to greet")
class MyTool(Tool):
name = "greet"
description = "Greet someone by name."
kind = ToolKind.READ
schema = MyParams
async def execute(self, invocation: ToolInvocation) -> ToolResult:
p = MyParams(**invocation.params)
return ToolResult.success_result(f"Hello, {p.name}!")Custom sub-agent — .opendev/agents/security_auditor.md:
---
name: security_auditor
description: Audits code for security vulnerabilities
tools: read_file, grep, glob, list_dir
max_turns: 15
---
You are a meticulous application-security reviewer. Report each finding with a
concrete path:line and a suggested fix.MCP server — add an [mcp_servers.<name>] block (see config above).
Hook — add a [[hooks]] block. Hooks receive context via AI_AGENT_* env vars
(AI_AGENT_TOOL_NAME, AI_AGENT_TOOL_PARAMS, AI_AGENT_RESPONSE, …). They observe and
react (format, test, log, notify, back up) but cannot veto a tool call — that is the
approval system's job.
Config ─▶ CLI/TUI ─▶ Agent ─▶ Session ─┬─ ContextManager (history, compaction, pruning)
├─ ToolRegistry ─▶ builtin / discovered / MCP / sub-agent tools
├─ LLMClient (unified streaming contract, retry)
├─ ApprovalManager + HookSystem (gate/observe tool calls)
├─ LoopDetector + ChatCompactor (loop-break / summarize)
└─ MCPManager (connect + register MCP tools)
LLMClient.chat_completionalways returnsAsyncGenerator[StreamEvent], so callers consume streaming and non-streaming identically.Sessionuses a two-phase init: sync__init__builds managers; asyncinitialize()connects MCP, runs tool discovery, then builds the system prompt (which must list every tool).- Approval and hooks are woven into
ToolRegistry.invoke, so every caller — including sub-agents — inherits them.
See notes.md for a deep dive (streaming internals, the agentic loop, context management, subagents, MCP, and an interview cheat-sheet) and CLAUDE.md for guidance when working in the codebase.
agent/ Agent loop, Session, events, persistence
client/ LLMClient + typed StreamEvent schema
config/ Pydantic config + layered TOML loader
context/ ContextManager, ChatCompactor, LoopDetector
hooks/ HookSystem (lifecycle shell hooks)
prompts/ System-prompt assembly
safety/ Dangerous-command detection + ApprovalManager
subagents/ SubAgent + markdown loader
tools/ base.py, registry, discovery, builtin/, mcp/
ui/ Rich TUI
utils/ paths, text, errors
test/ Standalone test scripts (see test/README.md)
public/ Design diagrams
main.py CLI entry point
Tests are self-contained assertion scripts (no pytest needed). Each resolves the repo
root itself, so you can run them from anywhere:
python test/run_all.py # everything (~273 checks across 15 suites)
python test/test_tools.py # a single suiteSee test/README.md for the full suite breakdown.
Most of the planned feature set (public/FeatureList) is implemented. Known scope
boundaries:
- Hooks are fire-and-forget — they cannot block a tool call (use approval policies to block).
- Checkpoints capture conversation state only — restoring does not revert file edits.
- The approval prompt shows the target path/command, but not yet a full edit diff.
web_search/web_fetchrequire network access; the LLM client's retry path has no mocked unit test.
