robot_lab-to 0.2.8 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 43c5ef1239f3c91dcf0b033466a2a346f3dafb385897d1e5696e2952a752a49d
4
- data.tar.gz: 29fb0fda8e3fbd13f079a3244c577c69e53fcb4e1c8dbdc5066b32c059bd7f67
3
+ metadata.gz: 3dad27a958d30948be8ed4f93401f169e799c24d3a4d77fc4d205bf00c80849c
4
+ data.tar.gz: bdd675595bb524d3e4f2f1e24dce9df7b7ef2070195b5b60e3317780c4e51163
5
5
  SHA512:
6
- metadata.gz: 7af7eca43dac57ae02b21b24b9ad8b7a813e473b5470372ee5048980948d0bb431e6b164d88f532394da9cd946d7ed75cafe2746efac41f79dc2cc1c0a1aaad6
7
- data.tar.gz: beb2270499d125d74f2a3333610c44b9cd030f8ee9142bf4b2b3e87b1f0e410b9f768ffdb83c7187e80e0d1d593ce77ebbf5f89cdee9b5d640432d788b429733
6
+ metadata.gz: 92a7c42b8b8586085a9894c15d2443e809b65147db939707b1d1c1ec2b83eeaec09752a165fa2193fb38d9f7fb10211fe33b8c0cce8a86b4a40a4987affb4be8
7
+ data.tar.gz: 06bd6bf4074ea7a35e969736de4ba5946db22a2313ee7551528d93630950fddfc2572acee1e3116bf923fe5b9aeffcbc198234106a7f32e9ebbd67d758887b57
data/.envrc CHANGED
@@ -4,4 +4,4 @@ source_up
4
4
 
5
5
  export RR=$(pwd)
6
6
 
7
- export BUNDLE_GEMFILE=Gemfile
7
+ # BUNDLE_GEMFILE is inherited from the project root .envrc (asgard dev|prod)
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
 
@@ -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
 
@@ -70,7 +70,7 @@ PROVIDER = ENV.fetch("RLTO_PROVIDER", LOCAL ? "lms" : "anthropic").to_sym
70
70
  MODEL = ENV.fetch("RLTO_MODEL", LOCAL ? "qwen/qwen3.8-27b" : "claude-sonnet-4-6")
71
71
 
72
72
  # ruby_llm has no native "lms" adapter. "lms" is this example's friendly label for
73
- # "a local LM Studio model"; setup (common.rb) resolves it to RubyLLM's :openai
73
+ # "a local LM Studio model"; setup (common.rb) resolves it to the :lms provider
74
74
  # adapter pointed at LM Studio, starting the server and loading MODEL as needed.
75
75
  # Everything passed to RobotLab uses the resolved provider; PROVIDER itself is
76
76
  # kept only for display.
@@ -25,7 +25,7 @@
25
25
  # the robot must earn each commit on correctness AND quality.
26
26
  #
27
27
  # Models (per your request): reasoning on OpenAI gpt-5.5, building on local
28
- # LM Studio qwen/qwen3.8-27b. Because both use RubyLLM's :openai provider but different
28
+ # LM Studio qwen/qwen3.8-27b. Because the phases use different providers and different
29
29
  # endpoints (api.openai.com vs LM Studio's /v1), and openai_api_base is global,
30
30
  # we toggle it between the (sequential) phases.
31
31
  #
@@ -98,9 +98,9 @@ def use_real_openai!
98
98
  RubyLLM.logger.level = Logger::ERROR # keep raw API traffic out of the feed
99
99
  end
100
100
 
101
- # Implementation phase routes the :openai provider at the local LM Studio endpoint
101
+ # Implementation phase uses the :lms provider against the local LM Studio endpoint
102
102
  # (or whatever BUILD_PROVIDER resolves to). common.rb's setup starts the LM Studio
103
- # server and loads BUILD_MODEL as needed; the "lms" label resolves to :openai.
103
+ # server and loads BUILD_MODEL as needed; the "lms" label resolves to the :lms provider.
104
104
  def use_build_provider!
105
105
  setup(provider: BUILD_PROVIDER, model: BUILD_MODEL)
106
106
  end
@@ -64,7 +64,7 @@ PROVIDER = ENV.fetch("RLTO_PROVIDER", LOCAL ? "lms" : "anthropic").to_sym
64
64
  MODEL = ENV.fetch("RLTO_MODEL", LOCAL ? "qwen/qwen3.8-27b" : "claude-sonnet-4-6")
65
65
 
66
66
  # ruby_llm has no native "lms" adapter. "lms" is this example's friendly label for
67
- # "a local LM Studio model"; setup (common.rb) resolves it to RubyLLM's :openai
67
+ # "a local LM Studio model"; setup (common.rb) resolves it to the :lms provider
68
68
  # adapter pointed at LM Studio, starting the server and loading MODEL as needed.
69
69
  # Everything passed to RobotLab uses the resolved provider; PROVIDER itself is
70
70
  # kept only for display.
@@ -65,7 +65,7 @@ JUDGE_MODEL = ENV.fetch("RLTO_JUDGE_MODEL", LOCAL ? "qwen/qwen3.8-27b" : "claude
65
65
  TOPIC = ENV.fetch("RLTO_TOPIC", "writing good Git commit messages")
66
66
 
67
67
  # ruby_llm has no native "lms" adapter. "lms" is this example's friendly label for
68
- # "a local LM Studio model"; setup (common.rb) resolves it to RubyLLM's :openai
68
+ # "a local LM Studio model"; setup (common.rb) resolves it to the :lms provider
69
69
  # adapter pointed at LM Studio, starting the server and loading each model as
70
70
  # needed -- once for the doer, again for the judge (a no-op if they're the same
71
71
  # model, or if it's already loaded). Everything passed to RobotLab uses the
data/examples/common.rb CHANGED
@@ -12,12 +12,12 @@
12
12
  # actual_provider = setup(provider: PROVIDER, model: MODEL)
13
13
  # RobotLab::To.run(objective, provider: actual_provider, model: MODEL, ...)
14
14
  #
15
- # ruby_llm has no native "lms" adapter -- "lms" is this example suite's own
16
- # label for "a local LM Studio model". When `provider` is "lms", setup:
17
- # 1. points RubyLLM's :openai adapter at LMS_BASE_URL
15
+ # "lms" is the ruby_llm-providers-lms gem's native LM Studio provider.
16
+ # When `provider` is "lms", setup:
17
+ # 1. points the :lms provider at LMS_BASE_URL
18
18
  # 2. starts the LM Studio server if it isn't already running
19
19
  # 3. loads `model` into LM Studio if it isn't already loaded
20
- # and returns :openai -- the RubyLLM-recognized provider to pass to RobotLab.
20
+ # and returns :lms -- the RubyLLM-recognized provider to pass to RobotLab.
21
21
  # Any other provider (a cloud one) passes straight through untouched; setup
22
22
  # does nothing else for it (the example configures its own API key as before).
23
23
  #
@@ -32,6 +32,7 @@ require "open3"
32
32
  require "uri"
33
33
 
34
34
  require "ruby_llm"
35
+ require "ruby_llm/providers/lms"
35
36
 
36
37
  LMS_BASE_URL = ENV.fetch("LMS_BASE_URL", "http://localhost:1234/v1")
37
38
 
@@ -44,7 +45,7 @@ def setup(provider: ENV.fetch("RLTO_PROVIDER", "lms"), model: ENV.fetch("RLTO_MO
44
45
  configure_lms!
45
46
  ensure_lms_server_running!
46
47
  ensure_lms_model_loaded!(model)
47
- :openai
48
+ :lms
48
49
  end
49
50
 
50
51
  def teardown
@@ -58,18 +59,14 @@ at_exit { teardown }
58
59
 
59
60
  # --- internals ---------------------------------------------------------------
60
61
 
61
- # Route RubyLLM's :openai provider at LM Studio's OpenAI-compatible endpoint and
62
- # refresh the registry so tool attachment works.
62
+ # Point the :lms provider at the LM Studio endpoint. Local models are assumed
63
+ # to exist, so no registry refresh is needed.
63
64
  def configure_lms!
64
65
  RubyLLM.configure do |c|
65
- c.openai_api_base = LMS_BASE_URL
66
- c.openai_api_key = "lm-studio" # ignored by LM Studio, but RubyLLM wants a value
66
+ c.lms_api_base = LMS_BASE_URL
67
67
  c.request_timeout = 600
68
68
  end
69
69
  RubyLLM.logger.level = Logger::ERROR
70
- RubyLLM.models.refresh!
71
- rescue StandardError => e
72
- warn "warning: could not refresh LM Studio models (#{e.class}: #{e.message})"
73
70
  end
74
71
 
75
72
  def lms_server_running?
@@ -597,10 +597,14 @@ module RobotLab
597
597
  tools + [Tools::Read.new, Tools::Write.new, Tools::Edit.new, Tools::Bash.new]
598
598
  end
599
599
 
600
+ # :reek:TooManyStatements -- the streaming callback accumulates usage and enforces the stop in one place.
600
601
  def token_tracker
601
602
  lambda do |chunk|
602
- @run.input_tokens += chunk.input_tokens.to_i
603
- @run.output_tokens += chunk.output_tokens.to_i
603
+ # ruby_llm 2.0 nests counts under chunk.tokens (usually only on the
604
+ # final chunk of a stream).
605
+ tokens = chunk.respond_to?(:tokens) ? chunk.tokens : nil
606
+ @run.input_tokens += tokens&.input.to_i
607
+ @run.output_tokens += tokens&.output.to_i
604
608
 
605
609
  return unless @stop_conditions.token_limit_exceeded?
606
610
 
@@ -17,8 +17,8 @@ module RobotLab
17
17
  output and exit status. Use for building, testing, listing, and git.
18
18
  DESC
19
19
 
20
- param :command, type: "string", desc: "The shell command to run"
21
- param :timeout, type: "integer", desc: "Seconds before the command is killed", required: false
20
+ parameter :command, type: "string", description: "The shell command to run"
21
+ parameter :timeout, type: "integer", description: "Seconds before the command is killed", required: false
22
22
 
23
23
  def initialize(robot: nil, timeout: DEFAULT_TIMEOUT)
24
24
  super(robot: robot)
@@ -14,10 +14,10 @@ module RobotLab
14
14
  replace_all is true. Read the file first to get the exact text.
15
15
  DESC
16
16
 
17
- param :path, type: "string", desc: "Path of the file to edit"
18
- param :old_text, type: "string", desc: "Exact text to replace"
19
- param :new_text, type: "string", desc: "Replacement text"
20
- param :replace_all, type: "boolean", desc: "Replace every occurrence", required: false
17
+ parameter :path, type: "string", description: "Path of the file to edit"
18
+ parameter :old_text, type: "string", description: "Exact text to replace"
19
+ parameter :new_text, type: "string", description: "Replacement text"
20
+ parameter :replace_all, type: "boolean", description: "Replace every occurrence", required: false
21
21
 
22
22
  # :reek:BooleanParameter -- replace_all is a tool param the LLM sets;
23
23
  # the `param` declarations above fix this signature.
@@ -14,9 +14,9 @@ module RobotLab
14
14
  start line) and limit (max lines). Always read a file before editing it.
15
15
  DESC
16
16
 
17
- param :path, type: "string", desc: "Path to the file to read"
18
- param :offset, type: "integer", desc: "1-based first line to return", required: false
19
- param :limit, type: "integer", desc: "Maximum number of lines to return", required: false
17
+ parameter :path, type: "string", description: "Path to the file to read"
18
+ parameter :offset, type: "integer", description: "1-based first line to return", required: false
19
+ parameter :limit, type: "integer", description: "Maximum number of lines to return", required: false
20
20
 
21
21
  def execute(path:, offset: nil, limit: nil, **)
22
22
  resolved = File.expand_path(path, Dir.pwd)
@@ -25,20 +25,20 @@ module RobotLab
25
25
  for routine engineering choices you are equipped to make yourself.
26
26
  DESC
27
27
 
28
- param :question, type: "string",
29
- desc: "The decision that needs a human answer, phrased as a question"
28
+ parameter :question, type: "string",
29
+ description: "The decision that needs a human answer, phrased as a question"
30
30
 
31
- param :situation, type: "string",
32
- desc: "Why this needs a human and what is at stake", required: false
31
+ parameter :situation, type: "string",
32
+ description: "Why this needs a human and what is at stake", required: false
33
33
 
34
- param :options, type: "array",
35
- desc: "The distinct options you see (strings)", required: false
34
+ parameter :options, type: "array",
35
+ description: "The distinct options you see (strings)", required: false
36
36
 
37
- param :recommendation, type: "string",
38
- desc: "Your recommended option and the reasoning behind it", required: false
37
+ parameter :recommendation, type: "string",
38
+ description: "Your recommended option and the reasoning behind it", required: false
39
39
 
40
- param :blocking, type: "boolean",
41
- desc: "true if work cannot correctly proceed until this is answered", required: false
40
+ parameter :blocking, type: "boolean",
41
+ description: "true if work cannot correctly proceed until this is answered", required: false
42
42
 
43
43
  def captured_requests
44
44
  @captured_requests ||= []
@@ -13,23 +13,23 @@ module RobotLab
13
13
  FINAL action before finishing. Do not call it until your work is complete.
14
14
  DESC
15
15
 
16
- param :success, type: "boolean",
17
- desc: "true if you made meaningful progress toward the objective; " \
18
- "false if you made no meaningful changes AND have no new learnings"
16
+ parameter :success, type: "boolean",
17
+ description: "true if you made meaningful progress toward the objective; " \
18
+ "false if you made no meaningful changes AND have no new learnings"
19
19
 
20
- param :summary, type: "string",
21
- desc: "Brief one-sentence description of what you accomplished or why you stopped"
20
+ parameter :summary, type: "string",
21
+ description: "Brief one-sentence description of what you accomplished or why you stopped"
22
22
 
23
- param :key_changes, type: "array",
24
- desc: "List of files or changes made this iteration (empty if none)",
23
+ parameter :key_changes, type: "array",
24
+ description: "List of files or changes made this iteration (empty if none)",
25
25
  required: false
26
26
 
27
- param :key_learnings, type: "array",
28
- desc: "Insights worth remembering for future iterations (empty if none)",
27
+ parameter :key_learnings, type: "array",
28
+ description: "Insights worth remembering for future iterations (empty if none)",
29
29
  required: false
30
30
 
31
- param :should_fully_stop, type: "boolean",
32
- desc: "Set to true only when instructed by a stop condition",
31
+ parameter :should_fully_stop, type: "boolean",
32
+ description: "Set to true only when instructed by a stop condition",
33
33
  required: false
34
34
 
35
35
  attr_reader :captured_result
@@ -15,8 +15,8 @@ module RobotLab
15
15
  do not exist yet; to change an existing file, use Edit.
16
16
  DESC
17
17
 
18
- param :path, type: "string", desc: "Path of the file to create"
19
- param :content, type: "string", desc: "Full text content to write"
18
+ parameter :path, type: "string", description: "Path of the file to create"
19
+ parameter :content, type: "string", description: "Full text content to write"
20
20
 
21
21
  def execute(path:, content:, **)
22
22
  resolved = File.expand_path(path, Dir.pwd)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RobotLab
4
4
  module To
5
- VERSION = "0.2.8"
5
+ VERSION = "0.3.0"
6
6
  end
7
7
  end
data/lib/robot_lab/to.rb CHANGED
@@ -44,6 +44,7 @@ module RobotLab
44
44
  # @return [void]
45
45
  def run(objective, **)
46
46
  config = Config.new(**)
47
+ ensure_provider_loaded(config.provider)
47
48
  suppress_llm_logging unless config.debug?
48
49
  Orchestrator.new(objective, config).run
49
50
  end
@@ -52,6 +53,7 @@ module RobotLab
52
53
  # objective and prior state are loaded from the run's run.json.
53
54
  def resume(run_id, **)
54
55
  config = Config.new(**)
56
+ ensure_provider_loaded(config.provider)
55
57
  suppress_llm_logging unless config.debug?
56
58
  Orchestrator.new(nil, config, resume_run_id: run_id).run
57
59
  end
@@ -66,6 +68,26 @@ module RobotLab
66
68
 
67
69
  private
68
70
 
71
+ # Provider gems register themselves on require by convention
72
+ # (ruby_llm-providers-<name> defines ruby_llm/providers/<name>). When the
73
+ # configured provider isn't registered yet — e.g. `robot-to --provider
74
+ # lms` from the CLI — try that conventional require so the user doesn't
75
+ # need a wrapper script. A miss falls through to RubyLLM's own "Unknown
76
+ # provider" error at request time.
77
+ def ensure_provider_loaded(provider)
78
+ return unless provider
79
+ return unless defined?(RubyLLM::Provider)
80
+
81
+ name = provider.to_s
82
+ return if RubyLLM::Provider.resolve(name)
83
+
84
+ begin
85
+ require "ruby_llm/providers/#{name}"
86
+ rescue LoadError
87
+ # Not a provider-gem provider; RubyLLM will report it if truly unknown.
88
+ end
89
+ end
90
+
69
91
  def suppress_llm_logging
70
92
  require "logger"
71
93
  null = Logger.new(File::NULL)
data/mkdocs.yml CHANGED
@@ -136,7 +136,7 @@ nav:
136
136
  - CLI Reference: configuration/cli.md
137
137
  - Local Models:
138
138
  - Overview: local-models/index.md
139
- - Ollama Setup: local-models/ollama.md
139
+ - LM Studio Setup: local-models/lm-studio.md
140
140
  - Built-in Tools: local-models/tools.md
141
141
  - Guardrails: local-models/guardrails.md
142
142
  - Reference:
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: robot_lab-to
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.8
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dewayne VanHoozer
@@ -13,16 +13,16 @@ dependencies:
13
13
  name: robot_lab
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
- - - ">="
16
+ - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: '0.1'
18
+ version: 0.3.0
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
- - - ">="
23
+ - - "~>"
24
24
  - !ruby/object:Gem::Version
25
- version: '0.1'
25
+ version: 0.3.0
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: myway_config
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -83,7 +83,7 @@ files:
83
83
  - docs/index.md
84
84
  - docs/local-models/guardrails.md
85
85
  - docs/local-models/index.md
86
- - docs/local-models/ollama.md
86
+ - docs/local-models/lm-studio.md
87
87
  - docs/local-models/tools.md
88
88
  - docs/reference/architecture.md
89
89
  - examples/.envrc
@@ -172,7 +172,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
172
172
  - !ruby/object:Gem::Version
173
173
  version: '0'
174
174
  requirements: []
175
- rubygems_version: 4.0.20
175
+ rubygems_version: 4.0.21
176
176
  specification_version: 4
177
177
  summary: Autonomous overnight agent loop for RobotLab — run robots while you sleep.
178
178
  test_files: []
@@ -1,122 +0,0 @@
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).