robot_lab-to 0.2.7 → 0.3.0
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 +4 -4
- data/.envrc +6 -0
- data/Archspec.rb +34 -0
- data/CHANGELOG.md +4 -0
- data/CLAUDE.md +3 -2
- data/README.md +9 -7
- data/Rakefile +6 -108
- data/docs/concepts/stop-conditions.md +3 -3
- data/docs/configuration/cli.md +4 -5
- data/docs/configuration/index.md +2 -3
- data/docs/configuration/settings.md +7 -7
- data/docs/getting-started/installation.md +5 -5
- data/docs/index.md +3 -3
- data/docs/local-models/index.md +14 -17
- data/docs/local-models/lm-studio.md +92 -0
- data/docs/reference/architecture.md +4 -4
- data/examples/.envrc +8 -0
- data/examples/01_basic_usage/README.md +14 -15
- data/examples/01_basic_usage/basic_usage.rb +20 -57
- data/examples/02_advanced_usage/README.md +10 -7
- data/examples/02_advanced_usage/advanced_usage.rb +23 -31
- data/examples/03_scored/scored_run.rb +19 -51
- data/examples/04_prose/README.md +15 -14
- data/examples/04_prose/prose_run.rb +22 -38
- data/examples/common.rb +111 -0
- data/lib/robot_lab/to/cli.rb +61 -34
- data/lib/robot_lab/to/commit_manager.rb +4 -0
- data/lib/robot_lab/to/config.rb +12 -0
- data/lib/robot_lab/to/decision_manager.rb +12 -1
- data/lib/robot_lab/to/exit_summary.rb +17 -16
- data/lib/robot_lab/to/guards/checkpoint.rb +6 -3
- data/lib/robot_lab/to/guards/quality_monitor.rb +4 -2
- data/lib/robot_lab/to/guards/run_store.rb +4 -2
- data/lib/robot_lab/to/notes_manager.rb +3 -0
- data/lib/robot_lab/to/orchestrator.rb +85 -34
- data/lib/robot_lab/to/prompt_builder.rb +2 -0
- data/lib/robot_lab/to/run.rb +8 -8
- data/lib/robot_lab/to/stop_conditions.rb +10 -6
- data/lib/robot_lab/to/tools/bash.rb +7 -2
- data/lib/robot_lab/to/tools/edit.rb +9 -4
- data/lib/robot_lab/to/tools/read.rb +3 -3
- data/lib/robot_lab/to/tools/request_decision.rb +14 -10
- data/lib/robot_lab/to/tools/submit_result.rb +12 -11
- data/lib/robot_lab/to/tools/write.rb +2 -2
- data/lib/robot_lab/to/version.rb +1 -1
- data/lib/robot_lab/to.rb +22 -0
- data/mkdocs.yml +1 -1
- metadata +10 -7
- data/docs/local-models/ollama.md +0 -122
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3dad27a958d30948be8ed4f93401f169e799c24d3a4d77fc4d205bf00c80849c
|
|
4
|
+
data.tar.gz: bdd675595bb524d3e4f2f1e24dce9df7b7ef2070195b5b60e3317780c4e51163
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 92a7c42b8b8586085a9894c15d2443e809b65147db939707b1d1c1ec2b83eeaec09752a165fa2193fb38d9f7fb10211fe33b8c0cce8a86b4a40a4987affb4be8
|
|
7
|
+
data.tar.gz: 06bd6bf4074ea7a35e969736de4ba5946db22a2313ee7551528d93630950fddfc2572acee1e3116bf923fe5b9aeffcbc198234106a7f32e9ebbd67d758887b57
|
data/.envrc
CHANGED
data/Archspec.rb
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# robot_lab-to is a plain Ruby gem (RobotLab::To), not a Rails app -- there is
|
|
2
|
+
# no app/ tree, no controllers/models/views, so the :rails preset doesn't
|
|
3
|
+
# apply. These are the actual boundaries documented in CLAUDE.md.
|
|
4
|
+
|
|
5
|
+
# Matched by exact constant, not a file glob: every file here reopens the
|
|
6
|
+
# bare `module RobotLab` namespace (shared with the external robot_lab gem's
|
|
7
|
+
# own top-level module), so an `in:` glob would misattribute any bare
|
|
8
|
+
# `RobotLab.xxx` call anywhere in the gem to this component.
|
|
9
|
+
component :orchestrator, constants: "RobotLab::To::Orchestrator"
|
|
10
|
+
component :tools, in: "lib/robot_lab/to/tools/**/*.rb"
|
|
11
|
+
component :guards, in: %w[lib/robot_lab/to/guards.rb lib/robot_lab/to/guards/**/*.rb]
|
|
12
|
+
component :evals, in: "lib/robot_lab/to/evals/**/*.rb"
|
|
13
|
+
component :commit_manager, in: "lib/robot_lab/to/commit_manager.rb"
|
|
14
|
+
|
|
15
|
+
# Tools are RobotLab::Tool subclasses the robot calls mid-turn (see
|
|
16
|
+
# lib/robot_lab/to/tools/file_tool.rb); Guards are RobotLab::Hook subclasses
|
|
17
|
+
# wired onto a robot by Orchestrator#build_robot; Evals are "orchestrator-owned
|
|
18
|
+
# scorers" per CLAUDE.md. All three are built and consumed by the orchestrator
|
|
19
|
+
# -- the dependency runs one way, never back into the loop that drives them.
|
|
20
|
+
tools.cannot_use :orchestrator,
|
|
21
|
+
because: "tools run inside an LLM turn, invoked by the robot -- " \
|
|
22
|
+
"they must not reach back into the loop that drives them"
|
|
23
|
+
guards.cannot_use :orchestrator,
|
|
24
|
+
because: "guards are wired onto a robot by Orchestrator#build_robot -- " \
|
|
25
|
+
"the dependency runs one way"
|
|
26
|
+
evals.cannot_use :orchestrator,
|
|
27
|
+
because: "evals are orchestrator-owned scorers (CLAUDE.md) -- " \
|
|
28
|
+
"Orchestrator calls Evals.build, not the reverse"
|
|
29
|
+
|
|
30
|
+
# CLAUDE.md: "CommitManager -- all git ops via Open3.capture3 (no shell
|
|
31
|
+
# interpolation)". Enforce the "no shell interpolation" half architecturally.
|
|
32
|
+
commit_manager.cannot_call :system, receiver: :none,
|
|
33
|
+
because: "git ops must go through Open3.capture3 with an argv array, " \
|
|
34
|
+
"never system()/backticks with an interpolated string"
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,10 @@ and [Conventional Commits](https://www.conventionalcommits.org/) (see `COMMITS.m
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.2.8] - 2026-09-09
|
|
12
|
+
|
|
13
|
+
Released in lockstep with `robot_lab` core v0.2.8: this gem now resolves the released core gem from RubyGems instead of the local sibling checkout (local-path development remains available via `BUNDLE_GEMFILE=Gemfile.local`). Also in this release: reek pass completed, archspec added to the development bundle, and shared LM Studio example configuration extracted for the examples.
|
|
14
|
+
|
|
11
15
|
### Added
|
|
12
16
|
|
|
13
17
|
- **Human-in-the-loop decision files.** When the robot hits a choice it must not
|
data/CLAUDE.md
CHANGED
|
@@ -11,7 +11,8 @@ autonomous loop toward a stated objective, committing one focused change per ite
|
|
|
11
11
|
bundle install
|
|
12
12
|
bundle exec rake test # all tests
|
|
13
13
|
bundle exec rake test_verbose # verbose
|
|
14
|
-
|
|
14
|
+
asgard quality # all *_check gates in parallel (tests + coverage,
|
|
15
|
+
# rubocop, flog, flay, reek, fasterer, typos, ...)
|
|
15
16
|
bin/console # IRB shell
|
|
16
17
|
```
|
|
17
18
|
|
|
@@ -71,4 +72,4 @@ All run state lives in `.robot_lab_to/runs/<run_id>/` (added to `.git/info/exclu
|
|
|
71
72
|
## Testing
|
|
72
73
|
|
|
73
74
|
Minitest. Git-dependent tests use `Dir.mktmpdir` with real `git init`.
|
|
74
|
-
Coverage gate: 95% line / 75% branch (`
|
|
75
|
+
Coverage gate: 95% line / 75% branch (`asgard quality` also runs rubocop + flog + flay + reek + more).
|
data/README.md
CHANGED
|
@@ -244,16 +244,18 @@ RobotLab::To.run(
|
|
|
244
244
|
## Local models
|
|
245
245
|
|
|
246
246
|
`robot_lab-to` can drive a local model running offline against
|
|
247
|
-
[
|
|
247
|
+
[LM Studio](https://lmstudio.ai) via the
|
|
248
|
+
[ruby_llm-providers-lms](https://github.com/madbomber/ruby_llm-providers-lms)
|
|
249
|
+
gem — no API keys, no per-token cost.
|
|
248
250
|
|
|
249
251
|
```bash
|
|
250
|
-
|
|
252
|
+
lms server start
|
|
253
|
+
lms get qwen/qwen3.8-27b
|
|
251
254
|
|
|
252
255
|
robot-to "Add a greet(name) method in greeter.rb" \
|
|
253
|
-
--provider
|
|
254
|
-
--model
|
|
256
|
+
--provider lms \
|
|
257
|
+
--model qwen/qwen3.8-27b \
|
|
255
258
|
--local-guards \
|
|
256
|
-
--no-stream \
|
|
257
259
|
--max-iterations 5
|
|
258
260
|
```
|
|
259
261
|
|
|
@@ -320,8 +322,8 @@ export ROBOT_LAB_TO_STREAM=false
|
|
|
320
322
|
|
|
321
323
|
API keys are read by the underlying provider (via RobotLab / RubyLLM), **not** by
|
|
322
324
|
`robot_lab-to` itself — e.g. `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`. Local
|
|
323
|
-
[
|
|
324
|
-
[Local Models guide](https://madbomber.github.io/robot_lab-to/local-models/
|
|
325
|
+
[LM Studio](https://lmstudio.ai) models need no key; see the
|
|
326
|
+
[Local Models guide](https://madbomber.github.io/robot_lab-to/local-models/lm-studio/).
|
|
325
327
|
|
|
326
328
|
---
|
|
327
329
|
|
data/Rakefile
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
# Quality gates (quality, rubocop_check, flog_check, flay_check, ...),
|
|
4
|
+
# documentation tasks (doc_builder, doc_server), and the gem lifecycle
|
|
5
|
+
# (build, install, release) live in asgard — see .loki and the shared
|
|
6
|
+
# dev/*.loki files it imports. This Rakefile keeps only the task asgard
|
|
7
|
+
# itself delegates to: the test suite.
|
|
8
|
+
|
|
4
9
|
require 'rake/testtask'
|
|
5
10
|
|
|
6
11
|
Rake::TestTask.new(:test) do |t|
|
|
@@ -23,110 +28,3 @@ desc 'Run a single test file'
|
|
|
23
28
|
task :test_file, [:file] do |_t, args|
|
|
24
29
|
ruby "test/#{args[:file]}"
|
|
25
30
|
end
|
|
26
|
-
|
|
27
|
-
desc 'Check code complexity with Flog (warn >=20, fail >=50)'
|
|
28
|
-
task :flog_check do
|
|
29
|
-
require 'flog'
|
|
30
|
-
|
|
31
|
-
method_warn = 20.0
|
|
32
|
-
method_fail = 50.0
|
|
33
|
-
|
|
34
|
-
flogger = Flog.new(all: true)
|
|
35
|
-
flogger.flog(*Dir.glob('lib/**/*.rb'))
|
|
36
|
-
|
|
37
|
-
warnings = []
|
|
38
|
-
failures = []
|
|
39
|
-
|
|
40
|
-
flogger.each_by_score do |method, score|
|
|
41
|
-
next if method.end_with?('#none')
|
|
42
|
-
|
|
43
|
-
if score > method_fail
|
|
44
|
-
failures << "#{format('%.1f', score)}: #{method}"
|
|
45
|
-
elsif score > method_warn
|
|
46
|
-
warnings << "#{format('%.1f', score)}: #{method}"
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
unless warnings.empty?
|
|
51
|
-
puts "\nFlog warnings (#{method_warn}–#{method_fail}) — target for future refactoring:"
|
|
52
|
-
warnings.each { |v| puts " #{v}" }
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
if failures.empty?
|
|
56
|
-
puts "\nFlog: no methods exceed the failure threshold (>=#{method_fail})"
|
|
57
|
-
else
|
|
58
|
-
puts "\nFlog failures (>=#{method_fail}) — must be refactored:"
|
|
59
|
-
failures.each { |v| puts " #{v}" }
|
|
60
|
-
abort "\nFlog quality gate failed: #{failures.size} method(s) exceed #{method_fail}"
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
desc 'Check for structural code duplication with Flay (mass >= 50)'
|
|
65
|
-
task :flay_check do
|
|
66
|
-
require 'flay'
|
|
67
|
-
|
|
68
|
-
mass_threshold = 50
|
|
69
|
-
|
|
70
|
-
flay = Flay.new({ mass: mass_threshold, diff: false, verbose: false, summary: false, timeout: 60 })
|
|
71
|
-
flay.process(*Dir.glob('lib/**/*.rb'))
|
|
72
|
-
flay.analyze
|
|
73
|
-
|
|
74
|
-
if flay.hashes.empty?
|
|
75
|
-
puts "\nFlay: no structural duplication detected (mass >= #{mass_threshold})"
|
|
76
|
-
else
|
|
77
|
-
puts "\nFlay found structural duplication (mass >= #{mass_threshold}):"
|
|
78
|
-
flay.report
|
|
79
|
-
abort "\nFlay quality gate failed: #{flay.hashes.length} pattern(s) detected"
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
desc 'Run all quality checks: tests (with coverage), RuboCop, Flog, and Flay'
|
|
84
|
-
task :quality do
|
|
85
|
-
gates = [
|
|
86
|
-
['Tests + Coverage', 'bundle exec rake test'],
|
|
87
|
-
['RuboCop', 'bundle exec rubocop'],
|
|
88
|
-
['Flog Complexity', 'bundle exec rake flog_check'],
|
|
89
|
-
['Flay Duplication', 'bundle exec rake flay_check']
|
|
90
|
-
]
|
|
91
|
-
|
|
92
|
-
results = gates.map do |label, command|
|
|
93
|
-
puts "\n#{'=' * 60}"
|
|
94
|
-
puts "Quality Gate: #{label}"
|
|
95
|
-
puts '=' * 60
|
|
96
|
-
[label, system(command) ? :pass : :fail]
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
green = ->(s) { "\e[32m#{s}\e[0m" }
|
|
100
|
-
red = ->(s) { "\e[31m#{s}\e[0m" }
|
|
101
|
-
width = results.map { |label, _| label.length }.max
|
|
102
|
-
|
|
103
|
-
puts "\n#{'=' * 60}"
|
|
104
|
-
puts 'Quality Gate Summary'
|
|
105
|
-
puts '=' * 60
|
|
106
|
-
results.each do |label, status|
|
|
107
|
-
badge = status == :pass ? green.call('PASS') : red.call('FAIL')
|
|
108
|
-
puts " [#{badge}] #{label.ljust(width)}"
|
|
109
|
-
end
|
|
110
|
-
puts '-' * 60
|
|
111
|
-
|
|
112
|
-
passed = results.count { |_, s| s == :pass }
|
|
113
|
-
failed = results.count { |_, s| s == :fail }
|
|
114
|
-
tally = "#{passed} passed, #{failed} failed"
|
|
115
|
-
puts " #{failed.zero? ? green.call(tally) : red.call(tally)}"
|
|
116
|
-
puts '=' * 60
|
|
117
|
-
|
|
118
|
-
abort "\n#{red.call('Quality gate failed.')}" unless failed.zero?
|
|
119
|
-
puts "\n#{green.call('All quality gates passed.')}"
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
namespace :docs do
|
|
123
|
-
desc 'Build MkDocs documentation'
|
|
124
|
-
task :build do
|
|
125
|
-
sh 'mkdocs build'
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
desc 'Serve MkDocs documentation locally on http://localhost:8000'
|
|
129
|
-
task :serve do
|
|
130
|
-
sh 'mkdocs serve'
|
|
131
|
-
end
|
|
132
|
-
end
|
|
@@ -37,9 +37,9 @@ limit.
|
|
|
37
37
|
|
|
38
38
|
- **Streaming runs** (the default) account tokens per chunk and can interrupt an
|
|
39
39
|
in-flight iteration the moment the budget is exhausted.
|
|
40
|
-
- **Non-streaming runs** (`--no-stream
|
|
41
|
-
|
|
42
|
-
[Local Models](../local-models/
|
|
40
|
+
- **Non-streaming runs** (`--no-stream`) account tokens from each iteration's
|
|
41
|
+
result and stop at the next iteration boundary. See
|
|
42
|
+
[Local Models](../local-models/index.md).
|
|
43
43
|
|
|
44
44
|
### Consecutive failures
|
|
45
45
|
|
data/docs/configuration/cli.md
CHANGED
|
@@ -43,7 +43,7 @@ echo "Add request logging middleware and tests" | robot-to
|
|
|
43
43
|
| `--commit-format` | `default`\|`conventional` | `default` | Commit message format. |
|
|
44
44
|
| `--run-dir` | `PATH` | `.robot_lab_to` | Directory for run state. |
|
|
45
45
|
| `--local-guards` | — | off | Add built-in file tools + small-model guardrails. |
|
|
46
|
-
| `--no-stream` | — | streaming on | Disable streaming (
|
|
46
|
+
| `--no-stream` | — | streaming on | Disable streaming (tokens are then accounted per iteration instead of per chunk). |
|
|
47
47
|
| `--debug` | — | off | Keep verbose provider logging enabled. |
|
|
48
48
|
| `--version` | — | — | Print version and exit. |
|
|
49
49
|
| `-h`, `--help` | — | — | Show help and exit. |
|
|
@@ -92,14 +92,13 @@ robot-to "Write an opinionated guide to the Viable Systems Model" \
|
|
|
92
92
|
--stop-on-plateau 3
|
|
93
93
|
```
|
|
94
94
|
|
|
95
|
-
A fully local run on
|
|
95
|
+
A fully local run on LM Studio (see [Local Models](../local-models/index.md)):
|
|
96
96
|
|
|
97
97
|
```bash
|
|
98
98
|
robot-to "Add a greet(name) method in greeter.rb" \
|
|
99
|
-
--provider
|
|
100
|
-
--model
|
|
99
|
+
--provider lms \
|
|
100
|
+
--model qwen/qwen3.8-27b \
|
|
101
101
|
--local-guards \
|
|
102
|
-
--no-stream \
|
|
103
102
|
--max-iterations 5
|
|
104
103
|
```
|
|
105
104
|
|
data/docs/configuration/index.md
CHANGED
|
@@ -43,10 +43,9 @@ Create `~/.config/robot_lab/to.yml` to set your own defaults — for example, to
|
|
|
43
43
|
always use a local model:
|
|
44
44
|
|
|
45
45
|
```yaml
|
|
46
|
-
provider:
|
|
47
|
-
model:
|
|
46
|
+
provider: lms # LM Studio via the ruby_llm-providers-lms gem
|
|
47
|
+
model: qwen/qwen3.8-27b
|
|
48
48
|
local_guards: true
|
|
49
|
-
stream: false
|
|
50
49
|
max_consecutive_failures: 4
|
|
51
50
|
```
|
|
52
51
|
|
|
@@ -13,8 +13,9 @@ CLI flag, or a `RobotLab::To.run` keyword argument (see
|
|
|
13
13
|
- **CLI:** `--provider NAME`
|
|
14
14
|
|
|
15
15
|
The LLM provider, passed through to RobotLab / RubyLLM. Use `anthropic`,
|
|
16
|
-
`openai`, etc. For **local
|
|
17
|
-
|
|
16
|
+
`openai`, etc. For **local LM Studio models**, require the
|
|
17
|
+
ruby_llm-providers-lms gem and set this to `lms` — see
|
|
18
|
+
[LM Studio Setup](../local-models/lm-studio.md).
|
|
18
19
|
|
|
19
20
|
### `model`
|
|
20
21
|
|
|
@@ -22,7 +23,7 @@ RubyLLM at Ollama's OpenAI-compatible endpoint — see [Ollama Setup](../local-m
|
|
|
22
23
|
- **CLI:** `--model MODEL`
|
|
23
24
|
|
|
24
25
|
The model identifier for the chosen provider (e.g. `claude-sonnet-4-6`,
|
|
25
|
-
`gpt-5.5`, `
|
|
26
|
+
`gpt-5.5`, `qwen/qwen3.8-27b`).
|
|
26
27
|
|
|
27
28
|
### `stream`
|
|
28
29
|
|
|
@@ -30,10 +31,9 @@ The model identifier for the chosen provider (e.g. `claude-sonnet-4-6`,
|
|
|
30
31
|
- **CLI:** `--no-stream` to disable
|
|
31
32
|
|
|
32
33
|
Whether to stream the model response. Streaming enables per-chunk token
|
|
33
|
-
accounting and mid-iteration token-budget enforcement.
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
[Streaming and tool calls](../local-models/ollama.md#streaming-and-tool-calls).
|
|
34
|
+
accounting and mid-iteration token-budget enforcement. With streaming off,
|
|
35
|
+
tokens are accounted from each iteration's result instead. See
|
|
36
|
+
[LM Studio Setup](../local-models/lm-studio.md).
|
|
37
37
|
|
|
38
38
|
## Loop control
|
|
39
39
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
| Ruby **>= 3.2** | Set in the gemspec `required_ruby_version`. |
|
|
8
8
|
| Git | A repository with at least one commit. The loop creates a branch and commits there. |
|
|
9
9
|
| `robot_lab` | The core framework `robot_lab-to` builds on. |
|
|
10
|
-
| An LLM provider | A cloud API key, **or** a local [
|
|
10
|
+
| An LLM provider | A cloud API key, **or** a local [LM Studio](https://lmstudio.ai) server (see [Local Models](../local-models/index.md)). |
|
|
11
11
|
|
|
12
12
|
## Install the gem
|
|
13
13
|
|
|
@@ -46,11 +46,11 @@ RobotLab / RubyLLM. Provide credentials the usual way for your provider:
|
|
|
46
46
|
robot-to "..." --provider openai --model gpt-5.5
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
=== "Local (
|
|
49
|
+
=== "Local (LM Studio)"
|
|
50
50
|
|
|
51
|
-
No API key required. See [
|
|
52
|
-
full configuration —
|
|
53
|
-
|
|
51
|
+
No API key required. See [LM Studio Setup](../local-models/lm-studio.md)
|
|
52
|
+
for the full configuration — require the ruby_llm-providers-lms gem, then
|
|
53
|
+
run with `--provider lms` and `--local-guards`.
|
|
54
54
|
|
|
55
55
|
The default provider/model is `openai` / `gpt-5.5` (see
|
|
56
56
|
[Settings Reference](../configuration/settings.md)).
|
data/docs/index.md
CHANGED
|
@@ -68,7 +68,7 @@ See [The Iteration Loop](concepts/iteration-loop.md) for the full lifecycle.
|
|
|
68
68
|
`robot-to --resume <run_id>` continues a stopped run and an external scheduler
|
|
69
69
|
can drive it one commit per tick.
|
|
70
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
|
|
71
|
+
small-model guardrails, so it can drive a local LM Studio model offline. See
|
|
72
72
|
[Local Models](local-models/index.md).
|
|
73
73
|
|
|
74
74
|
---
|
|
@@ -80,7 +80,7 @@ See [The Iteration Loop](concepts/iteration-loop.md) for the full lifecycle.
|
|
|
80
80
|
- :material-rocket-launch: **[Installation](getting-started/installation.md)** — install the gem and the `robot-to` CLI.
|
|
81
81
|
- :material-play: **[Quick Start](getting-started/quick-start.md)** — your first overnight run in five minutes.
|
|
82
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
|
|
83
|
+
- :material-laptop: **[Local Models](local-models/index.md)** — drive a local LM Studio model with guardrails.
|
|
84
84
|
- :material-sitemap: **[Architecture](reference/architecture.md)** — how the pieces fit together.
|
|
85
85
|
|
|
86
86
|
</div>
|
|
@@ -93,4 +93,4 @@ See [The Iteration Loop](concepts/iteration-loop.md) for the full lifecycle.
|
|
|
93
93
|
- A git repository with at least one commit (the loop branches and commits there)
|
|
94
94
|
- The [`robot_lab`](https://github.com/MadBomber/robot_lab) gem and an LLM provider
|
|
95
95
|
(a cloud key such as `ANTHROPIC_API_KEY` / `OPENAI_API_KEY`, **or** a local
|
|
96
|
-
[
|
|
96
|
+
[LM Studio](https://lmstudio.ai) server via ruby_llm-providers-lms)
|
data/docs/local-models/index.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Local Models
|
|
2
2
|
|
|
3
3
|
`robot_lab-to` can drive a **local** model — running entirely offline against an
|
|
4
|
-
[
|
|
4
|
+
[LM Studio](https://lmstudio.ai) server via the `:lms` provider — instead of a cloud API. This is the
|
|
5
5
|
"local assistant" mode: no API keys, no per-token cost, no data leaving your
|
|
6
6
|
machine.
|
|
7
7
|
|
|
@@ -19,33 +19,31 @@ Both are enabled together with `--local-guards`.
|
|
|
19
19
|
|
|
20
20
|
```bash
|
|
21
21
|
# 1. Serve a tool-capable model
|
|
22
|
-
|
|
22
|
+
lms server start
|
|
23
|
+
lms get qwen/qwen3.8-27b
|
|
23
24
|
|
|
24
25
|
# 2. Run robot-to against it
|
|
25
26
|
robot-to "Add a greet(name) method in greeter.rb" \
|
|
26
|
-
--provider
|
|
27
|
-
--model
|
|
27
|
+
--provider lms \
|
|
28
|
+
--model qwen/qwen3.8-27b \
|
|
28
29
|
--local-guards \
|
|
29
|
-
--no-stream \
|
|
30
30
|
--max-iterations 5
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
The full setup — including the
|
|
34
|
-
|
|
33
|
+
The full setup — including requiring the ruby_llm-providers-lms gem — is on
|
|
34
|
+
the [LM Studio Setup](lm-studio.md) page.
|
|
35
35
|
|
|
36
36
|
## Why these flags
|
|
37
37
|
|
|
38
|
-
Driving a local model end-to-end
|
|
39
|
-
|
|
38
|
+
Driving a local model end-to-end takes two settings. Each exists because of a
|
|
39
|
+
concrete limitation discovered in testing:
|
|
40
40
|
|
|
41
41
|
| Flag / setting | Why |
|
|
42
42
|
|----------------|-----|
|
|
43
|
-
| `--provider
|
|
44
|
-
| `--no-stream` | Ollama suppresses tool calls when the response is streamed. With streaming off, tool calls come through. |
|
|
43
|
+
| `--provider lms` | The ruby_llm-providers-lms gem's LM Studio provider — no API key, and local model ids are assumed to exist. Its default `:chat_completions` protocol supports client tools, structured output, and streaming. |
|
|
45
44
|
| `--local-guards` | Attaches the file tools the model needs to do work, plus guardrails that make those tools safe for a small model. |
|
|
46
45
|
|
|
47
|
-
See [
|
|
48
|
-
for the details.
|
|
46
|
+
See [LM Studio Setup](lm-studio.md) for the details.
|
|
49
47
|
|
|
50
48
|
## The design philosophy
|
|
51
49
|
|
|
@@ -74,15 +72,14 @@ The model **must support tool calling**. In testing on an M2 Max:
|
|
|
74
72
|
|
|
75
73
|
| Model | Size | Tool calls? | Notes |
|
|
76
74
|
|-------|------|-------------|-------|
|
|
77
|
-
| `
|
|
78
|
-
| `
|
|
79
|
-
| `phi4-mini` | 3.8B | ❌ | Explains instead of calling tools. |
|
|
75
|
+
| `qwen/qwen3.8-27b` | 27B | ✅ reliable | Honors `tool_choice` and structured output; the right pick for the autonomous loop. **Recommended.** |
|
|
76
|
+
| `openai/gpt-oss-20b` | 20B | ⚠️ partial | Accepts tool definitions but ignores `tool_choice: required`, and its structured output parses without meaning anything. Fine for simple chat, not for the loop. |
|
|
80
77
|
|
|
81
78
|
Prefer a larger, instruction-following model for the autonomous loop — it has to
|
|
82
79
|
both use tools *and* remember to submit its result every iteration.
|
|
83
80
|
|
|
84
81
|
---
|
|
85
82
|
|
|
86
|
-
- [
|
|
83
|
+
- [LM Studio Setup](lm-studio.md) — install, serve, and configure.
|
|
87
84
|
- [Built-in Tools](tools.md) — what `read`/`write`/`edit`/`bash` do.
|
|
88
85
|
- [Guardrails](guardrails.md) — the small-model safety policies.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# LM Studio Setup
|
|
2
|
+
|
|
3
|
+
This page covers running `robot_lab-to` against a local
|
|
4
|
+
[LM Studio](https://lmstudio.ai) server end-to-end, through the
|
|
5
|
+
[ruby_llm-providers-lms](https://github.com/madbomber/ruby_llm-providers-lms)
|
|
6
|
+
gem, which registers the `:lms` provider with RubyLLM.
|
|
7
|
+
|
|
8
|
+
## 1. Install and start LM Studio
|
|
9
|
+
|
|
10
|
+
Install LM Studio, then bootstrap its CLI and start the server:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
lms bootstrap # puts the `lms` CLI on your PATH
|
|
14
|
+
lms server start # serves on http://localhost:1234
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## 2. Download a tool-capable model
|
|
18
|
+
|
|
19
|
+
The model **must** support tool calling *and honor it*. `qwen/qwen3.8-27b` is
|
|
20
|
+
the recommended choice — it obeys `tool_choice` and returns meaningful
|
|
21
|
+
structured output, where `gpt-oss` models accept the request shape but ignore
|
|
22
|
+
it (see [model selection](index.md#choosing-a-model)):
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
lms get qwen/qwen3.8-27b
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## 3. Point RubyLLM at LM Studio
|
|
29
|
+
|
|
30
|
+
`robot_lab-to` reaches the model through RobotLab / RubyLLM. Require the
|
|
31
|
+
provider gem and, only if your server is not on the default endpoint,
|
|
32
|
+
configure the base URL:
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
require "ruby_llm"
|
|
36
|
+
require "ruby_llm/providers/lms"
|
|
37
|
+
require "robot_lab"
|
|
38
|
+
require "robot_lab/to"
|
|
39
|
+
|
|
40
|
+
RubyLLM.configure do |c|
|
|
41
|
+
c.lms_api_base = "http://localhost:1234/v1" # the default; override if needed
|
|
42
|
+
c.request_timeout = 600
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
RobotLab::To.run(
|
|
46
|
+
"Add a greet(name) method in greeter.rb",
|
|
47
|
+
provider: :lms,
|
|
48
|
+
model: "qwen/qwen3.8-27b",
|
|
49
|
+
local_guards: true,
|
|
50
|
+
max_iterations: 5
|
|
51
|
+
)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
No API key is needed — LM Studio's local server does not require one.
|
|
55
|
+
|
|
56
|
+
## 4. Run
|
|
57
|
+
|
|
58
|
+
From the launcher above, or from the CLI:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
robot-to "Add a greet(name) method in greeter.rb" \
|
|
62
|
+
--provider lms \
|
|
63
|
+
--model qwen/qwen3.8-27b \
|
|
64
|
+
--local-guards \
|
|
65
|
+
--max-iterations 5
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Because LM Studio is a local provider, RubyLLM assumes any model id you pass
|
|
69
|
+
exists — LM Studio just-in-time loads the model if it isn't loaded yet. No
|
|
70
|
+
registry refresh is needed.
|
|
71
|
+
|
|
72
|
+
## Protocols
|
|
73
|
+
|
|
74
|
+
LM Studio serves several API protocols on one port; the `:lms` provider
|
|
75
|
+
defaults to `:chat_completions`, which is the most capable one here — client
|
|
76
|
+
tools, structured output, streaming, and reasoning control all work. Stay on
|
|
77
|
+
the default for `robot_lab-to`. See the
|
|
78
|
+
[ruby_llm-providers-lms README](https://github.com/madbomber/ruby_llm-providers-lms)
|
|
79
|
+
for the full protocol matrix.
|
|
80
|
+
|
|
81
|
+
## Troubleshooting
|
|
82
|
+
|
|
83
|
+
| Symptom | Cause | Fix |
|
|
84
|
+
|---------|-------|-----|
|
|
85
|
+
| Connection error naming `lms server start` | Server not running | `lms server start`. |
|
|
86
|
+
| Tool calls accepted but ignored, or `{"name":"analysis","age":0}`-style junk from schemas | Model doesn't honor `tool_choice` / grammar output (e.g. `gpt-oss`) | Use `qwen/qwen3.8-27b`. |
|
|
87
|
+
| Every iteration "did not submit" | Model too small to follow the final-report step | Use a larger model (e.g. `qwen/qwen3.8-27b`). |
|
|
88
|
+
| No tool calls at all, model only explains | Model lacks tool support | Pick a tool-capable model. |
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
Next: [Built-in Tools](tools.md).
|
|
@@ -103,10 +103,10 @@ robot actually exposes the submit tool, guarding against that class of mistake.
|
|
|
103
103
|
|
|
104
104
|
### Streaming is optional
|
|
105
105
|
|
|
106
|
-
By default the robot streams, enabling per-chunk token accounting and
|
|
107
|
-
budget enforcement.
|
|
108
|
-
tokens
|
|
109
|
-
|
|
106
|
+
By default the robot streams, enabling per-chunk token accounting and
|
|
107
|
+
mid-stream budget enforcement. Non-streaming runs (`stream: false`) account
|
|
108
|
+
tokens from each iteration's result and enforce the budget at iteration
|
|
109
|
+
boundaries. See [LM Studio Setup](../local-models/lm-studio.md).
|
|
110
110
|
|
|
111
111
|
### Interruptible by design
|
|
112
112
|
|
data/examples/.envrc
ADDED
|
@@ -30,14 +30,13 @@ the `1..3999` range, round-trip correctness, case-insensitive parsing, and
|
|
|
30
30
|
|
|
31
31
|
## Prerequisites
|
|
32
32
|
|
|
33
|
-
Either
|
|
33
|
+
Either **local LM Studio** (default, no API key) or a **cloud** provider key.
|
|
34
34
|
|
|
35
|
-
=== "Local
|
|
35
|
+
=== "Local LM Studio (default)"
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
```
|
|
37
|
+
Nothing to start by hand -- `common.rb` (required by the script) starts the
|
|
38
|
+
LM Studio server and loads the model for you if they aren't already
|
|
39
|
+
running/loaded.
|
|
41
40
|
|
|
42
41
|
=== "Cloud provider"
|
|
43
42
|
|
|
@@ -50,7 +49,7 @@ Either a **local Ollama** server (default, no API key) or a **cloud** provider k
|
|
|
50
49
|
From the gem root:
|
|
51
50
|
|
|
52
51
|
```bash
|
|
53
|
-
# Local
|
|
52
|
+
# Local LM Studio (default)
|
|
54
53
|
bundle exec ruby examples/01_basic_usage/basic_usage.rb
|
|
55
54
|
|
|
56
55
|
# A cloud model instead
|
|
@@ -64,10 +63,10 @@ All optional, set via environment variables:
|
|
|
64
63
|
|
|
65
64
|
| Variable | Default | Description |
|
|
66
65
|
|----------|---------|-------------|
|
|
67
|
-
| `RLTO_LOCAL` | `true` | Use a local
|
|
68
|
-
| `RLTO_PROVIDER` | `
|
|
69
|
-
| `RLTO_MODEL` | `qwen3.
|
|
70
|
-
| `
|
|
66
|
+
| `RLTO_LOCAL` | `true` | Use a local LM Studio model. Set `false` for a cloud provider. |
|
|
67
|
+
| `RLTO_PROVIDER` | `lms` (local) | Provider label. `lms` resolves to RubyLLM's `:openai` adapter pointed at `LMS_BASE_URL` -- ruby_llm has no native `lms` adapter. |
|
|
68
|
+
| `RLTO_MODEL` | `qwen/qwen3.8-27b` (local) | Model id. Any tool-capable model works. |
|
|
69
|
+
| `LMS_BASE_URL` | `http://localhost:1234/v1` | LM Studio's OpenAI-compatible endpoint. |
|
|
71
70
|
|
|
72
71
|
## What you'll see
|
|
73
72
|
|
|
@@ -77,7 +76,7 @@ live, so the run never sits silent while the model works:
|
|
|
77
76
|
```
|
|
78
77
|
Cleaning leftover: .../examples/01_basic_usage/project
|
|
79
78
|
Project dir: examples/01_basic_usage/project
|
|
80
|
-
Provider/model:
|
|
79
|
+
Provider/model: lms/qwen/qwen3.8-27b (local LM Studio)
|
|
81
80
|
Objective: implement lib/roman_numeral.rb to pass the test suite
|
|
82
81
|
|
|
83
82
|
🤔 thinking…
|
|
@@ -157,10 +156,10 @@ The whole example is one call:
|
|
|
157
156
|
```ruby
|
|
158
157
|
RobotLab::To.run(
|
|
159
158
|
objective,
|
|
160
|
-
provider: :openai, #
|
|
161
|
-
model: "qwen3.
|
|
159
|
+
provider: :openai, # "lms" (RLTO_PROVIDER's default) resolves to :openai, routed at LM Studio
|
|
160
|
+
model: "qwen/qwen3.8-27b",
|
|
162
161
|
local_guards: true, # built-in file tools + guardrails
|
|
163
|
-
stream: false, #
|
|
162
|
+
stream: false, # local LM Studio tool calls run non-streaming
|
|
164
163
|
max_iterations: 6,
|
|
165
164
|
verify_command: "ruby -Ilib -Itest test/roman_numeral_test.rb",
|
|
166
165
|
stop_when: "test/roman_numeral_test.rb passes with 0 failures and 0 errors"
|