robot_lab-to 0.2.7 → 0.2.8
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/Rakefile +6 -108
- 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 +114 -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 +79 -32
- 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 +5 -0
- data/lib/robot_lab/to/tools/edit.rb +5 -0
- data/lib/robot_lab/to/tools/request_decision.rb +4 -0
- data/lib/robot_lab/to/tools/submit_result.rb +1 -0
- data/lib/robot_lab/to/version.rb +1 -1
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 43c5ef1239f3c91dcf0b033466a2a346f3dafb385897d1e5696e2952a752a49d
|
|
4
|
+
data.tar.gz: 29fb0fda8e3fbd13f079a3244c577c69e53fcb4e1c8dbdc5066b32c059bd7f67
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7af7eca43dac57ae02b21b24b9ad8b7a813e473b5470372ee5048980948d0bb431e6b164d88f532394da9cd946d7ed75cafe2746efac41f79dc2cc1c0a1aaad6
|
|
7
|
+
data.tar.gz: beb2270499d125d74f2a3333610c44b9cd030f8ee9142bf4b2b3e87b1f0e410b9f768ffdb83c7187e80e0d1d593ce77ebbf5f89cdee9b5d640432d788b429733
|
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/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
|
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"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
#
|
|
4
4
|
# Basic Usage would generally be as a CLI tool; however, its a library
|
|
5
|
-
# so that means you can build it
|
|
5
|
+
# so that means you can build it into an application program.
|
|
6
6
|
#
|
|
7
7
|
# ===========================================================================
|
|
8
8
|
# 01_basic_usage — drive robot_lab-to programmatically
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
# ---------------------------------------------------------------------------
|
|
33
33
|
# Run it
|
|
34
34
|
# ---------------------------------------------------------------------------
|
|
35
|
-
# # Local
|
|
36
|
-
#
|
|
35
|
+
# # Local LM Studio (default — no API key needed; common.rb starts the server
|
|
36
|
+
# # and loads the model for you if they aren't already running/loaded):
|
|
37
37
|
# bundle exec ruby examples/01_basic_usage/basic_usage.rb
|
|
38
38
|
#
|
|
39
39
|
# # A cloud model instead:
|
|
@@ -41,17 +41,15 @@
|
|
|
41
41
|
# ANTHROPIC_API_KEY=sk-... \
|
|
42
42
|
# bundle exec ruby examples/01_basic_usage/basic_usage.rb
|
|
43
43
|
#
|
|
44
|
-
# Configuration (all optional, via environment):
|
|
45
|
-
# RLTO_LOCAL true|false use a local
|
|
46
|
-
# RLTO_PROVIDER name LLM provider (default:
|
|
47
|
-
# RLTO_MODEL id model id (default:
|
|
48
|
-
#
|
|
44
|
+
# Configuration (all optional, via environment; examples/.envrc sets these for you):
|
|
45
|
+
# RLTO_LOCAL true|false use a local LM Studio model (default true)
|
|
46
|
+
# RLTO_PROVIDER name LLM provider label (default: lms for local)
|
|
47
|
+
# RLTO_MODEL id model id (default: qwen/qwen3.8-27b for local)
|
|
48
|
+
# LMS_BASE_URL url LM Studio OpenAI-compatible endpoint (default localhost:1234/v1)
|
|
49
49
|
# ===========================================================================
|
|
50
50
|
|
|
51
51
|
require "fileutils"
|
|
52
|
-
require "logger"
|
|
53
52
|
require "open3"
|
|
54
|
-
require "net/http"
|
|
55
53
|
|
|
56
54
|
# Make the example runnable straight from the repo during development, with or
|
|
57
55
|
# without `bundle exec`. (When the gem is installed normally, these paths simply
|
|
@@ -61,52 +59,22 @@ require "net/http"
|
|
|
61
59
|
File.expand_path("../../../robot_lab/lib", __dir__) # sibling robot_lab/lib
|
|
62
60
|
].each { |p| $LOAD_PATH.unshift(p) if Dir.exist?(p) }
|
|
63
61
|
|
|
64
|
-
require "ruby_llm"
|
|
65
62
|
require "robot_lab"
|
|
66
63
|
require "robot_lab/to"
|
|
64
|
+
require_relative "../common"
|
|
67
65
|
|
|
68
66
|
# --- configuration ---------------------------------------------------------
|
|
69
67
|
|
|
70
68
|
LOCAL = ENV.fetch("RLTO_LOCAL", "true") == "true"
|
|
71
|
-
PROVIDER = ENV.fetch("RLTO_PROVIDER", LOCAL ? "
|
|
72
|
-
MODEL = ENV.fetch("RLTO_MODEL", LOCAL ? "qwen3.
|
|
73
|
-
OLLAMA = ENV.fetch("OLLAMA_BASE", "http://localhost:11434/v1")
|
|
74
|
-
|
|
75
|
-
# For a local model we route RubyLLM's :openai provider at Ollama's
|
|
76
|
-
# OpenAI-compatible endpoint, refresh the registry so tool attachment works, and
|
|
77
|
-
# run non-streaming (Ollama suppresses tool calls when streaming). See the
|
|
78
|
-
# "Local Models" guide in the docs for why.
|
|
79
|
-
def configure_local!
|
|
80
|
-
RubyLLM.configure do |c|
|
|
81
|
-
c.openai_api_base = OLLAMA
|
|
82
|
-
c.openai_api_key = "ollama" # ignored by Ollama, but RubyLLM wants a value
|
|
83
|
-
c.request_timeout = 600
|
|
84
|
-
end
|
|
85
|
-
RubyLLM.logger.level = Logger::ERROR
|
|
86
|
-
RubyLLM.models.refresh!
|
|
87
|
-
rescue StandardError => e
|
|
88
|
-
warn "warning: could not refresh Ollama models (#{e.class}: #{e.message})"
|
|
89
|
-
end
|
|
69
|
+
PROVIDER = ENV.fetch("RLTO_PROVIDER", LOCAL ? "lms" : "anthropic").to_sym
|
|
70
|
+
MODEL = ENV.fetch("RLTO_MODEL", LOCAL ? "qwen/qwen3.8-27b" : "claude-sonnet-4-6")
|
|
90
71
|
|
|
91
|
-
#
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
rescue StandardError
|
|
98
|
-
abort <<~MSG
|
|
99
|
-
Cannot reach an Ollama server at #{OLLAMA}.
|
|
100
|
-
Start it and pull a tool-capable model first:
|
|
101
|
-
|
|
102
|
-
ollama serve &
|
|
103
|
-
ollama pull #{MODEL}
|
|
104
|
-
|
|
105
|
-
Or run against a cloud model:
|
|
106
|
-
RLTO_LOCAL=false RLTO_PROVIDER=anthropic RLTO_MODEL=claude-sonnet-4-6 \\
|
|
107
|
-
ANTHROPIC_API_KEY=sk-... ruby #{File.basename(__FILE__)}
|
|
108
|
-
MSG
|
|
109
|
-
end
|
|
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
|
|
74
|
+
# adapter pointed at LM Studio, starting the server and loading MODEL as needed.
|
|
75
|
+
# Everything passed to RobotLab uses the resolved provider; PROVIDER itself is
|
|
76
|
+
# kept only for display.
|
|
77
|
+
LLM_PROVIDER = setup(provider: PROVIDER, model: MODEL)
|
|
110
78
|
|
|
111
79
|
# --- sandbox repository ----------------------------------------------------
|
|
112
80
|
|
|
@@ -352,15 +320,10 @@ OBJECTIVE = <<~OBJ.strip
|
|
|
352
320
|
file. Call submit_result when the suite passes.
|
|
353
321
|
OBJ
|
|
354
322
|
|
|
355
|
-
if LOCAL
|
|
356
|
-
preflight_local!
|
|
357
|
-
configure_local!
|
|
358
|
-
end
|
|
359
|
-
|
|
360
323
|
clean_slate! # delete any project / .robot_lab_to left by a previous run
|
|
361
324
|
sandbox = make_sandbox
|
|
362
325
|
puts "Project dir: #{sandbox}"
|
|
363
|
-
puts "Provider/model: #{PROVIDER}/#{MODEL} (#{LOCAL ? 'local
|
|
326
|
+
puts "Provider/model: #{PROVIDER}/#{MODEL} (#{LOCAL ? 'local LM Studio' : 'cloud'})"
|
|
364
327
|
puts "Objective: implement lib/roman_numeral.rb to pass the test suite"
|
|
365
328
|
puts
|
|
366
329
|
|
|
@@ -370,10 +333,10 @@ RobotLab.on(FeedbackHook)
|
|
|
370
333
|
Dir.chdir(sandbox) do
|
|
371
334
|
RobotLab::To.run(
|
|
372
335
|
OBJECTIVE,
|
|
373
|
-
provider:
|
|
336
|
+
provider: LLM_PROVIDER,
|
|
374
337
|
model: MODEL,
|
|
375
338
|
local_guards: LOCAL, # built-in file tools + small-model guardrails
|
|
376
|
-
stream: !LOCAL, # local
|
|
339
|
+
stream: !LOCAL, # local LM Studio tool calls run non-streaming
|
|
377
340
|
max_iterations: 6, # a richer task needs room to iterate
|
|
378
341
|
run_dir: RUN_DIR, # keep logs + notes under this example directory
|
|
379
342
|
# The change only commits if the seeded test suite passes:
|
|
@@ -11,7 +11,7 @@ autonomously implements the plan behind a real **quality gate**.
|
|
|
11
11
|
│ (AskUser tool) │ │ acceptance │ spec │ passes AND the quality gate │
|
|
12
12
|
│ │ │ test suite │ │ is clean │
|
|
13
13
|
└───────────────────┘ └──────────────────┘ └───────────────────────────────┘
|
|
14
|
-
gpt-5.5 (cloud) · robot_lab Network qwen3.
|
|
14
|
+
gpt-5.5 (cloud) · robot_lab Network qwen/qwen3.8-27b (local LM Studio)
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
This is the natural progression from
|
|
@@ -25,11 +25,11 @@ the implementer) writes the spec, so the implementer still can't game it.
|
|
|
25
25
|
|-------|-------------------|-------|
|
|
26
26
|
| 1 — Ideate | `AskUser` tool, network task, templated robot | OpenAI `gpt-5.5` |
|
|
27
27
|
| 2 — Plan | sequential network `task … depends_on`, data hand-off, file tools | OpenAI `gpt-5.5` |
|
|
28
|
-
| 3 — Implement | `RobotLab::To.run` autonomous loop, verify gate, `stop_when`, guardrails | local
|
|
28
|
+
| 3 — Implement | `RobotLab::To.run` autonomous loop, verify gate, `stop_when`, guardrails | local LM Studio `qwen/qwen3.8-27b` |
|
|
29
29
|
|
|
30
30
|
The reasoning phases run on a capable cloud model; the implementation loop runs
|
|
31
31
|
fully local. Because both use RubyLLM's `:openai` provider but different endpoints
|
|
32
|
-
(`api.openai.com` vs
|
|
32
|
+
(`api.openai.com` vs LM Studio's `/v1`), and `openai_api_base` is global, the example
|
|
33
33
|
toggles it between the sequential phases.
|
|
34
34
|
|
|
35
35
|
## The quality gate
|
|
@@ -55,10 +55,12 @@ flog + flay) into an autonomous gate. Tune the thresholds with `FLOG_MAX` /
|
|
|
55
55
|
|
|
56
56
|
```bash
|
|
57
57
|
export OPENAI_API_KEY="sk-..." # ideation + planning (gpt-5.5)
|
|
58
|
-
ollama serve & # implementation (local)
|
|
59
|
-
ollama pull qwen3.6:latest
|
|
60
58
|
```
|
|
61
59
|
|
|
60
|
+
`common.rb` starts the LM Studio server and loads the build model itself if
|
|
61
|
+
they aren't already running/loaded, so there's nothing to start by hand for
|
|
62
|
+
the implementation phase.
|
|
63
|
+
|
|
62
64
|
## Run it
|
|
63
65
|
|
|
64
66
|
```bash
|
|
@@ -75,8 +77,9 @@ acceptance suite and the local model implement it.
|
|
|
75
77
|
|----------|---------|-------------|
|
|
76
78
|
| `RLTO_REASON_MODEL` | `gpt-5.5` | model for ideate + plan |
|
|
77
79
|
| `RLTO_REASON_PROVIDER` | `openai` | provider for ideate + plan |
|
|
78
|
-
| `
|
|
79
|
-
| `
|
|
80
|
+
| `RLTO_BUILD_PROVIDER` | `lms` (falls back to `RLTO_PROVIDER`) | provider label for implementation; `lms` resolves to `:openai` routed at `LMS_BASE_URL` |
|
|
81
|
+
| `RLTO_BUILD_MODEL` | `qwen/qwen3.8-27b` (falls back to `RLTO_MODEL`) | model for implementation |
|
|
82
|
+
| `LMS_BASE_URL` | `http://localhost:1234/v1` | LM Studio OpenAI-compatible endpoint |
|
|
80
83
|
| `FLOG_MAX` | `25` | per-method complexity ceiling |
|
|
81
84
|
| `FLAY_MAX` | `40` | duplication-mass ceiling |
|
|
82
85
|
|
|
@@ -18,28 +18,29 @@
|
|
|
18
18
|
# suite* into the project (test/). It does not implement anything. Its
|
|
19
19
|
# reply is a one-paragraph implementation objective.
|
|
20
20
|
#
|
|
21
|
-
# Phase 3 IMPLEMENT (robot_lab-to, local
|
|
21
|
+
# Phase 3 IMPLEMENT (robot_lab-to, local LM Studio qwen/qwen3.8-27b)
|
|
22
22
|
# robot_lab-to runs an autonomous loop that writes lib/ code until the
|
|
23
23
|
# Planner's acceptance suite passes AND a quality gate is clean. The
|
|
24
24
|
# verify command (quality_gate.rb) runs tests + rubocop + flog + flay, so
|
|
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
|
-
#
|
|
29
|
-
# endpoints (api.openai.com vs
|
|
30
|
-
# toggle it between the (sequential) phases.
|
|
28
|
+
# LM Studio qwen/qwen3.8-27b. Because both use RubyLLM's :openai provider but different
|
|
29
|
+
# endpoints (api.openai.com vs LM Studio's /v1), and openai_api_base is global,
|
|
30
|
+
# we toggle it between the (sequential) phases.
|
|
31
31
|
#
|
|
32
32
|
# Everything stays under examples/02_advanced_usage/ (project/, .robot_lab_to/),
|
|
33
33
|
# both git-ignored and recreated on each run.
|
|
34
34
|
#
|
|
35
|
-
# Run it (from the gem root, with OPENAI_API_KEY set
|
|
35
|
+
# Run it (from the gem root, with OPENAI_API_KEY set; common.rb starts the LM
|
|
36
|
+
# Studio server and loads the build model for you if they aren't already
|
|
37
|
+
# running/loaded):
|
|
36
38
|
# bundle exec ruby examples/02_advanced_usage/advanced_usage.rb
|
|
37
39
|
# ===========================================================================
|
|
38
40
|
|
|
39
41
|
require "fileutils"
|
|
40
42
|
require "logger"
|
|
41
43
|
require "open3"
|
|
42
|
-
require "net/http"
|
|
43
44
|
|
|
44
45
|
# Make the example runnable straight from the repo, with or without bundler.
|
|
45
46
|
[
|
|
@@ -47,17 +48,19 @@ require "net/http"
|
|
|
47
48
|
File.expand_path("../../../robot_lab/lib", __dir__) # sibling robot_lab/lib
|
|
48
49
|
].each { |p| $LOAD_PATH.unshift(p) if Dir.exist?(p) }
|
|
49
50
|
|
|
50
|
-
require "ruby_llm"
|
|
51
51
|
require "robot_lab"
|
|
52
52
|
require "robot_lab/to"
|
|
53
|
+
require_relative "../common"
|
|
53
54
|
|
|
54
55
|
# --- configuration ---------------------------------------------------------
|
|
55
56
|
|
|
56
57
|
REASON_PROVIDER = ENV.fetch("RLTO_REASON_PROVIDER", "openai").to_sym
|
|
57
|
-
REASON_MODEL = ENV.fetch("RLTO_REASON_MODEL", "gpt-5.5")
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
REASON_MODEL = ENV.fetch("RLTO_REASON_MODEL", "gpt-5.5") # real OpenAI
|
|
59
|
+
# Falls back to the shared RLTO_PROVIDER / RLTO_MODEL (examples/.envrc sets these)
|
|
60
|
+
# so the build phase picks up the same local model as the other examples unless
|
|
61
|
+
# RLTO_BUILD_PROVIDER / RLTO_BUILD_MODEL override it specifically.
|
|
62
|
+
BUILD_PROVIDER = ENV.fetch("RLTO_BUILD_PROVIDER", ENV.fetch("RLTO_PROVIDER", "lms")).to_sym
|
|
63
|
+
BUILD_MODEL = ENV.fetch("RLTO_BUILD_MODEL", ENV.fetch("RLTO_MODEL", "qwen/qwen3.8-27b")) # local LM Studio
|
|
61
64
|
|
|
62
65
|
SANDBOX_DIR = File.expand_path("project", __dir__)
|
|
63
66
|
RUN_DIR = File.expand_path(".robot_lab_to", __dir__)
|
|
@@ -95,28 +98,17 @@ def use_real_openai!
|
|
|
95
98
|
RubyLLM.logger.level = Logger::ERROR # keep raw API traffic out of the feed
|
|
96
99
|
end
|
|
97
100
|
|
|
98
|
-
# Implementation phase routes the :openai provider at the local
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
c.request_timeout = 600
|
|
104
|
-
end
|
|
105
|
-
RubyLLM.logger.level = Logger::ERROR
|
|
106
|
-
RubyLLM.models.refresh! # register local models so tool attachment works
|
|
107
|
-
rescue StandardError => e
|
|
108
|
-
warn "warning: could not refresh Ollama models (#{e.class}: #{e.message})"
|
|
101
|
+
# Implementation phase routes the :openai provider at the local LM Studio endpoint
|
|
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.
|
|
104
|
+
def use_build_provider!
|
|
105
|
+
setup(provider: BUILD_PROVIDER, model: BUILD_MODEL)
|
|
109
106
|
end
|
|
110
107
|
|
|
111
108
|
# --- preflight -------------------------------------------------------------
|
|
112
109
|
|
|
113
110
|
def preflight!
|
|
114
111
|
abort "Set OPENAI_API_KEY (the ideation/planning phases use #{REASON_MODEL})." unless ENV["OPENAI_API_KEY"]
|
|
115
|
-
|
|
116
|
-
uri = URI.join(OLLAMA_BASE, "models")
|
|
117
|
-
Net::HTTP.start(uri.host, uri.port, open_timeout: 2, read_timeout: 2) { |h| h.get(uri.request_uri) }
|
|
118
|
-
rescue StandardError
|
|
119
|
-
abort "Cannot reach Ollama at #{OLLAMA_BASE}. Start it and `ollama pull #{BUILD_MODEL}`."
|
|
120
112
|
end
|
|
121
113
|
|
|
122
114
|
# --- sandbox ---------------------------------------------------------------
|
|
@@ -243,7 +235,7 @@ RobotLab::Narrator.enable! # live narration for every robot, across all phases
|
|
|
243
235
|
|
|
244
236
|
puts "Project dir: #{sandbox}"
|
|
245
237
|
puts "Reasoning: #{REASON_PROVIDER}/#{REASON_MODEL} (cloud) → ideate + plan"
|
|
246
|
-
puts "Building: #{BUILD_PROVIDER}/#{BUILD_MODEL} (local
|
|
238
|
+
puts "Building: #{BUILD_PROVIDER}/#{BUILD_MODEL} (local LM Studio) → implement"
|
|
247
239
|
puts
|
|
248
240
|
|
|
249
241
|
# -- Phases 1 & 2: ideate -> plan, as a robot_lab network --------------------
|
|
@@ -289,15 +281,15 @@ puts "Objective derived from the spec.\n\n"
|
|
|
289
281
|
|
|
290
282
|
# -- Phase 3: autonomous implementation with robot_lab-to --------------------
|
|
291
283
|
puts "── Phase 3: implementation (robot_lab-to, quality-gated) ──"
|
|
292
|
-
|
|
284
|
+
build_llm_provider = use_build_provider!
|
|
293
285
|
|
|
294
286
|
Dir.chdir(sandbox) do
|
|
295
287
|
RobotLab::To.run(
|
|
296
288
|
objective,
|
|
297
|
-
provider:
|
|
289
|
+
provider: build_llm_provider,
|
|
298
290
|
model: BUILD_MODEL,
|
|
299
291
|
local_guards: true, # built-in file tools + small-model guardrails
|
|
300
|
-
stream: false, #
|
|
292
|
+
stream: false, # local LM Studio tool calls run non-streaming
|
|
301
293
|
max_iterations: 8,
|
|
302
294
|
run_dir: RUN_DIR,
|
|
303
295
|
verify_command: "ruby quality_gate.rb", # tests + rubocop + flog + flay
|