____ ____ ___ __ __ _____ _____ _ _ _____ _ _ ____
| _ \| _ \ / _ \| \/ || ____|_ _| | | | ____| | | / ___|
| |_) | |_) | | | | |\/| || _| | | | |_| | _| | | | \___ \
| __/| _ <| |_| | | | || |___ | | | _ | |___| |_| |___) |
|_| |_| \_\\___/|_| |_||_____| |_| |_| |_|_____|\___/|____/
Give an agent a loop. Give the loop tools. Let it steal the fire.
Prometheus is a Python agent runner: a tight execution loop that calls a model, dispatches tools, and feeds results back until the job is done.
No framework maze. No hidden magic. One runner. Pluggable tools.
you ──► runner ──► model
▲ │
│ ▼
└── tools ◄┘
| Runner-first | The loop is the product. Everything else plugs into it. |
| Tools as contracts | Register a function, get a schema, run it. |
| Poetry-native | Reproducible installs, locked deps, one command to start. |
| Small surface | Read the source in an afternoon. Extend it in an evening. |
# Clone
git clone git@github.com:FedericoGabrielCastro/Prometheus.git
cd Prometheus
# Install (Python 3.12+)
poetry install
# Echo loop (no API key)
poetry run prometheus run --model echo "steal the fire"
# Real model (needs OPENAI_API_KEY)
poetry run prometheus run "what files are here?"
# Run tests
poetry run pytestPlug in any model that implements complete(messages) -> AssistantReply. The runner does the rest.
from prometheus import AgentRunner, AssistantReply
class Echo:
def complete(self, messages):
last = messages[-1].content
return AssistantReply(content=f"heard: {last}")
result = AgentRunner(Echo(), system_prompt="keep it short").run("steal the fire")
print(result.output) # heard: steal the fire
print(result.stop_reason) # completed
print(len(result.turns)) # 1Stop conditions:
| Reason | When |
|---|---|
completed |
The model replies with no tool calls |
max_turns |
The loop hits the turn budget (default 16) |
If the model asks for a tool, the runner executes it (or records an error) and feeds the result back as a tool message. Tool exceptions never kill the loop.
Register a function. Prometheus builds the JSON schema from type hints, Annotated metadata, and the docstring.
from prometheus import AgentRunner, ToolRegistry, tool
@tool
def spark(n: int = 1) -> str:
"""Make n sparks."""
return "ember" * n
tools = ToolRegistry([spark])
result = AgentRunner(your_model, tools=tools).run("ignite")The runner passes tools.schemas() into every model.complete(...) call so the model can see what it is allowed to use. Unknown names, missing arguments, and extra arguments come back as error: tool messages — they do not crash the loop.
Filesystem paths that resolve outside the workspace are rejected. run_command starts with cwd at the workspace root. HTTP is http(s) only. Output is clipped so a huge file cannot flood the model.
from prometheus import AgentRunner, builtin_tools
tools = builtin_tools(".", shell=True, http=True)
result = AgentRunner(your_model, tools=tools).run("what files are here?")| Tool | Role |
|---|---|
read_file / write_file / list_dir |
UTF-8 files under the workspace |
run_command |
Shell command with cwd at the workspace root |
http_get |
Fetch an http or https URL |
Disable a group when you do not want it: builtin_tools(root, shell=False, http=False).
prometheus run "steal the fire"
prometheus run --model echo "no cloud required"
prometheus run --root . --no-shell --no-http -v "what files are here?"| Flag | Meaning |
|---|---|
--model echo | openai |
Local echo, or OpenAI-compatible chat. Default is openai when OPENAI_API_KEY is set. |
--model-name |
Remote model id (default gpt-4o-mini) |
--base-url |
OpenAI-compatible API root |
--root |
Workspace for built-in tools |
--no-filesystem / --no-shell / --no-http |
Disable a built-in group |
--max-turns / --system / -v |
Turn budget, system prompt, trace tool hops |
Compatible servers (Ollama, vLLM, LiteLLM, …) work via --base-url.
flowchart LR
U[User / CLI] --> R[Agent Runner]
R --> M[Model]
M -->|tool call| R
R --> T[Tool Registry]
T --> F[Filesystem]
T --> S[Shell]
T --> H[HTTP]
T --> X[Your tool]
F --> R
S --> R
H --> R
X --> R
R -->|final answer| U
The runner owns the loop. Tools never talk to the model. The model never touches the filesystem. That boundary is the whole design.
Prometheus/
├── src/prometheus/
│ ├── cli.py # `prometheus run`
│ ├── providers.py # Echo + OpenAI-compatible models
│ ├── runner.py # Agent loop, turn state, stop conditions
│ ├── tools.py # @tool, registry, JSON schemas
│ ├── builtins.py # Filesystem, shell, HTTP
│ ├── schema.py # Type hints → JSON Schema
│ ├── model.py # Model protocol
│ └── types.py # Messages, turns, stop reasons
├── tests/
├── pyproject.toml
└── README.md
Built as a sequence of small, reviewable PRs:
- 0 — Bootstrap — Poetry project, layout, tests, this README
- 1 — Runner — Agent loop, turn state, stop conditions
- 2 — Tools — Tool protocol, registry, schema generation
- 3 — Built-ins — First-party tools the runner can actually use
- 4 — CLI —
prometheus runfrom the terminal
- Python 3.12+
- Poetry
MIT. See LICENSE.
Steal the fire. Keep the loop honest.