robot_lab-to 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.envrc +1 -0
- data/.github/workflows/deploy-github-pages.yml +52 -0
- data/.loki +21 -0
- data/.quality/reek_baseline.txt +17 -0
- data/.rubocop.yml +7 -0
- data/CHANGELOG.md +67 -0
- data/CLAUDE.md +74 -0
- data/COMMITS.md +27 -0
- data/README.md +352 -0
- data/Rakefile +132 -0
- data/bin/robot-to +14 -0
- data/docs/about/changelog.md +82 -0
- data/docs/assets/architecture.svg +110 -0
- data/docs/assets/images/robot_lab-to.jpg +0 -0
- data/docs/assets/iteration-loop.svg +97 -0
- data/docs/concepts/evals.md +254 -0
- data/docs/concepts/iteration-loop.md +128 -0
- data/docs/concepts/notes.md +91 -0
- data/docs/concepts/run-state.md +93 -0
- data/docs/concepts/stop-conditions.md +111 -0
- data/docs/concepts/verification.md +75 -0
- data/docs/configuration/cli.md +111 -0
- data/docs/configuration/index.md +93 -0
- data/docs/configuration/settings.md +270 -0
- data/docs/getting-started/anatomy-of-a-run.md +111 -0
- data/docs/getting-started/installation.md +78 -0
- data/docs/getting-started/quick-start.md +105 -0
- data/docs/index.md +96 -0
- data/docs/local-models/guardrails.md +117 -0
- data/docs/local-models/index.md +88 -0
- data/docs/local-models/ollama.md +122 -0
- data/docs/local-models/tools.md +92 -0
- data/docs/reference/architecture.md +139 -0
- data/examples/01_basic_usage/.gitignore +9 -0
- data/examples/01_basic_usage/README.md +173 -0
- data/examples/01_basic_usage/basic_usage.rb +386 -0
- data/examples/02_advanced_usage/.gitignore +8 -0
- data/examples/02_advanced_usage/README.md +117 -0
- data/examples/02_advanced_usage/advanced_usage.rb +308 -0
- data/examples/02_advanced_usage/quality_gate.rb +99 -0
- data/examples/03_scored/.gitignore +2 -0
- data/examples/03_scored/README.md +117 -0
- data/examples/03_scored/scored_run.rb +325 -0
- data/examples/04_prose/.gitignore +2 -0
- data/examples/04_prose/README.md +43 -0
- data/examples/04_prose/prompts_dir/grade_message.md +14 -0
- data/examples/04_prose/prompts_dir/judge.md +11 -0
- data/examples/04_prose/prompts_dir/outline_criteria.md +9 -0
- data/examples/04_prose/prompts_dir/outline_objective.md +11 -0
- data/examples/04_prose/prompts_dir/section_criteria.md +7 -0
- data/examples/04_prose/prompts_dir/sections_objective.md +14 -0
- data/examples/04_prose/prose_run.rb +302 -0
- data/lib/robot_lab/to/atomic_file.rb +69 -0
- data/lib/robot_lab/to/backoff.rb +46 -0
- data/lib/robot_lab/to/cli.rb +176 -0
- data/lib/robot_lab/to/commit_manager.rb +135 -0
- data/lib/robot_lab/to/config/defaults.yml +27 -0
- data/lib/robot_lab/to/config.rb +86 -0
- data/lib/robot_lab/to/decision.rb +40 -0
- data/lib/robot_lab/to/decision_manager.rb +193 -0
- data/lib/robot_lab/to/errors.rb +30 -0
- data/lib/robot_lab/to/evals/base.rb +23 -0
- data/lib/robot_lab/to/evals/code.rb +77 -0
- data/lib/robot_lab/to/evals/context.rb +17 -0
- data/lib/robot_lab/to/evals/factory.rb +84 -0
- data/lib/robot_lab/to/evals/null.rb +17 -0
- data/lib/robot_lab/to/evals/prose.rb +110 -0
- data/lib/robot_lab/to/evals/score.rb +22 -0
- data/lib/robot_lab/to/exit_summary.rb +75 -0
- data/lib/robot_lab/to/guards/checkpoint.rb +84 -0
- data/lib/robot_lab/to/guards/grader_lock.rb +62 -0
- data/lib/robot_lab/to/guards/path_resolution.rb +61 -0
- data/lib/robot_lab/to/guards/quality_monitor.rb +113 -0
- data/lib/robot_lab/to/guards/read_before_edit.rb +83 -0
- data/lib/robot_lab/to/guards/run_store.rb +31 -0
- data/lib/robot_lab/to/guards/write_guard.rb +71 -0
- data/lib/robot_lab/to/guards.rb +54 -0
- data/lib/robot_lab/to/iteration_result.rb +29 -0
- data/lib/robot_lab/to/jsonl_logger.rb +59 -0
- data/lib/robot_lab/to/notes_manager.rb +121 -0
- data/lib/robot_lab/to/orchestrator.rb +642 -0
- data/lib/robot_lab/to/prompt_builder.rb +208 -0
- data/lib/robot_lab/to/run.rb +109 -0
- data/lib/robot_lab/to/stop_conditions.rb +71 -0
- data/lib/robot_lab/to/tools/bash.rb +75 -0
- data/lib/robot_lab/to/tools/edit.rb +52 -0
- data/lib/robot_lab/to/tools/file_tool.rb +32 -0
- data/lib/robot_lab/to/tools/read.rb +57 -0
- data/lib/robot_lab/to/tools/request_decision.rb +66 -0
- data/lib/robot_lab/to/tools/submit_result.rb +50 -0
- data/lib/robot_lab/to/tools/write.rb +32 -0
- data/lib/robot_lab/to/verifier.rb +63 -0
- data/lib/robot_lab/to/version.rb +7 -0
- data/lib/robot_lab/to.rb +81 -0
- data/mkdocs.yml +145 -0
- metadata +175 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Installation
|
|
2
|
+
|
|
3
|
+
## Requirements
|
|
4
|
+
|
|
5
|
+
| Requirement | Notes |
|
|
6
|
+
|-------------|-------|
|
|
7
|
+
| Ruby **>= 3.2** | Set in the gemspec `required_ruby_version`. |
|
|
8
|
+
| Git | A repository with at least one commit. The loop creates a branch and commits there. |
|
|
9
|
+
| `robot_lab` | The core framework `robot_lab-to` builds on. |
|
|
10
|
+
| An LLM provider | A cloud API key, **or** a local [Ollama](https://ollama.com) server (see [Local Models](../local-models/index.md)). |
|
|
11
|
+
|
|
12
|
+
## Install the gem
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
gem install robot_lab-to
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Or add it to a `Gemfile`:
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
gem "robot_lab-to"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
bundle install
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
This installs the `robot-to` executable on your `PATH`.
|
|
29
|
+
|
|
30
|
+
## Provider credentials
|
|
31
|
+
|
|
32
|
+
`robot_lab-to` passes your configured provider and model straight through to
|
|
33
|
+
RobotLab / RubyLLM. Provide credentials the usual way for your provider:
|
|
34
|
+
|
|
35
|
+
=== "Anthropic"
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
export ANTHROPIC_API_KEY="sk-ant-..."
|
|
39
|
+
robot-to "..." --provider anthropic --model claude-sonnet-4-6
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
=== "OpenAI"
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
export OPENAI_API_KEY="sk-..."
|
|
46
|
+
robot-to "..." --provider openai --model gpt-5.5
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
=== "Local (Ollama)"
|
|
50
|
+
|
|
51
|
+
No API key required. See [Ollama Setup](../local-models/ollama.md) for the
|
|
52
|
+
full configuration — local models need `--provider openai` against Ollama's
|
|
53
|
+
OpenAI-compatible endpoint plus `--no-stream` and `--local-guards`.
|
|
54
|
+
|
|
55
|
+
The default provider/model is `openai` / `gpt-5.5` (see
|
|
56
|
+
[Settings Reference](../configuration/settings.md)).
|
|
57
|
+
|
|
58
|
+
## Verify the installation
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
robot-to --version
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Prepare your project
|
|
65
|
+
|
|
66
|
+
The loop branches from `HEAD` and commits each successful iteration, so the
|
|
67
|
+
working repository must have at least one commit:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
git init
|
|
71
|
+
git add -A
|
|
72
|
+
git commit -m "initial" # or: git commit --allow-empty -m "initial"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Run state is written under `.robot_lab_to/` and automatically added to
|
|
76
|
+
`.git/info/exclude`, so it never pollutes your working tree or commits.
|
|
77
|
+
|
|
78
|
+
You're ready — continue to the [Quick Start](quick-start.md).
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Quick Start
|
|
2
|
+
|
|
3
|
+
This walkthrough takes you from a clean repository to a finished overnight run.
|
|
4
|
+
|
|
5
|
+
## 1. Start in a git repository with a commit
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
cd my-project
|
|
9
|
+
git status # must be a git repo
|
|
10
|
+
git log --oneline # must have at least one commit
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
If there are no commits yet:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
git commit --allow-empty -m "initial"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## 2. Run a bounded objective
|
|
20
|
+
|
|
21
|
+
Give `robot-to` a clear, single-sentence objective and a stop condition. Keep
|
|
22
|
+
the first run small and bounded so you can watch it work:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
robot-to "Add a CHANGELOG.md and document the public API in the README" \
|
|
26
|
+
--max-iterations 5 \
|
|
27
|
+
--provider anthropic \
|
|
28
|
+
--model claude-sonnet-4-6
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
What happens:
|
|
32
|
+
|
|
33
|
+
- A branch `robot-to/<slugified-objective>-<timestamp>` is created.
|
|
34
|
+
- For up to 5 iterations, a fresh robot makes one focused change and reports a
|
|
35
|
+
result.
|
|
36
|
+
- Each successful iteration becomes one commit on the branch.
|
|
37
|
+
- Failures are rolled back (`git reset --hard`) and recorded in the notes.
|
|
38
|
+
|
|
39
|
+
## 3. Add an independent verification gate
|
|
40
|
+
|
|
41
|
+
The robot's self-reported success only counts if a command **you** choose
|
|
42
|
+
passes. This is the single most important flag for unattended runs:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
robot-to "Fix the failing tests in spec/parser" \
|
|
46
|
+
--verify-command "bundle exec rake test" \
|
|
47
|
+
--max-iterations 15 \
|
|
48
|
+
--max-consecutive-failures 4
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
If `--verify-command` exits non-zero (or times out), the iteration is rolled
|
|
52
|
+
back even though the robot claimed success. See [Verification Gate](../concepts/verification.md).
|
|
53
|
+
|
|
54
|
+
## 4. Let it stop itself
|
|
55
|
+
|
|
56
|
+
For a true overnight run, combine a hard ceiling with a natural-language stop
|
|
57
|
+
condition:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
robot-to "Migrate the codebase from Minitest to RSpec" \
|
|
61
|
+
--verify-command "bundle exec rake" \
|
|
62
|
+
--max-iterations 50 \
|
|
63
|
+
--max-tokens 2000000 \
|
|
64
|
+
--stop-when "all test files are RSpec and the suite passes"
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The robot evaluates `--stop-when` after each successful iteration and sets
|
|
68
|
+
`should_fully_stop` when it judges the condition met. See
|
|
69
|
+
[Stop Conditions](../concepts/stop-conditions.md).
|
|
70
|
+
|
|
71
|
+
## 5. Review in the morning
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
git log --oneline robot-to/migrate-the-codebase-from-... # the iteration history
|
|
75
|
+
git diff main..HEAD # the full change
|
|
76
|
+
cat .robot_lab_to/runs/*/notes.md # the reasoning log
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Each commit is one iteration. Squash, cherry-pick, or reset as you like — it's
|
|
80
|
+
an ordinary branch.
|
|
81
|
+
|
|
82
|
+
## Programmatic use
|
|
83
|
+
|
|
84
|
+
You can also drive a run from Ruby instead of the CLI:
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
require "robot_lab"
|
|
88
|
+
require "robot_lab/to"
|
|
89
|
+
|
|
90
|
+
RobotLab::To.run(
|
|
91
|
+
"Add a CHANGELOG.md and document the public API",
|
|
92
|
+
provider: :anthropic,
|
|
93
|
+
model: "claude-sonnet-4-6",
|
|
94
|
+
max_iterations: 5,
|
|
95
|
+
verify_command: "bundle exec rake test"
|
|
96
|
+
)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`RobotLab::To.run(objective, **opts)` accepts the same options as the CLI flags
|
|
100
|
+
(as keyword arguments). See the [Settings Reference](../configuration/settings.md).
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
Next: understand exactly what one run produces in
|
|
105
|
+
[Anatomy of a Run](anatomy-of-a-run.md).
|
data/docs/index.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="assets/images/robot_lab-to.jpg" alt="robot_lab-to" width="320"><br />
|
|
3
|
+
"Robot, takeover!"
|
|
4
|
+
</p>
|
|
5
|
+
|
|
6
|
+
# robot_lab-to
|
|
7
|
+
|
|
8
|
+
**Autonomous overnight agent loop for [RobotLab](https://github.com/MadBomber/robot_lab) — run a robot while you sleep.**
|
|
9
|
+
|
|
10
|
+
`robot_lab-to` ("takeover") runs a RobotLab `Robot` in an autonomous loop toward a
|
|
11
|
+
stated objective, committing **one focused change per iteration**. Each iteration
|
|
12
|
+
spins up a fresh robot, lets it work, and asks it to report a result. Good
|
|
13
|
+
iterations are committed; failures are rolled back. A running log (`notes.md`)
|
|
14
|
+
carries memory across iterations so the robot learns from what came before.
|
|
15
|
+
|
|
16
|
+
Start it before bed; review the branch in the morning.
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
robot-to "Increase test coverage of the parser to 90%" \
|
|
20
|
+
--max-iterations 20 \
|
|
21
|
+
--verify-command "bundle exec rake test"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## How it works
|
|
27
|
+
|
|
28
|
+
Every iteration is an independent, sandboxed attempt at *one* improvement. The
|
|
29
|
+
orchestrator owns the git history and the stop conditions; the robot owns the
|
|
30
|
+
work.
|
|
31
|
+
|
|
32
|
+

|
|
33
|
+
|
|
34
|
+
1. **Build a fresh robot** with a per-iteration system prompt that includes the
|
|
35
|
+
objective and the accumulated notes.
|
|
36
|
+
2. **The robot works** and calls `submit_iteration_result` to report success or
|
|
37
|
+
failure.
|
|
38
|
+
3. **The orchestrator decides** — an independent `--verify-command` (if set) must
|
|
39
|
+
pass, then the change is committed. Otherwise the working tree is reset.
|
|
40
|
+
4. **Notes are updated** with what happened, becoming context for the next round.
|
|
41
|
+
5. **Stop conditions are checked** — iterations, tokens, consecutive failures, or
|
|
42
|
+
a natural-language `--stop-when` — and the loop continues or ends.
|
|
43
|
+
|
|
44
|
+
See [The Iteration Loop](concepts/iteration-loop.md) for the full lifecycle.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Why use it
|
|
49
|
+
|
|
50
|
+
- **Bounded, reviewable history.** Each iteration is a single commit on a
|
|
51
|
+
dedicated branch. Bad attempts never reach the branch — they are reset before
|
|
52
|
+
the next try.
|
|
53
|
+
- **Cross-iteration memory.** `notes.md` records summaries, changes, and learnings
|
|
54
|
+
so the robot doesn't repeat itself. See [Cross-Iteration Memory](concepts/notes.md).
|
|
55
|
+
- **Independent verification.** The robot can't self-certify success — a real
|
|
56
|
+
command you choose is the deciding authority. See [Verification Gate](concepts/verification.md).
|
|
57
|
+
- **Scored, descending iterations.** An **eval** replaces the robot's self-report
|
|
58
|
+
with an orchestrator-owned measurable judgement — a deterministic `--measure`/
|
|
59
|
+
`--target` for code, or a pairwise LLM judge for prose — so the branch only
|
|
60
|
+
ever moves forward. See [Evals](concepts/evals.md).
|
|
61
|
+
- **It stops on its own.** Hard limits plus a natural-language stop condition mean
|
|
62
|
+
you can leave it unattended. See [Stop Conditions](concepts/stop-conditions.md).
|
|
63
|
+
- **It asks before overstepping.** For irreversible or ambiguous choices the robot
|
|
64
|
+
writes a **decision file** — situation, options, and its recommendation — that
|
|
65
|
+
you resolve out-of-band instead of letting it guess. Run `robot-to decisions`
|
|
66
|
+
to see what's waiting on you.
|
|
67
|
+
- **Resumable & cron-friendly.** Run state is persisted to `run.json`, so
|
|
68
|
+
`robot-to --resume <run_id>` continues a stopped run and an external scheduler
|
|
69
|
+
can drive it one commit per tick.
|
|
70
|
+
- **Runs on local models.** With `--local-guards` it ships built-in file tools and
|
|
71
|
+
small-model guardrails, so it can drive a local Ollama model offline. See
|
|
72
|
+
[Local Models](local-models/index.md).
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Quick links
|
|
77
|
+
|
|
78
|
+
<div class="grid cards" markdown>
|
|
79
|
+
|
|
80
|
+
- :material-rocket-launch: **[Installation](getting-started/installation.md)** — install the gem and the `robot-to` CLI.
|
|
81
|
+
- :material-play: **[Quick Start](getting-started/quick-start.md)** — your first overnight run in five minutes.
|
|
82
|
+
- :material-cog: **[Configuration](configuration/index.md)** — every setting, the config cascade, and the CLI.
|
|
83
|
+
- :material-laptop: **[Local Models](local-models/index.md)** — drive a local Ollama model with guardrails.
|
|
84
|
+
- :material-sitemap: **[Architecture](reference/architecture.md)** — how the pieces fit together.
|
|
85
|
+
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Requirements
|
|
91
|
+
|
|
92
|
+
- Ruby **>= 3.2**
|
|
93
|
+
- A git repository with at least one commit (the loop branches and commits there)
|
|
94
|
+
- The [`robot_lab`](https://github.com/MadBomber/robot_lab) gem and an LLM provider
|
|
95
|
+
(a cloud key such as `ANTHROPIC_API_KEY` / `OPENAI_API_KEY`, **or** a local
|
|
96
|
+
[Ollama](https://ollama.com) server)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Guardrails
|
|
2
|
+
|
|
3
|
+
The guardrails are `RobotLab::Hook` policies that intercept the robot's tool calls
|
|
4
|
+
to catch the mistakes small local models reliably make. They are enabled with
|
|
5
|
+
`--local-guards` and are no-ops you'd never notice with a frontier model — they
|
|
6
|
+
exist for the 8–35B local case.
|
|
7
|
+
|
|
8
|
+
They are adapted from the open-source [`little-coder`](https://github.com/itayinbarr/little-coder)
|
|
9
|
+
harness, whose core finding is that a guard-rich scaffold raised the same small
|
|
10
|
+
model's coding-benchmark score by **2.4×**. The harness, not the model, does the
|
|
11
|
+
heavy lifting.
|
|
12
|
+
|
|
13
|
+
## The four guards
|
|
14
|
+
|
|
15
|
+
### write-guard
|
|
16
|
+
|
|
17
|
+
**Problem:** small models love to "fix" a file by rewriting the whole thing with
|
|
18
|
+
`write`, destroying everything they didn't think to include.
|
|
19
|
+
|
|
20
|
+
**Policy:** refuse `write` to a file that already exists and tell the model to use
|
|
21
|
+
`edit` instead. It also normalizes a root-bare path like `/foo.rb` (a common
|
|
22
|
+
small-model mistake) to the working directory.
|
|
23
|
+
|
|
24
|
+
Implemented as `around_tool_call` (to block) + `before_tool_call` (to normalize
|
|
25
|
+
the path).
|
|
26
|
+
|
|
27
|
+
**Opting out:** `write-guard` is the one guard in the set that can get in the way
|
|
28
|
+
of *legitimate* work — a robot iteratively rewriting a whole prose draft each
|
|
29
|
+
pass wants plain `write`, not a forced `edit`. Set `write_guard: false` (Ruby-only,
|
|
30
|
+
default `true`; no CLI flag) to drop it from the installed set while keeping
|
|
31
|
+
`read-before-edit`, `checkpoint`, and `quality-monitor`:
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
RobotLab::To.run(objective, local_guards: true, write_guard: false)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Internally this calls `Guards.install(robot, run:, except: [Guards::WriteGuard])`.
|
|
38
|
+
|
|
39
|
+
### read-before-edit
|
|
40
|
+
|
|
41
|
+
**Problem:** models fire `edit` with an `old_text` they never actually saw —
|
|
42
|
+
guessing the file's contents — which either fails the exact-match (a wasted turn)
|
|
43
|
+
or, worse, matches the wrong span.
|
|
44
|
+
|
|
45
|
+
**Policy:** refuse `edit` on any file that wasn't `read` this run, directing the
|
|
46
|
+
model to read it first. A successful `read`, `edit`, or `write` marks the file as
|
|
47
|
+
known. The read-set is per-run state.
|
|
48
|
+
|
|
49
|
+
### checkpoint
|
|
50
|
+
|
|
51
|
+
**Problem:** a single botched edit can spoil otherwise-good work within an
|
|
52
|
+
iteration.
|
|
53
|
+
|
|
54
|
+
**Policy:** before the first `write`/`edit` of a file, snapshot its current bytes
|
|
55
|
+
(first-write-wins) to `runs/<run_id>/checkpoints/`. Files that didn't exist get an
|
|
56
|
+
`.absent` sentinel. This is finer-grained than the orchestrator's
|
|
57
|
+
`git reset --hard` rollback — it survives *within* an iteration. Best-effort:
|
|
58
|
+
checkpointing never raises into the tool path.
|
|
59
|
+
|
|
60
|
+
### quality-monitor
|
|
61
|
+
|
|
62
|
+
**Problem:** a small model can get stuck repeating the exact same tool call, which
|
|
63
|
+
on an unattended overnight run silently burns the entire token budget.
|
|
64
|
+
|
|
65
|
+
**Policy:** detect the same `name` + arguments called on consecutive turns. After
|
|
66
|
+
a couple of consecutive repeats it raises a `QualityError`, which the orchestrator
|
|
67
|
+
handles like any iteration error — roll back, record the reason in `notes.md`, and
|
|
68
|
+
move on. This is the load-bearing guard for long unattended runs.
|
|
69
|
+
|
|
70
|
+
!!! info "Empty-response handling is intentionally left out"
|
|
71
|
+
An earlier version also flagged "empty" responses, but RobotLab fires the
|
|
72
|
+
generation hook once per *run* (not per turn), so that check false-tripped on
|
|
73
|
+
legitimate tool-using turns. Empty / no-progress is handled instead by the
|
|
74
|
+
orchestrator's existing nudge → "did not submit" path, which recovers
|
|
75
|
+
gracefully rather than hard-erroring.
|
|
76
|
+
|
|
77
|
+
!!! info "A fifth guard, `GraderLock`, is not part of this set"
|
|
78
|
+
`Guards::GraderLock` (`guards/grader_lock.rb`) lives alongside these four but
|
|
79
|
+
is **not** gated by `--local-guards` — it locks an [eval's](../concepts/evals.md)
|
|
80
|
+
grading criteria (a spec, a floor script, `--protect-path` globs) from robot
|
|
81
|
+
edits, and is installed whenever such paths exist, regardless of model size.
|
|
82
|
+
A frontier model can reward-hack its own grading criteria just as easily as a
|
|
83
|
+
small model can mangle a file. See
|
|
84
|
+
[Evals: Guarding the grader](../concepts/evals.md#guarding-the-grader-graderlock).
|
|
85
|
+
|
|
86
|
+
## How they're registered
|
|
87
|
+
|
|
88
|
+
`Guards.install(robot, run:)` registers all four on the per-iteration robot. The
|
|
89
|
+
orchestrator calls it from `build_robot` only when `local_guards` is enabled:
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
robot = RobotLab.build(..., local_tools: iteration_tools(submit_tool))
|
|
93
|
+
RobotLab::To::Guards.install(robot, run: @run) if @config.local_guards?
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Per-run state
|
|
97
|
+
|
|
98
|
+
RobotLab builds a fresh hook context per tool call with no shared metadata across
|
|
99
|
+
calls, so cross-call guard state (the read-set, the checkpoint-set, the loop
|
|
100
|
+
tracker) can't live in the hook context. Because `robot_lab-to` runs each
|
|
101
|
+
iteration in its own thread, the guards keep that state in a thread-local
|
|
102
|
+
`RunStore` — the same idiom `robot_lab-audit` uses for run IDs. It's reset at the
|
|
103
|
+
start of each run.
|
|
104
|
+
|
|
105
|
+
## Relationship to the git rollback
|
|
106
|
+
|
|
107
|
+
The guards and the orchestrator's git rollback are complementary, at two
|
|
108
|
+
granularities:
|
|
109
|
+
|
|
110
|
+
| Layer | Granularity | Recovers |
|
|
111
|
+
|-------|-------------|----------|
|
|
112
|
+
| `checkpoint` guard | a single file, within an iteration | a botched individual edit |
|
|
113
|
+
| `git reset --hard` | the whole working tree, per iteration | a failed iteration |
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
Next: [Architecture](../reference/architecture.md).
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Local Models
|
|
2
|
+
|
|
3
|
+
`robot_lab-to` can drive a **local** model — running entirely offline against an
|
|
4
|
+
[Ollama](https://ollama.com) server — instead of a cloud API. This is the
|
|
5
|
+
"local assistant" mode: no API keys, no per-token cost, no data leaving your
|
|
6
|
+
machine.
|
|
7
|
+
|
|
8
|
+
Local models are smaller and less forgiving than frontier models, so this mode
|
|
9
|
+
adds two things:
|
|
10
|
+
|
|
11
|
+
- **Built-in file tools** (`read`, `write`, `edit`, `bash`) — a small, predictable
|
|
12
|
+
tool surface the model can use to do real work.
|
|
13
|
+
- **Guardrails** — `RobotLab::Hook` policies that catch the mistakes small models
|
|
14
|
+
reliably make, before they corrupt the working tree or burn the token budget.
|
|
15
|
+
|
|
16
|
+
Both are enabled together with `--local-guards`.
|
|
17
|
+
|
|
18
|
+
## The short version
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# 1. Serve a tool-capable model
|
|
22
|
+
ollama pull gpt-oss:20b
|
|
23
|
+
|
|
24
|
+
# 2. Run robot-to against it
|
|
25
|
+
robot-to "Add a greet(name) method in greeter.rb" \
|
|
26
|
+
--provider openai \
|
|
27
|
+
--model gpt-oss:20b \
|
|
28
|
+
--local-guards \
|
|
29
|
+
--no-stream \
|
|
30
|
+
--max-iterations 5
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The full setup — including the RubyLLM configuration that points `:openai` at
|
|
34
|
+
Ollama — is on the [Ollama Setup](ollama.md) page.
|
|
35
|
+
|
|
36
|
+
## Why these flags
|
|
37
|
+
|
|
38
|
+
Driving a local model end-to-end requires three non-obvious settings. Each exists
|
|
39
|
+
because of a concrete limitation discovered in testing:
|
|
40
|
+
|
|
41
|
+
| Flag / setting | Why |
|
|
42
|
+
|----------------|-----|
|
|
43
|
+
| `--provider openai` (+ Ollama base URL) | RubyLLM's native `:ollama` provider doesn't reliably get these models to emit tool calls. Routing through the `:openai` provider against Ollama's OpenAI-compatible `/v1` endpoint does. |
|
|
44
|
+
| `--no-stream` | Ollama suppresses tool calls when the response is streamed. With streaming off, tool calls come through. |
|
|
45
|
+
| `--local-guards` | Attaches the file tools the model needs to do work, plus guardrails that make those tools safe for a small model. |
|
|
46
|
+
|
|
47
|
+
See [Ollama Setup → Streaming and tool calls](ollama.md#streaming-and-tool-calls)
|
|
48
|
+
for the details.
|
|
49
|
+
|
|
50
|
+
## The design philosophy
|
|
51
|
+
|
|
52
|
+
This mode is inspired by the research behind
|
|
53
|
+
[`little-coder`](https://github.com/itayinbarr/little-coder): the same small model
|
|
54
|
+
scored **2.4× higher** on a coding benchmark through a guard-rich harness than it
|
|
55
|
+
did unscaffolded. With small local models, **the harness is the product** — a
|
|
56
|
+
frontier model forgives a sloppy tool loop; a 9–35B local model does not.
|
|
57
|
+
|
|
58
|
+
The guardrails are policies that intercept the model's tool calls:
|
|
59
|
+
|
|
60
|
+
- **`write-guard`** — refuses `write` on a file that already exists (small models
|
|
61
|
+
rewrite whole files and destroy content); redirects to `edit`.
|
|
62
|
+
- **`read-before-edit`** — refuses `edit` on a file the model hasn't read this run,
|
|
63
|
+
so `oldText` reflects the real contents.
|
|
64
|
+
- **`checkpoint`** — snapshots a file before the first Write/Edit, for fine-grained
|
|
65
|
+
recovery within an iteration.
|
|
66
|
+
- **`quality-monitor`** — detects a model spinning on the same tool call and stops
|
|
67
|
+
it before it burns the token budget.
|
|
68
|
+
|
|
69
|
+
See [Guardrails](guardrails.md) for how each works.
|
|
70
|
+
|
|
71
|
+
## Choosing a model
|
|
72
|
+
|
|
73
|
+
The model **must support tool calling**. In testing on an M2 Max:
|
|
74
|
+
|
|
75
|
+
| Model | Size | Tool calls? | Notes |
|
|
76
|
+
|-------|------|-------------|-------|
|
|
77
|
+
| `gpt-oss:20b` | 20B | ✅ reliable | Called tools *and* `submit_iteration_result`; completed full runs cleanly. **Recommended.** |
|
|
78
|
+
| `qwen3` | 8B | ✅ tools work | Created files via the write tool, but didn't reliably call `submit_iteration_result` (the final-report step). |
|
|
79
|
+
| `phi4-mini` | 3.8B | ❌ | Explains instead of calling tools. |
|
|
80
|
+
|
|
81
|
+
Prefer a larger, instruction-following model for the autonomous loop — it has to
|
|
82
|
+
both use tools *and* remember to submit its result every iteration.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
- [Ollama Setup](ollama.md) — install, serve, and configure.
|
|
87
|
+
- [Built-in Tools](tools.md) — what `read`/`write`/`edit`/`bash` do.
|
|
88
|
+
- [Guardrails](guardrails.md) — the small-model safety policies.
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Ollama Setup
|
|
2
|
+
|
|
3
|
+
This page covers running `robot_lab-to` against a local
|
|
4
|
+
[Ollama](https://ollama.com) server end-to-end.
|
|
5
|
+
|
|
6
|
+
## 1. Install and start Ollama
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
# macOS / Linux
|
|
10
|
+
curl -fsSL https://ollama.com/install.sh | sh
|
|
11
|
+
|
|
12
|
+
# Ollama runs a server on http://localhost:11434
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## 2. Pull a tool-capable model
|
|
16
|
+
|
|
17
|
+
The model **must** support tool calling. `gpt-oss:20b` is the recommended choice
|
|
18
|
+
(see [model selection](index.md#choosing-a-model)):
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
ollama pull gpt-oss:20b
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
You can confirm a model advertises tool support:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
curl -s http://localhost:11434/api/tags | jq '.models[] | {name, caps: .capabilities}'
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## 3. Point RubyLLM at Ollama
|
|
31
|
+
|
|
32
|
+
`robot_lab-to` reaches the model through RobotLab / RubyLLM. The robust path is
|
|
33
|
+
the **`:openai` provider** aimed at Ollama's OpenAI-compatible `/v1` endpoint.
|
|
34
|
+
Configure RubyLLM once, before launching the run:
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
require "ruby_llm"
|
|
38
|
+
require "robot_lab"
|
|
39
|
+
require "robot_lab/to"
|
|
40
|
+
|
|
41
|
+
RubyLLM.configure do |c|
|
|
42
|
+
c.openai_api_base = "http://localhost:11434/v1"
|
|
43
|
+
c.openai_api_key = "ollama" # ignored by Ollama, but RubyLLM requires a value
|
|
44
|
+
c.request_timeout = 600
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
RobotLab::To.run(
|
|
48
|
+
"Add a greet(name) method in greeter.rb",
|
|
49
|
+
provider: :openai,
|
|
50
|
+
model: "gpt-oss:20b",
|
|
51
|
+
local_guards: true,
|
|
52
|
+
stream: false,
|
|
53
|
+
max_iterations: 5
|
|
54
|
+
)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
!!! note "Why a wrapper script"
|
|
58
|
+
The Ollama base-URL configuration lives in RubyLLM's global config, so it's
|
|
59
|
+
set in a small Ruby launcher (as above) rather than via a `robot-to` CLI flag.
|
|
60
|
+
The CLI flags `--provider openai --model gpt-oss:20b --local-guards --no-stream`
|
|
61
|
+
cover the rest; only the base URL needs the wrapper. You can also set it in
|
|
62
|
+
your application's RubyLLM initializer.
|
|
63
|
+
|
|
64
|
+
## 4. Run
|
|
65
|
+
|
|
66
|
+
From the launcher above, or — once the base URL is configured in your environment
|
|
67
|
+
— from the CLI:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
robot-to "Add a greet(name) method in greeter.rb" \
|
|
71
|
+
--provider openai \
|
|
72
|
+
--model gpt-oss:20b \
|
|
73
|
+
--local-guards \
|
|
74
|
+
--no-stream \
|
|
75
|
+
--max-iterations 5
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Streaming and tool calls
|
|
79
|
+
|
|
80
|
+
**Local Ollama models must run non-streaming** (`--no-stream` / `stream: false`).
|
|
81
|
+
|
|
82
|
+
Ollama's OpenAI-compatible endpoint suppresses tool calls when the response is
|
|
83
|
+
streamed — the model "thinks" but never emits a tool call, so the robot can't do
|
|
84
|
+
any work. With streaming disabled, tool calls come through normally.
|
|
85
|
+
|
|
86
|
+
The trade-off: streaming is what enables per-chunk token accounting and
|
|
87
|
+
mid-iteration token-budget enforcement. With `--no-stream`, tokens are accounted
|
|
88
|
+
from each iteration's *result* instead, and `--max-tokens` is enforced at
|
|
89
|
+
iteration boundaries rather than mid-stream. For overnight local runs this is
|
|
90
|
+
almost always fine.
|
|
91
|
+
|
|
92
|
+
## Why not the native `:ollama` provider?
|
|
93
|
+
|
|
94
|
+
RubyLLM ships a native `:ollama` provider, but in testing it did **not** reliably
|
|
95
|
+
get tool-capable models (e.g. qwen3) to emit tool calls — the same model tool-calls
|
|
96
|
+
correctly through the `:openai` provider against Ollama's `/v1` endpoint. Until
|
|
97
|
+
that changes, prefer `provider: :openai` + `openai_api_base` for `robot_lab-to`.
|
|
98
|
+
|
|
99
|
+
## Registering models (if needed)
|
|
100
|
+
|
|
101
|
+
RubyLLM validates models against its registry. `robot_lab-to` sets
|
|
102
|
+
`assume_model_exists` when a provider is given, which is enough for a bare chat —
|
|
103
|
+
but attaching tools triggers a capability lookup that can raise
|
|
104
|
+
`ModelNotFoundError` for an unregistered model. If you hit that, refresh the
|
|
105
|
+
registry so your local models are known:
|
|
106
|
+
|
|
107
|
+
```ruby
|
|
108
|
+
RubyLLM.models.refresh! # discovers locally-served Ollama models
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Troubleshooting
|
|
112
|
+
|
|
113
|
+
| Symptom | Cause | Fix |
|
|
114
|
+
|---------|-------|-----|
|
|
115
|
+
| Model thinks but never calls a tool | Streaming is on | Add `--no-stream`. |
|
|
116
|
+
| `RobotLab::ModelNotFoundError` when tools attach | Model not in RubyLLM registry | `RubyLLM.models.refresh!`. |
|
|
117
|
+
| Every iteration "did not submit" | Model too small to follow the final-report step | Use a larger model (e.g. `gpt-oss:20b`). |
|
|
118
|
+
| No tool calls at all, model only explains | Model lacks tool support | Pick a model whose `capabilities` include `tools`. |
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
Next: [Built-in Tools](tools.md).
|