Make agents do anything — with just a prompt and tools.
A simple, extendable agentic framework for running LLM-powered agents against any OpenAI-compatible local or remote inference server.
basic-agents provides a lightweight CLI harness for building and running specialized agents. Each agent lives in its own directory under agents/ and follows a naming convention that lets the framework load it dynamically. Sessions are persisted to a local SQLite database so conversations can be resumed across runs.
One of the core motivations behind this project is eliminating the massive context overhead that generalized agents carry by default. Instead of one bloated agent that knows about everything, you build lean, purpose-specific agents — each carrying only the system prompt, tools, and context it actually needs. This keeps inference fast and costs low, especially for long-running tasks.
The framework is intentionally minimal — no heavy dependencies, no cloud lock-in. It works out of the box with local inference servers like llama-server (llama.cpp / ik_llama.cpp) and with any OpenAI-compatible remote endpoint including OpenRouter.
basic-agents/
├── agents/ # One subdirectory per agent (e.g. agents/default/)
│ └── <name>/
│ ├── agent.py # Must export a class named <Name>Agent
│ ├── system_prompt.txt # Agent-specific system prompt
│ └── tools.py # Agent-specific tools
├── core/ # Base agent class, tool registry, and shared loop logic
├── db/ # SQLite session persistence (session_db.py)
├── utils/ # File helpers, LLM client, session manager, tool executor
├── tests/ # pytest test suite
├── main.py # CLI entry point with run + session subcommands
├── config.py # Global config (FILES_BASE_DIR)
├── providers.json # Provider/model config (not committed — copy providers.example.json)
├── providers.example.json # Template for providers.json
├── requirements.txt # Runtime deps: requests, ddgs
├── requirements-dev.txt # Dev deps: pytest, pytest-cov, pytest-mock, pytest-asyncio, python-dotenv
└── pytest.ini # pytest config
git clone https://github.com/roypr/basic-agents.git
cd basic-agents
pip install -r requirements.txt
# For development / running tests
pip install -r requirements-dev.txtThe CLI uses subcommands. The two main commands are run (to execute an agent) and session (to manage sessions).
Providers and models are configured in providers.json (see Providers). The default provider and model are picked up automatically, so the simplest invocation is:
python main.py run \
--agent default \
--query "What files are in my workspace?"Note: For backward compatibility, you can omit the
runsubcommand — the old flat-style invocation still works:python main.py --agent default --query "What files are in my workspace?"
To pick a specific provider and model:
python main.py run \
--agent default \
--provider deepseek \
--model deepseek-v4-flash \
--query "Refactor this module" \
--max-turns 50List everything configured in providers.json:
python main.py run --list-providersIf you want to bypass providers.json entirely (e.g. for a one-off local server), pass --llm-base, --model, and optionally --api-key:
python main.py run \
--agent default \
--query "Quick question" \
--llm-base http://localhost:8080 \
--model local \
--api-key sk-...This works with any provider that exposes an OpenAI-compatible /chat/completions endpoint.
For tasks that span many tool calls — large refactors, file processing pipelines, multi-step research — set --max-turns generously. If the agent hits the limit before finishing, resume from where it left off.
Resume a specific session by ID:
# Start a long task
python main.py run --agent default --query "Audit and fix all TODO comments in the repo" \
--max-turns 100 --session-name "todo-audit"
# If it stops, resume by session ID
python main.py run --agent default --query "Continue" --resume-session 3 --max-turns 100Or resume the latest active session automatically with --continue:
python main.py run --agent default --query "Continue" --continue --max-turns 100Note:
--continueand--resume-sessionare mutually exclusive.
Sessions persist the full conversation history, so the agent picks up with complete context intact.
| Flag | Default | Description |
|---|---|---|
--agent |
default |
Which agent to run (must match a directory under agents/) |
--query |
"" |
The question or task to send to the agent |
--provider |
providers.json default |
Provider name from providers.json |
--model |
providers.json default |
Model name for the chosen provider |
--llm-base |
— | Override the LLM base URL (bypasses providers.json) |
--api-key |
— | Override the API key (bypasses providers.json) |
--max-turns |
10 |
Maximum tool-call rounds before stopping — set generously for long tasks |
--files-base-dir |
/workspace |
Base directory exposed to file tools |
--include |
— | Path to a file whose contents are appended to the query |
--lines |
— | Line range from --include, e.g. 10-20 or 20 |
--resume-session |
— | Resume an existing session by ID |
--continue |
— | Resume the latest active session (mutually exclusive with --resume-session) |
--session-name |
Default Session |
Name for a new session |
--list-providers |
— | List configured providers and models, then exit |
You can inline a file (or a slice of it) into your query:
# Include entire file
python main.py run --agent default --query "Explain this code" --include ./myfile.py
# Include only lines 10–40
python main.py run --agent default --query "What does this function do?" \
--include ./myfile.py --lines 10-40Sessions store conversation history in a local SQLite database, allowing agents to resume where they left off. All session operations are available via the session subcommand:
# Create a session with a custom system prompt
python main.py session create --name "My Project" --system-prompt "You are a coding assistant."
# List all active sessions
python main.py session list
# Export a session to JSON
python main.py session get --id 1
# Delete a session
python main.py session delete --id 1Long sessions can be compressed to save storage and reduce context overhead. This exports the raw conversation to a JSON file and replaces it with a compressed summary:
python main.py session compress --id 1Options for compression:
| Flag | Default | Description |
|---|---|---|
--id |
(required) | Session ID to compress |
--output-dir |
logs |
Directory for the raw JSON export |
--summarize |
— | Also generate an LLM summary after compression |
--provider |
providers.json default |
Provider for summarization |
--model |
providers.json default |
Model for summarization |
--llm-base |
— | Override LLM base URL for summarization |
--api-key |
— | Override API key for summarization |
When running an agent, resume a specific session by ID:
python main.py run --agent default --query "Continue" --resume-session 1Or resume the latest active session automatically:
python main.py run --agent default --query "Continue" --continue- Create a directory under
agents/:
agents/myagent/
├── __init__.py
├── agent.py
├── system_prompt.txt # (optional) Agent-specific system prompt
└── tools.py # (optional) Agent-specific tools
- In
agent.py, define a class namedMyagentAgent(capitalized agent name +Agent):
from core.base_agent import BaseAgent
class MyagentAgent(BaseAgent):
def __init__(self, model, llm_base, max_turns, resume_session, session_name, api_key):
super().__init__(
model=model,
llm_base=llm_base,
max_turns=max_turns,
resume_session=resume_session,
session_name=session_name,
api_key=api_key,
)
def run(self, query: str):
# Implement your agent loop here
...- Run it:
python main.py run --agent myagent --query "Do something"The framework discovers agents by directory name, so no registration step is needed.
Model endpoints are configured in providers.json at the project root. The file is intentionally lightweight — just providers, their API URLs/keys, and the models each exposes. No router, fallback, or transformer logic.
{
"default_provider": "deepseek",
"default_model": "deepseek-v4-flash",
"providers": [
{
"name": "deepseek",
"api_base_url": "https://api.deepseek.com/chat/completions",
"api_key": "sk-...",
"models": ["deepseek-v4-pro", "deepseek-v4-flash"]
},
{
"name": "local",
"api_base_url": "http://localhost:8080/chat/completions",
"api_key": "sk-",
"models": ["claude-sonet-4.6"]
}
]
}default_provider/default_model— used when--provider/--modelare omitted.providers[].name— referenced by--provider.providers[].models— list of valid model names for--model.
A providers.example.json template is included. Copy it to get started:
cp providers.example.json providers.json
# then edit providers.json with your real API keys
providers.jsoncontains secrets and should not be committed. It is already covered by.gitignoreif you add it there.
FILES_BASE_DIR controls the root directory that file tools can access. It defaults to /workspace (or ./workspace on Windows) and can be overridden via environment variable or --files-base-dir:
export FILES_BASE_DIR=/home/user/projects
# or
python main.py run --agent default --query "..." --files-base-dir /home/user/projectspytest
# or
python run_tests.pyRuntime
requests— HTTP client for LLM server communicationddgs— DuckDuckGo search (used by built-in search tools)
Dev
pytest,pytest-cov,pytest-mock,pytest-asyncio— testingpython-dotenv—.envsupport for local dev
Designed to work with any OpenAI-compatible /chat/completions endpoint. Tested with local servers running via llama.cpp / ik_llama.cpp. Works with remote providers configured in providers.json — including OpenRouter, DeepSeek, OpenAI, Together AI, and others. For one-off runs against an unconfigured endpoint, use --llm-base / --model / --api-key to bypass providers.json.