https://github.com/luispabon/steiner
https://github.com/luispabon/steiner
Last synced: about 1 month ago
JSON representation
- Host: GitHub
- URL: https://github.com/luispabon/steiner
- Owner: luispabon
- Created: 2026-04-20T21:52:45.000Z (3 months ago)
- Default Branch: main
- Last Pushed: 2026-05-29T09:00:48.000Z (about 1 month ago)
- Last Synced: 2026-05-29T11:04:02.390Z (about 1 month ago)
- Language: Go
- Size: 15.1 MB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Roadmap: docs/ROADMAP.md
- Agents: AGENTS.md
Awesome Lists containing this project
README
# steiner
`steiner` is a minimal, local-first coding agent in Go. It is built to work against real local repositories with bounded context, explicit approvals, and a split provider/model configuration that supports local OpenAI-compatible servers plus native provider integrations.
## Today in brief
* Single-agent loop with interactive terminal mode and `--exec`
* Config, tools, skills, and version commands are available now
* Default provider targets a local OpenAI-compatible endpoint
* Mutating tools are approval-gated by default; reads are auto-approved
## Quickstart
Requirements: Go `1.25+`.
1. Start from source in this repo.
2. Make sure an OpenAI-compatible server is running at `http://localhost:11434/v1`, or override the provider/model settings in config.
3. Run a first request from source:
```bash
go run ./cmd/steiner --exec "summarize this repository in one sentence"
```
4. Optional: inspect the resolved provider/model configuration before you do real work:
```bash
go run ./cmd/steiner config
```
If you want a local binary:
```bash
make build-binaries
./bin/steiner
```
## Common commands
* `go run ./cmd/steiner` or `./bin/steiner` - interactive mode
* `go run ./cmd/steiner --exec "..."` - run one request and exit
* `go run ./cmd/steiner version` - print the build version
* `go run ./cmd/steiner config` - print the resolved configuration, including provider and model definitions
* `go run ./cmd/steiner tools` - list configured tools
* `go run ./cmd/steiner skills` - list discovered skills
## Configuration
Configuration precedence is:
1. compiled defaults
2. `~/.config/steiner/config.yaml`
3. `.steiner/config.yaml`
4. environment variables with the `STEINER_` prefix
5. CLI flags
Key environment variables:
* `STEINER_MODEL`
* `STEINER_SCHEDULER_PARALLELISM`
* `STEINER_MAX_TURNS`
* `STEINER_MAX_TOKENS`
* `STEINER_CAVEMAN_MODE`
* `STEINER_LOG_LEVEL`
* `STEINER_LOG_FILE`
* `STEINER_TOOL_OUTPUT_MAX_BYTES`
### Config format
Configuration is split between `providers` and `models`:
* `providers.` defines how Steiner connects to an API endpoint or native provider.
* `models.` defines a selectable model alias, references one provider with `provider`, and sets model-specific request behavior.
* `default_model` selects the model alias used by default.
The compiled defaults in `internal/config/defaults.go` are equivalent to one local provider plus one default model alias.
Example 1: minimal local config for LM Studio or Ollama:
```yaml
default_model: local
providers: {local: {type: openai_compat, base_url: http://localhost:11434/v1}}
models: {local: {provider: local, id: qwen3-35b-a3b}}
```
Use `http://127.0.0.1:1234/v1` as the `base_url` when pointing the same shape at LM Studio.
Example 2: OpenRouter with `api_key_env`:
```yaml
default_model: sonnet
providers:
openrouter:
type: openrouter
api_key_env: OPENROUTER_API_KEY
models:
sonnet:
provider: openrouter
id: anthropic/claude-3.7-sonnet
```
Example 3: multiple providers with multiple models:
```yaml
default_model: local-fast
providers:
local:
type: ollama
base_url: http://localhost:11434
router:
type: openrouter
api_key_env: OPENROUTER_API_KEY
models:
local-fast:
provider: local
id: qwen3:14b
local-deep:
provider: local
id: deepseek-r1:32b
sonnet:
provider: router
id: anthropic/claude-3.7-sonnet
gpt-4.1-mini:
provider: router
id: openai/gpt-4.1-mini
```
Example 4: tuned model with `params`, `extra_params`, and a prompt suffix:
```yaml
default_model: precise
providers:
local:
type: lmstudio
base_url: http://127.0.0.1:1234/v1
models:
precise:
provider: local
id: qwen/qwen3-coder-30b
params:
temperature: 0.1
top_p: 0.9
extra_params:
frequency_penalty: 0
metadata:
profile: precise
prompt_suffix: <|think_off|>
```
Example 5: advanced overrides with explicit limits:
```yaml
default_model: long-context
providers:
local:
type: openai_compat
base_url: http://localhost:11434/v1
timeout: 45s
models:
long-context:
provider: local
id: qwen3-32b
advanced:
limits:
context_window: 131072
max_output_tokens: 8192
limits:
max_turns: 80
max_tokens: 900000
tool_timeout_default: 45s
tool_output_max_bytes: 131072
```
Example 6: multi-model setup with `default_model`:
```yaml
default_model: everyday
providers:
local:
type: openai_compat
base_url: http://localhost:11434/v1
models:
everyday:
provider: local
id: qwen3-14b
review:
provider: local
id: qwen3-32b
extra_params:
thinking:
type: enabled
budget_tokens: 16000
```
Provider fields:
* `type`: `openai_compat`, `ollama`, `lmstudio`, `openrouter`, `openai`, `anthropic`, `gemini`, or `litellm`
* `base_url`: endpoint override when the provider type uses one
* `api_key` or `api_key_env`: credential source
* `headers`: optional extra HTTP headers
* `timeout`: provider request timeout
Model fields:
* `provider`: provider name from `providers`
* `id`: backend model identifier sent to the provider
* `params`: normalized generation params
* `extra_params`: provider-specific request fields merged on top of `params`
* `prompt_suffix`: optional text appended to the last user message for each model request
* `retry`: retry policy for model requests
* `prompts`: per-model prompt overrides
* `advanced.limits`: prompt budgeting and output token limits
Approval defaults are conservative: `read`, `glob`, `grep`, and `ls` are auto-approved; mutating actions like `write`, `edit`, and `bash` prompt first. For most installs, the minimum useful config is `default_model`, one provider entry, one model entry that points at that provider, and any overrides you actually need in `limits`, `approval`, `tools`, `project_context`, `paths`, or `logging`.
## Web search
The `web_search` tool lets the model search the web and return URL, title, and description results. It is not available by default — it must be enabled by setting `search.backend` in the config.
### Supported backends
| Backend | Config key | Auth |
|---------|------------|------|
| **Google** | `google` | `GOOGLE_SEARCH_CX` + `GOOGLE_SEARCH_API_KEY` env vars |
| **Kagi** | `kagi` | `KAGI_API_KEY` env var |
| **Brave** | `brave` | `BRAVE_API_KEY` env var |
| **SearXNG** | `searxng` | `search.searxng_url` config key (self-hosted, no API key) |
Setting `search.backend` to `""` (or omitting the `search` block entirely) disables the `web_search` tool. The model will not have access to web search.
### Config example
```yaml
search:
backend: google # one of: google, kagi, brave, searxng
# searxng_url: http://localhost:8888 # required when backend is "searxng"
```
#### Per-backend environment variables
**Google:**
```bash
export GOOGLE_SEARCH_CX=your_custom_search_engine_id
export GOOGLE_SEARCH_API_KEY=your_google_api_key
```
**Kagi:**
```bash
export KAGI_API_KEY=your_kagi_api_key
```
**Brave:**
```bash
export BRAVE_API_KEY=your_brave_api_key
```
**SearXNG:** no API key required. Point `search.searxng_url` at your instance.
### How it works
The search config is loaded from the `search` block in `config.yaml` (or the equivalent `STEINER_SEARCH_*` env vars). At startup `NewSearchBackend` dispatches on `search.backend` and creates the corresponding `web.Searcher` implementation.
When a searcher is available:
- The `web_search` tool is registered in the active tool registry (visible to the main model).
- The `research` sub-agent also gains `web_search` in its allowlist.
When no searcher is available (`search.backend` is unset):
- The `web_search` tool is not registered at all — the model never sees it.
- The `research` sub-agent exclude `web_search` from its available tools.
### All providers config example
This example shows all four backends configured across different config layers (only one can be active at a time — set `search.backend` to pick):
```yaml
# Only one backend is active at a time, selected by search.backend.
# Environment variables must be set for the selected backend (see above).
search:
backend: brave # change to google, kagi, or searxng as needed
searxng_url: http://localhost:8888 # only used when backend is "searxng"
```
### Notes
- The `web_search` tool is separate from the model provider — it uses its own HTTP client and does not route through the model provider configuration.
- SearXNG is self-hosted and has no API key; any public or private instance URL works.
- Brave caps results at 20. All other backends cap at 30 (the global limit is adjustable per invocation via the `limit` parameter, up to 30).
## Sub-agent delegation
`steiner` exposes seven sub-agent-as-tool operations that delegate bounded tasks to isolated child agents. Sub-agent delegation is **enabled by default** — the model sees the following tools:
- **`explore`** — navigate the codebase to find files, symbols, call sites, and patterns
- **`research`** — gather and synthesise information from the codebase or web (only available with a `web_search` backend configured)
- **`code`** — implement a scoped change, run tests, report results
- **`plan`** — analyse a sub-problem and produce a structured recommendation
- **`verify`** — run checks (tests, linters, builds) and report pass/fail
- **`delegate`** — generic sub-agent with custom system prompt, context, and per-invocation overrides
- **`follow_up`** — resume an existing sub-agent session by ID with a new user message; preserves conversation history while resetting the child's budget to fresh defaults
### Configuration
Sub-agents are configured under the `sub_agent` key in `config.yaml`:
```yaml
sub_agent:
enabled: true # set to false to remove sub-agent tools
max_turns: 30 # default turn limit
max_tokens: 100000 # default output token budget
allowed_tools: # tools for generic `delegate` only
- read
- glob
- grep
- ls
- write
- edit
- bash
- scratchpad
agents: # per-type model overrides (optional)
code:
model: gpt-4o
research:
model: claude-sonnet-4
```
See [docs/SUBAGENTS.md](docs/SUBAGENTS.md) for full documentation — including agent-specific tool allowlists, safety restrictions, and per-invocation overrides for the `delegate` tool.
## Caveman mode
Caveman mode makes the model speak tersely, stripping filler, articles, pleasantries, and hedging. Reduces tokens and response length while keeping technical content intact.
**Disabled by default.** Enable explicitly via config, env var, CLI flag, or `/caveman` slash command in interactive mode.
```yaml
# config.yaml
caveman_mode: true
```
```bash
# environment
STEINER_CAVEMAN_MODE=true
# CLI flag
--caveman
```
In interactive TUI, toggle on/off with `/caveman`. The toggle persists for the session.
When enabled, caveman-style instructions are injected into:
- **System preamble** — the main agent prompt instructs the model to respond tersely
- **Compaction prompts** — compaction summaries are written in caveman style to maximise information per token
- **Sub-agent prompts** — delegated agents inherit the terseness instruction
- **Scaffold inference** — internal scaffold generation also follows caveman style
Caveman mode is purely a prompt-layer transformation. It does not change tool behavior, approval gates, or any other config.
## Development
### Build and test
```bash
go build ./...
go test ./...
make build-binaries
go vet ./...
```
### Development checks
Install local check tools:
```bash
make install-check-tools
```
Run all checks:
```bash
make check
```
Formatting:
```bash
make fmt
```
The full linter configuration is in `.golangci.yml` at the repo root.
## Development notes
Repo layout is compact: `cmd/` for entrypoints, `internal/` for agent/provider/tool/config code, `docs/` for product docs, and `testdata/` for fixtures. Conventions and deeper repo rules live in [AGENTS.md](AGENTS.md).
## Further reading
* [AGENTS.md](AGENTS.md)
* [docs/PRD.md](docs/PRD.md)
* [docs/ROADMAP.md](docs/ROADMAP.md)
* [docs/INITIAL_IMPLEMENTATION_PLAN.md](docs/INITIAL_IMPLEMENTATION_PLAN.md)