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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/.envrc +6 -0
  3. data/Archspec.rb +34 -0
  4. data/CHANGELOG.md +4 -0
  5. data/CLAUDE.md +3 -2
  6. data/README.md +9 -7
  7. data/Rakefile +6 -108
  8. data/docs/concepts/stop-conditions.md +3 -3
  9. data/docs/configuration/cli.md +4 -5
  10. data/docs/configuration/index.md +2 -3
  11. data/docs/configuration/settings.md +7 -7
  12. data/docs/getting-started/installation.md +5 -5
  13. data/docs/index.md +3 -3
  14. data/docs/local-models/index.md +14 -17
  15. data/docs/local-models/lm-studio.md +92 -0
  16. data/docs/reference/architecture.md +4 -4
  17. data/examples/.envrc +8 -0
  18. data/examples/01_basic_usage/README.md +14 -15
  19. data/examples/01_basic_usage/basic_usage.rb +20 -57
  20. data/examples/02_advanced_usage/README.md +10 -7
  21. data/examples/02_advanced_usage/advanced_usage.rb +23 -31
  22. data/examples/03_scored/scored_run.rb +19 -51
  23. data/examples/04_prose/README.md +15 -14
  24. data/examples/04_prose/prose_run.rb +22 -38
  25. data/examples/common.rb +111 -0
  26. data/lib/robot_lab/to/cli.rb +61 -34
  27. data/lib/robot_lab/to/commit_manager.rb +4 -0
  28. data/lib/robot_lab/to/config.rb +12 -0
  29. data/lib/robot_lab/to/decision_manager.rb +12 -1
  30. data/lib/robot_lab/to/exit_summary.rb +17 -16
  31. data/lib/robot_lab/to/guards/checkpoint.rb +6 -3
  32. data/lib/robot_lab/to/guards/quality_monitor.rb +4 -2
  33. data/lib/robot_lab/to/guards/run_store.rb +4 -2
  34. data/lib/robot_lab/to/notes_manager.rb +3 -0
  35. data/lib/robot_lab/to/orchestrator.rb +85 -34
  36. data/lib/robot_lab/to/prompt_builder.rb +2 -0
  37. data/lib/robot_lab/to/run.rb +8 -8
  38. data/lib/robot_lab/to/stop_conditions.rb +10 -6
  39. data/lib/robot_lab/to/tools/bash.rb +7 -2
  40. data/lib/robot_lab/to/tools/edit.rb +9 -4
  41. data/lib/robot_lab/to/tools/read.rb +3 -3
  42. data/lib/robot_lab/to/tools/request_decision.rb +14 -10
  43. data/lib/robot_lab/to/tools/submit_result.rb +12 -11
  44. data/lib/robot_lab/to/tools/write.rb +2 -2
  45. data/lib/robot_lab/to/version.rb +1 -1
  46. data/lib/robot_lab/to.rb +22 -0
  47. data/mkdocs.yml +1 -1
  48. metadata +10 -7
  49. data/docs/local-models/ollama.md +0 -122
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a600189accdf098fb2cea44bec912fbc3399b89d48b7775d1237a78fd5bac7fd
4
- data.tar.gz: 8729282eb38252b3e2227b02cc9b48dc76b04005572ede0d24fccdbab1b47940
3
+ metadata.gz: 3dad27a958d30948be8ed4f93401f169e799c24d3a4d77fc4d205bf00c80849c
4
+ data.tar.gz: bdd675595bb524d3e4f2f1e24dce9df7b7ef2070195b5b60e3317780c4e51163
5
5
  SHA512:
6
- metadata.gz: 58a538bc23b7a68822dae324d6b179f0eba4ea4f8b79bfb489ea6cfcaedd59e82c592197ca67c11873404d52c78bae8a74f26b53b05e061c5753d43ad15c3f68
7
- data.tar.gz: bc1e417731a76c43f5671ff3fcfaec038a59884d9ac62a63c1cb2bbb53c8fade6f051560da30db2d1c517645cc2f448100b1e551fd458ea040bf738447edd145
6
+ metadata.gz: 92a7c42b8b8586085a9894c15d2443e809b65147db939707b1d1c1ec2b83eeaec09752a165fa2193fb38d9f7fb10211fe33b8c0cce8a86b4a40a4987affb4be8
7
+ data.tar.gz: 06bd6bf4074ea7a35e969736de4ba5946db22a2313ee7551528d93630950fddfc2572acee1e3116bf923fe5b9aeffcbc198234106a7f32e9ebbd67d758887b57
data/.envrc CHANGED
@@ -1 +1,7 @@
1
+ # robot_lab_project/robot_lab-to/.envrc
2
+
3
+ source_up
4
+
1
5
  export RR=$(pwd)
6
+
7
+ # BUNDLE_GEMFILE is inherited from the project root .envrc (asgard dev|prod)
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
- bundle exec rake quality # tests + rubocop + flog
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 (`rake quality` also runs rubocop + flog + flay).
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
- [Ollama](https://ollama.com) no API keys, no per-token cost.
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
- ollama pull gpt-oss:20b
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 openai \
254
- --model gpt-oss:20b \
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
- [Ollama](https://ollama.com) models need no key; see the
324
- [Local Models guide](https://madbomber.github.io/robot_lab-to/local-models/ollama/).
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
- require 'bundler/gem_tasks'
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`, used for local models) account tokens
41
- from each iteration's result and stop at the next iteration boundary. See
42
- [Local Models](../local-models/ollama.md#streaming-and-tool-calls).
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
 
@@ -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 (required for local Ollama tool calls). |
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 Ollama (see [Local Models](../local-models/index.md)):
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 openai \
100
- --model gpt-oss:20b \
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
 
@@ -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: openai # routes to Ollama's OpenAI-compatible endpoint
47
- model: gpt-oss:20b
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 Ollama models**, set this to `openai` and point
17
- RubyLLM at Ollama's OpenAI-compatible endpoint — see [Ollama Setup](../local-models/ollama.md).
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`, `gpt-oss:20b`).
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. **Local Ollama models must
34
- run with `--no-stream`** Ollama suppresses tool calls when streaming. With
35
- streaming off, tokens are accounted from each iteration's result instead. See
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 [Ollama](https://ollama.com) server (see [Local Models](../local-models/index.md)). |
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 (Ollama)"
49
+ === "Local (LM Studio)"
50
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`.
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 Ollama model offline. See
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 Ollama model with guardrails.
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
- [Ollama](https://ollama.com) server)
96
+ [LM Studio](https://lmstudio.ai) server via ruby_llm-providers-lms)
@@ -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
- [Ollama](https://ollama.com) server — instead of a cloud API. This is the
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
- ollama pull gpt-oss:20b
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 openai \
27
- --model gpt-oss:20b \
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 RubyLLM configuration that points `:openai` at
34
- Ollama — is on the [Ollama Setup](ollama.md) page.
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 requires three non-obvious settings. Each exists
39
- because of a concrete limitation discovered in testing:
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 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. |
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 [Ollama Setup → Streaming and tool calls](ollama.md#streaming-and-tool-calls)
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
- | `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. |
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
- - [Ollama Setup](ollama.md) — install, serve, and configure.
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 mid-stream
107
- budget enforcement. Local Ollama models run non-streaming (`stream: false`), so
108
- tokens are accounted from each iteration's result and the budget is enforced at
109
- iteration boundaries. See [Ollama Setup](../local-models/ollama.md#streaming-and-tool-calls).
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
@@ -0,0 +1,8 @@
1
+ # robot_lab-to/examples/.envrc
2
+
3
+ source_up
4
+
5
+ export LMS_BASE_URL=http://localhost:1234/v1
6
+
7
+ export RLTO_PROVIDER=lms
8
+ export RLTO_MODEL=qwen/qwen3.8-27b
@@ -30,14 +30,13 @@ the `1..3999` range, round-trip correctness, case-insensitive parsing, and
30
30
 
31
31
  ## Prerequisites
32
32
 
33
- Either a **local Ollama** server (default, no API key) or a **cloud** provider key.
33
+ Either **local LM Studio** (default, no API key) or a **cloud** provider key.
34
34
 
35
- === "Local Ollama (default)"
35
+ === "Local LM Studio (default)"
36
36
 
37
- ```bash
38
- ollama serve & # if not already running
39
- ollama pull qwen3.6:latest # a tool-capable model
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 Ollama (default)
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 Ollama model. Set `false` for a cloud provider. |
68
- | `RLTO_PROVIDER` | `openai` (local) | LLM provider passed to `RobotLab::To.run`. |
69
- | `RLTO_MODEL` | `qwen3.6:latest` (local) | Model id. Any tool-capable model works. |
70
- | `OLLAMA_BASE` | `http://localhost:11434/v1` | Ollama's OpenAI-compatible endpoint. |
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: openai/qwen3.6:latest (local Ollama)
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, # routes to Ollama for local runs
161
- model: "qwen3.6:latest",
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, # Ollama tool calls need non-streaming
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"