robot_lab-to 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.envrc +1 -0
- data/.github/workflows/deploy-github-pages.yml +52 -0
- data/.loki +21 -0
- data/.quality/reek_baseline.txt +17 -0
- data/.rubocop.yml +7 -0
- data/CHANGELOG.md +67 -0
- data/CLAUDE.md +74 -0
- data/COMMITS.md +27 -0
- data/README.md +352 -0
- data/Rakefile +132 -0
- data/bin/robot-to +14 -0
- data/docs/about/changelog.md +82 -0
- data/docs/assets/architecture.svg +110 -0
- data/docs/assets/images/robot_lab-to.jpg +0 -0
- data/docs/assets/iteration-loop.svg +97 -0
- data/docs/concepts/evals.md +254 -0
- data/docs/concepts/iteration-loop.md +128 -0
- data/docs/concepts/notes.md +91 -0
- data/docs/concepts/run-state.md +93 -0
- data/docs/concepts/stop-conditions.md +111 -0
- data/docs/concepts/verification.md +75 -0
- data/docs/configuration/cli.md +111 -0
- data/docs/configuration/index.md +93 -0
- data/docs/configuration/settings.md +270 -0
- data/docs/getting-started/anatomy-of-a-run.md +111 -0
- data/docs/getting-started/installation.md +78 -0
- data/docs/getting-started/quick-start.md +105 -0
- data/docs/index.md +96 -0
- data/docs/local-models/guardrails.md +117 -0
- data/docs/local-models/index.md +88 -0
- data/docs/local-models/ollama.md +122 -0
- data/docs/local-models/tools.md +92 -0
- data/docs/reference/architecture.md +139 -0
- data/examples/01_basic_usage/.gitignore +9 -0
- data/examples/01_basic_usage/README.md +173 -0
- data/examples/01_basic_usage/basic_usage.rb +386 -0
- data/examples/02_advanced_usage/.gitignore +8 -0
- data/examples/02_advanced_usage/README.md +117 -0
- data/examples/02_advanced_usage/advanced_usage.rb +308 -0
- data/examples/02_advanced_usage/quality_gate.rb +99 -0
- data/examples/03_scored/.gitignore +2 -0
- data/examples/03_scored/README.md +117 -0
- data/examples/03_scored/scored_run.rb +325 -0
- data/examples/04_prose/.gitignore +2 -0
- data/examples/04_prose/README.md +43 -0
- data/examples/04_prose/prompts_dir/grade_message.md +14 -0
- data/examples/04_prose/prompts_dir/judge.md +11 -0
- data/examples/04_prose/prompts_dir/outline_criteria.md +9 -0
- data/examples/04_prose/prompts_dir/outline_objective.md +11 -0
- data/examples/04_prose/prompts_dir/section_criteria.md +7 -0
- data/examples/04_prose/prompts_dir/sections_objective.md +14 -0
- data/examples/04_prose/prose_run.rb +302 -0
- data/lib/robot_lab/to/atomic_file.rb +69 -0
- data/lib/robot_lab/to/backoff.rb +46 -0
- data/lib/robot_lab/to/cli.rb +176 -0
- data/lib/robot_lab/to/commit_manager.rb +135 -0
- data/lib/robot_lab/to/config/defaults.yml +27 -0
- data/lib/robot_lab/to/config.rb +86 -0
- data/lib/robot_lab/to/decision.rb +40 -0
- data/lib/robot_lab/to/decision_manager.rb +193 -0
- data/lib/robot_lab/to/errors.rb +30 -0
- data/lib/robot_lab/to/evals/base.rb +23 -0
- data/lib/robot_lab/to/evals/code.rb +77 -0
- data/lib/robot_lab/to/evals/context.rb +17 -0
- data/lib/robot_lab/to/evals/factory.rb +84 -0
- data/lib/robot_lab/to/evals/null.rb +17 -0
- data/lib/robot_lab/to/evals/prose.rb +110 -0
- data/lib/robot_lab/to/evals/score.rb +22 -0
- data/lib/robot_lab/to/exit_summary.rb +75 -0
- data/lib/robot_lab/to/guards/checkpoint.rb +84 -0
- data/lib/robot_lab/to/guards/grader_lock.rb +62 -0
- data/lib/robot_lab/to/guards/path_resolution.rb +61 -0
- data/lib/robot_lab/to/guards/quality_monitor.rb +113 -0
- data/lib/robot_lab/to/guards/read_before_edit.rb +83 -0
- data/lib/robot_lab/to/guards/run_store.rb +31 -0
- data/lib/robot_lab/to/guards/write_guard.rb +71 -0
- data/lib/robot_lab/to/guards.rb +54 -0
- data/lib/robot_lab/to/iteration_result.rb +29 -0
- data/lib/robot_lab/to/jsonl_logger.rb +59 -0
- data/lib/robot_lab/to/notes_manager.rb +121 -0
- data/lib/robot_lab/to/orchestrator.rb +642 -0
- data/lib/robot_lab/to/prompt_builder.rb +208 -0
- data/lib/robot_lab/to/run.rb +109 -0
- data/lib/robot_lab/to/stop_conditions.rb +71 -0
- data/lib/robot_lab/to/tools/bash.rb +75 -0
- data/lib/robot_lab/to/tools/edit.rb +52 -0
- data/lib/robot_lab/to/tools/file_tool.rb +32 -0
- data/lib/robot_lab/to/tools/read.rb +57 -0
- data/lib/robot_lab/to/tools/request_decision.rb +66 -0
- data/lib/robot_lab/to/tools/submit_result.rb +50 -0
- data/lib/robot_lab/to/tools/write.rb +32 -0
- data/lib/robot_lab/to/verifier.rb +63 -0
- data/lib/robot_lab/to/version.rb +7 -0
- data/lib/robot_lab/to.rb +81 -0
- data/mkdocs.yml +145 -0
- metadata +175 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Built-in Tools
|
|
2
|
+
|
|
3
|
+
When `--local-guards` is enabled, the per-iteration robot is given a small set of
|
|
4
|
+
built-in workspace tools in addition to `submit_iteration_result`. They are
|
|
5
|
+
named with short, conventional names (`read`, `write`, `edit`, `bash`) β small
|
|
6
|
+
models do measurably better with short tool names than with long namespaced ones.
|
|
7
|
+
|
|
8
|
+
!!! note "Why these are bundled"
|
|
9
|
+
Without file tools, a robot can only *report* β it can't change code. The
|
|
10
|
+
built-in tools give a local model a predictable, guarded surface to do real
|
|
11
|
+
work. With a cloud model you'd typically supply richer tools yourself; these
|
|
12
|
+
are a dependable baseline for the local-assistant use case.
|
|
13
|
+
|
|
14
|
+
## The tools
|
|
15
|
+
|
|
16
|
+
### `read`
|
|
17
|
+
|
|
18
|
+
Read a UTF-8 text file, optionally a line range.
|
|
19
|
+
|
|
20
|
+
| Parameter | Required | Description |
|
|
21
|
+
|-----------|----------|-------------|
|
|
22
|
+
| `path` | yes | File to read. |
|
|
23
|
+
| `offset` | no | 1-based first line to return. |
|
|
24
|
+
| `limit` | no | Maximum number of lines to return. |
|
|
25
|
+
|
|
26
|
+
Output is capped (2 000 lines) so one oversized read can't blow a small model's
|
|
27
|
+
context, and a truncation note tells the model how to read more. Reading a file
|
|
28
|
+
also satisfies the [read-before-edit](guardrails.md#read-before-edit) guard.
|
|
29
|
+
|
|
30
|
+
### `write`
|
|
31
|
+
|
|
32
|
+
Create a **new** file. Parent directories are created as needed.
|
|
33
|
+
|
|
34
|
+
| Parameter | Required | Description |
|
|
35
|
+
|-----------|----------|-------------|
|
|
36
|
+
| `path` | yes | File to create. |
|
|
37
|
+
| `content` | yes | Full text content. |
|
|
38
|
+
|
|
39
|
+
`write` is for new files only. The [write-guard](guardrails.md#write-guard)
|
|
40
|
+
refuses it on a file that already exists and redirects the model to `edit` β small
|
|
41
|
+
models otherwise rewrite whole files and lose content.
|
|
42
|
+
|
|
43
|
+
### `edit`
|
|
44
|
+
|
|
45
|
+
Exact-string replacement in an existing file.
|
|
46
|
+
|
|
47
|
+
| Parameter | Required | Description |
|
|
48
|
+
|-----------|----------|-------------|
|
|
49
|
+
| `path` | yes | File to edit. |
|
|
50
|
+
| `old_text` | yes | Exact text to replace (whitespace included). |
|
|
51
|
+
| `new_text` | yes | Replacement text. |
|
|
52
|
+
| `replace_all` | no | Replace every occurrence (default false). |
|
|
53
|
+
|
|
54
|
+
`old_text` must match the current file contents exactly and be unique unless
|
|
55
|
+
`replace_all` is set; otherwise the edit is refused with a clear message. The
|
|
56
|
+
[read-before-edit](guardrails.md#read-before-edit) guard requires the file to have
|
|
57
|
+
been `read` this run first, so `old_text` reflects the real contents rather than a
|
|
58
|
+
guess.
|
|
59
|
+
|
|
60
|
+
### `bash`
|
|
61
|
+
|
|
62
|
+
Run a shell command in the project directory.
|
|
63
|
+
|
|
64
|
+
| Parameter | Required | Description |
|
|
65
|
+
|-----------|----------|-------------|
|
|
66
|
+
| `command` | yes | Shell command to run. |
|
|
67
|
+
| `timeout` | no | Seconds before the command is killed (default 120). |
|
|
68
|
+
|
|
69
|
+
Returns combined stdout+stderr and the exit status (`[exit 0]\n...`). Output is
|
|
70
|
+
capped (~30 000 chars) and a runaway process is killed at the timeout so it can't
|
|
71
|
+
stall an overnight loop.
|
|
72
|
+
|
|
73
|
+
## How they're wired in
|
|
74
|
+
|
|
75
|
+
The orchestrator builds them per iteration and attaches them through RobotLab's
|
|
76
|
+
`local_tools:`:
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
# only when local_guards is on
|
|
80
|
+
[submit_tool, Tools::Read.new, Tools::Write.new, Tools::Edit.new, Tools::Bash.new]
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Without `--local-guards`, the robot gets only `submit_iteration_result` β you're
|
|
84
|
+
expected to supply your own tools (typical for cloud-model setups).
|
|
85
|
+
|
|
86
|
+
The short names come from a small `FileTool` base class that overrides RubyLLM's
|
|
87
|
+
default namespaced tool name. The guardrail hooks match on exactly these names
|
|
88
|
+
(`read`/`write`/`edit`/`bash`).
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
Next: [Guardrails](guardrails.md).
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
`robot_lab-to` is a thin, well-separated orchestration layer over RobotLab. The
|
|
4
|
+
`Orchestrator` owns the loop and the git history; everything else is a
|
|
5
|
+
single-purpose collaborator it drives.
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
## Component map
|
|
10
|
+
|
|
11
|
+
| Component | File | Responsibility |
|
|
12
|
+
|-----------|------|----------------|
|
|
13
|
+
| `CLI` | `cli.rb` | `OptionParser`-based `robot-to` entry point. |
|
|
14
|
+
| `Config` | `config.rb` | `MywayConfig::Base` settings cascade. |
|
|
15
|
+
| `Orchestrator` | `orchestrator.rb` | The main loop: setup β iterate β eval-gated commit/rollback β stop β finalize. |
|
|
16
|
+
| `PromptBuilder` | `prompt_builder.rb` | Builds each iteration's system prompt (objective, notes, task, submit, repair, score feedback, stop-when). |
|
|
17
|
+
| `Run` | `run.rb` | Mutable run-state value object (iteration, counters, tokens, timing, paths, `last_score_value`, `iterations_since_improvement`). |
|
|
18
|
+
| `SubmitResult` | `tools/submit_result.rb` | Tool the robot calls to report its result. |
|
|
19
|
+
| `IterationResult` | `iteration_result.rb` | `Data.define` value object for a submitted result. |
|
|
20
|
+
| `CommitManager` | `commit_manager.rb` | All git operations via `Open3` (no shell interpolation); also diffs the working tree vs. a ref for `Evals::Prose`. |
|
|
21
|
+
| `NotesManager` | `notes_manager.rb` | Reads/writes the cross-iteration `notes.md`. |
|
|
22
|
+
| `StopConditions` | `stop_conditions.rb` | Evaluates iterations / tokens / failures / plateau / stop-when. |
|
|
23
|
+
| `Verifier` | `verifier.rb` | Low-level command runner (timeout, process-group kill); composed by `Evals::Code`. |
|
|
24
|
+
| `Backoff` | `backoff.rb` | Interruptible exponential backoff for retries. |
|
|
25
|
+
| `JsonlLogger` | `jsonl_logger.rb` | Append-only JSONL event log with a pre-init buffer. |
|
|
26
|
+
| `ExitSummary` | `exit_summary.rb` | Prints the post-run report. |
|
|
27
|
+
| `AtomicFile` | `atomic_file.rb` | Atomic writes/appends for `notes.md`. |
|
|
28
|
+
|
|
29
|
+
## Evals β orchestrator-owned scoring
|
|
30
|
+
|
|
31
|
+
The eval subsystem generalizes the verify-command gate into a pluggable, scalar
|
|
32
|
+
scoring loop. See [Evals](../concepts/evals.md) for the full behavioral model β
|
|
33
|
+
this table is the file map.
|
|
34
|
+
|
|
35
|
+
| Component | File | Responsibility |
|
|
36
|
+
|-----------|------|-----------------|
|
|
37
|
+
| `Evals::Score` | `evals/score.rb` | `Data.define(gate_ok, improved, met_target, value, detail, output)` β the verdict for one iteration. |
|
|
38
|
+
| `Evals::Context` | `evals/context.rb` | `Data.define(work_dir, previous_ref, previous_value, objective, iteration)` handed to every eval. |
|
|
39
|
+
| `Evals::Base` | `evals/base.rb` | Abstract eval; `#score(ctx)` and `#protected_paths` (default `[]`). |
|
|
40
|
+
| `Evals::Null` | `evals/null.rb` | Default when nothing is configured: always `gate_ok: true, improved: true, met_target: false`. |
|
|
41
|
+
| `Evals::Code` | `evals/code.rb` | Measured/deterministic: `--verify-command` floor + optional `--measure`/`--target`. Composes `Verifier`. |
|
|
42
|
+
| `Evals::Prose` | `evals/prose.rb` | Judged/pairwise: an LLM judge compares the working tree vs. the parent commit; optional `--floor`. `met_target` always `false`. |
|
|
43
|
+
| `Evals::ProcEval` | `evals/factory.rb` | Wraps a bare proc passed as `eval:` so it responds to `#score`. |
|
|
44
|
+
| `Evals.build` / `Evals.from_name` | `evals/factory.rb` | Resolves `config.eval` (instance / proc / registered Symbol / `"code"` / `"null"` / `"prose"` / `nil`) into an eval. |
|
|
45
|
+
| `Guards::GraderLock` | `guards/grader_lock.rb` | Always-on `RobotLab::Hook` (independent of `--local-guards`) refusing write/edit/bash on an eval's `protected_paths` + `--protect-path`. |
|
|
46
|
+
|
|
47
|
+
## Local-model layer
|
|
48
|
+
|
|
49
|
+
These components are active only when `--local-guards` is set:
|
|
50
|
+
|
|
51
|
+
| Component | File | Responsibility |
|
|
52
|
+
|-----------|------|----------------|
|
|
53
|
+
| `Tools::FileTool` | `tools/file_tool.rb` | Base class giving file tools short names. |
|
|
54
|
+
| `Tools::Read/Write/Edit/Bash` | `tools/*.rb` | Built-in workspace tools. |
|
|
55
|
+
| `Guards` | `guards.rb` | Registers all guards on a robot via `install(robot, run:, except:)`. |
|
|
56
|
+
| `Guards::WriteGuard` | `guards/write_guard.rb` | Refuse `write` on existing files; normalize paths. Excludable per-run via `Config#write_guard = false` (Ruby-only, default `true`) β useful when the robot is iteratively rewriting a whole file, e.g. a prose draft. |
|
|
57
|
+
| `Guards::ReadBeforeEdit` | `guards/read_before_edit.rb` | Refuse `edit` before `read`. |
|
|
58
|
+
| `Guards::Checkpoint` | `guards/checkpoint.rb` | Snapshot files before mutation. |
|
|
59
|
+
| `Guards::QualityMonitor` | `guards/quality_monitor.rb` | Detect repeated-tool-call loops. |
|
|
60
|
+
| `Guards::RunStore` | `guards/run_store.rb` | Thread-local per-run guard state. |
|
|
61
|
+
| `Guards::PathResolution` | `guards/path_resolution.rb` | Pure path helpers shared by the guards. |
|
|
62
|
+
|
|
63
|
+
See [Guardrails](../local-models/guardrails.md) and
|
|
64
|
+
[Built-in Tools](../local-models/tools.md).
|
|
65
|
+
|
|
66
|
+
## Key design decisions
|
|
67
|
+
|
|
68
|
+
### Fresh robot per iteration
|
|
69
|
+
|
|
70
|
+
Each iteration constructs a new `RobotLab::Robot`. There is no shared chat history;
|
|
71
|
+
continuity is carried entirely by `notes.md`. This keeps every iteration a clean,
|
|
72
|
+
independent attempt and bounds context growth.
|
|
73
|
+
|
|
74
|
+
### The orchestrator owns git, the robot owns work
|
|
75
|
+
|
|
76
|
+
The robot never commits. It makes changes and reports a result; the orchestrator
|
|
77
|
+
decides whether to commit (gated by the configured eval) or roll back. This
|
|
78
|
+
separation is what makes the git history trustworthy.
|
|
79
|
+
|
|
80
|
+
### The eval, not the robot, decides "better" and "done"
|
|
81
|
+
|
|
82
|
+
`should_fully_stop` and a claimed `success: true` are the robot grading its own
|
|
83
|
+
homework. `Evals::Base#score` is the orchestrator-owned authority instead:
|
|
84
|
+
`gate_ok`/`improved` decide commit vs. rollback, and `met_target` β not the
|
|
85
|
+
robot's self-report β decides when a measurable objective is done. `--stop-when`
|
|
86
|
+
remains as a fallback for objectives an eval can't measure. See
|
|
87
|
+
[Evals](../concepts/evals.md).
|
|
88
|
+
|
|
89
|
+
### Grader lockdown is separate from the local-model guards
|
|
90
|
+
|
|
91
|
+
`Guards::GraderLock` protects the eval's own criteria (a spec, a floor script,
|
|
92
|
+
`--protect-path` globs) from robot edits. It is wired in unconditionally when
|
|
93
|
+
protected paths exist, unlike the small-model guardrail set
|
|
94
|
+
(`Guards.install`, gated by `--local-guards`) β a frontier model can reward-hack
|
|
95
|
+
its grading criteria just as easily as a small one can mangle a file.
|
|
96
|
+
|
|
97
|
+
### Tools attach via `local_tools:`
|
|
98
|
+
|
|
99
|
+
Tool *instances* are passed to `RobotLab.build` as `local_tools:`. (RobotLab's
|
|
100
|
+
`tools:` parameter is a name *allowlist filter*, not an instance list β passing
|
|
101
|
+
instances there silently attaches nothing.) A regression test asserts the built
|
|
102
|
+
robot actually exposes the submit tool, guarding against that class of mistake.
|
|
103
|
+
|
|
104
|
+
### Streaming is optional
|
|
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).
|
|
110
|
+
|
|
111
|
+
### Interruptible by design
|
|
112
|
+
|
|
113
|
+
Signal handlers, the backoff sleep, and the per-iteration runner thread all
|
|
114
|
+
cooperate so a `SIGINT`/`SIGTERM` stops the run promptly and cleanly, with the
|
|
115
|
+
exit summary still printed.
|
|
116
|
+
|
|
117
|
+
## Run lifecycle (control flow)
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
To.run(objective, **opts)
|
|
121
|
+
ββ Orchestrator#run
|
|
122
|
+
ββ setup_run branch, notes, run dir, collaborators (incl. Evals.build)
|
|
123
|
+
ββ main_loop
|
|
124
|
+
β ββ per iteration:
|
|
125
|
+
β ββ build fresh robot (PromptBuilder + tools [+ guards] [+ GraderLock])
|
|
126
|
+
β ββ robot.run β SubmitResult captured
|
|
127
|
+
β ββ nudge if not submitted
|
|
128
|
+
β ββ evaluate (Eval#score) if success reported
|
|
129
|
+
β β ββ gate fails β repair_until_gate (R2), else reset --hard
|
|
130
|
+
β β ββ gate ok, not improved β reset --hard ([NO IMPROVEMENT])
|
|
131
|
+
β β ββ gate ok, improved β commit; check met_target?
|
|
132
|
+
β ββ NotesManager append
|
|
133
|
+
β ββ StopConditions check (incl. plateau)
|
|
134
|
+
ββ finalize_run JSONL close, ExitSummary
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
Next: [Changelog](../about/changelog.md).
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Artifacts produced when the example runs β never commit them into the
|
|
2
|
+
# robot_lab-to repository. (gitignore has no inline comments, so each note
|
|
3
|
+
# is on its own line.)
|
|
4
|
+
|
|
5
|
+
# the throwaway git repo the robot works in (recreated each run)
|
|
6
|
+
/project/
|
|
7
|
+
|
|
8
|
+
# the run's event log and cross-iteration notes.md
|
|
9
|
+
/.robot_lab_to/
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# 01 β Basic Usage
|
|
2
|
+
|
|
3
|
+
An end-to-end use of `robot_lab-to`, driven programmatically with
|
|
4
|
+
`RobotLab::To.run`.
|
|
5
|
+
|
|
6
|
+
The script ([`basic_usage.rb`](basic_usage.rb)):
|
|
7
|
+
|
|
8
|
+
1. Creates a fresh **`project/`** git repository next to the script,
|
|
9
|
+
seeded with a **failing Minitest suite** for a Roman-numeral library
|
|
10
|
+
(`test/roman_numeral_test.rb`). It's git-ignored and re-created on each run.
|
|
11
|
+
2. Runs an autonomous loop with the objective *"implement `lib/roman_numeral.rb`
|
|
12
|
+
until the seeded test suite passes."* Because the tests are fixed and the robot
|
|
13
|
+
may not edit them, it has to satisfy real, externally-defined requirements it
|
|
14
|
+
can't game.
|
|
15
|
+
3. Gates each iteration on `ruby -Ilib -Itest test/roman_numeral_test.rb` β a
|
|
16
|
+
change is committed only when the suite is green; otherwise it's rolled back.
|
|
17
|
+
4. Stops early (via `stop_when`) once the suite passes, and prints the resulting
|
|
18
|
+
branch, commit history, the generated implementation, and a final test run.
|
|
19
|
+
|
|
20
|
+
It exercises the full loop over several iterations: **branch β iterate β verify β
|
|
21
|
+
commit on green / roll back on red**.
|
|
22
|
+
|
|
23
|
+
### The seeded spec
|
|
24
|
+
|
|
25
|
+
The library must provide `RomanNumeral.to_roman(int)` and
|
|
26
|
+
`RomanNumeral.from_roman(str)` with: subtractive notation (`IV`, `IX`, `XL`, β¦),
|
|
27
|
+
the `1..3999` range, round-trip correctness, case-insensitive parsing, and
|
|
28
|
+
`ArgumentError` on invalid input β including **non-canonical** numerals like
|
|
29
|
+
`"IIII"`. The full expectations live in the seeded `test/roman_numeral_test.rb`.
|
|
30
|
+
|
|
31
|
+
## Prerequisites
|
|
32
|
+
|
|
33
|
+
Either a **local Ollama** server (default, no API key) or a **cloud** provider key.
|
|
34
|
+
|
|
35
|
+
=== "Local Ollama (default)"
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
ollama serve & # if not already running
|
|
39
|
+
ollama pull qwen3.6:latest # a tool-capable model
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
=== "Cloud provider"
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
export ANTHROPIC_API_KEY="sk-ant-..." # or OPENAI_API_KEY
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Run it
|
|
49
|
+
|
|
50
|
+
From the gem root:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# Local Ollama (default)
|
|
54
|
+
bundle exec ruby examples/01_basic_usage/basic_usage.rb
|
|
55
|
+
|
|
56
|
+
# A cloud model instead
|
|
57
|
+
RLTO_LOCAL=false RLTO_PROVIDER=anthropic RLTO_MODEL=claude-sonnet-4-6 \
|
|
58
|
+
bundle exec ruby examples/01_basic_usage/basic_usage.rb
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Configuration
|
|
62
|
+
|
|
63
|
+
All optional, set via environment variables:
|
|
64
|
+
|
|
65
|
+
| Variable | Default | Description |
|
|
66
|
+
|----------|---------|-------------|
|
|
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. |
|
|
71
|
+
|
|
72
|
+
## What you'll see
|
|
73
|
+
|
|
74
|
+
A `RobotLab::Hook` (`FeedbackHook` in the script) narrates the robot's actions
|
|
75
|
+
live, so the run never sits silent while the model works:
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
Cleaning leftover: .../examples/01_basic_usage/project
|
|
79
|
+
Project dir: examples/01_basic_usage/project
|
|
80
|
+
Provider/model: openai/qwen3.6:latest (local Ollama)
|
|
81
|
+
Objective: implement lib/roman_numeral.rb to pass the test suite
|
|
82
|
+
|
|
83
|
+
π€ thinkingβ¦
|
|
84
|
+
π read test/roman_numeral_test.rb
|
|
85
|
+
π write lib/roman_numeral.rb
|
|
86
|
+
π» bash: ruby -Ilib -Itest test/roman_numeral_test.rb
|
|
87
|
+
β³ exit 1
|
|
88
|
+
βοΈ edit lib/roman_numeral.rb
|
|
89
|
+
π» bash: ruby -Ilib -Itest test/roman_numeral_test.rb
|
|
90
|
+
β³ exit 0
|
|
91
|
+
β
submit result: Implemented RomanNumeral.to_roman/from_roman; all 9 tests pass.
|
|
92
|
+
|
|
93
|
+
=== Result ===
|
|
94
|
+
Branch: robot-to/implement-a-roman-numeral-library-...
|
|
95
|
+
|
|
96
|
+
Commits:
|
|
97
|
+
4a59bbc robot-to 1: Implemented lib/roman_numeral.rb ... all 9 tests pass.
|
|
98
|
+
72b06ec initial: failing roman-numeral test suite
|
|
99
|
+
|
|
100
|
+
lib/roman_numeral.rb:
|
|
101
|
+
module RomanNumeral
|
|
102
|
+
...
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
Final test run:
|
|
106
|
+
9 runs, 594 assertions, 0 failures, 0 errors, 0 skips
|
|
107
|
+
|
|
108
|
+
Notes: examples/01_basic_usage/.robot_lab_to/runs/.../notes.md
|
|
109
|
+
Project: examples/01_basic_usage/project
|
|
110
|
+
Run logs: examples/01_basic_usage/.robot_lab_to
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Live feedback via the hook system
|
|
114
|
+
|
|
115
|
+
`FeedbackHook < RobotLab::Hook` is registered globally with `RobotLab.on(...)`,
|
|
116
|
+
so it applies to every per-iteration robot the orchestrator builds β no change to
|
|
117
|
+
the orchestrator needed. It implements three hook methods:
|
|
118
|
+
|
|
119
|
+
| Hook method | Reports |
|
|
120
|
+
|-------------|---------|
|
|
121
|
+
| `before_llm_generation` | `π€ thinkingβ¦` β the model started generating |
|
|
122
|
+
| `before_tool_call` | the action: read / write / edit / bash / submit |
|
|
123
|
+
| `after_tool_call` | a shell exit code, or a tool error |
|
|
124
|
+
|
|
125
|
+
This is a compact, practical example of the robot_lab hook system; the guardrails
|
|
126
|
+
shipped with `--local-guards` use the same mechanism.
|
|
127
|
+
|
|
128
|
+
> **Don't want to write your own?** robot_lab ships a ready-made narrator β
|
|
129
|
+
> `RobotLab::Narrator.enable!` β which does exactly this. The
|
|
130
|
+
> [02_advanced_usage](../02_advanced_usage/README.md#live-feedback) example uses
|
|
131
|
+
> it. We hand-write the hook here to teach the API.
|
|
132
|
+
|
|
133
|
+
> **Note:** robot_lab-to's own `robot-to: β¦` progress lines use `Kernel#warn`,
|
|
134
|
+
> which is silenced when Ruby warnings are disabled (`$VERBOSE` is `nil`, common
|
|
135
|
+
> under `bundle exec`). The hook writes to `$stderr` directly, so its narration
|
|
136
|
+
> always shows.
|
|
137
|
+
|
|
138
|
+
Everything the example produces stays under `examples/01_basic_usage/`:
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
examples/01_basic_usage/
|
|
142
|
+
βββ basic_usage.rb
|
|
143
|
+
βββ project/ # the throwaway git repo (re-created on every run)
|
|
144
|
+
βββ .robot_lab_to/ # run event logs + cross-iteration notes.md
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Each run **starts with a clean slate** β `project/` and `.robot_lab_to/`
|
|
148
|
+
left over from a previous execution are deleted before setup, then rebuilt. The
|
|
149
|
+
most recent run's repo, logs, and `notes.md` remain afterward for inspection
|
|
150
|
+
(until the next run clears them). Both are git-ignored, so you never need to
|
|
151
|
+
clean them up by hand.
|
|
152
|
+
|
|
153
|
+
## How it maps to the library
|
|
154
|
+
|
|
155
|
+
The whole example is one call:
|
|
156
|
+
|
|
157
|
+
```ruby
|
|
158
|
+
RobotLab::To.run(
|
|
159
|
+
objective,
|
|
160
|
+
provider: :openai, # routes to Ollama for local runs
|
|
161
|
+
model: "qwen3.6:latest",
|
|
162
|
+
local_guards: true, # built-in file tools + guardrails
|
|
163
|
+
stream: false, # Ollama tool calls need non-streaming
|
|
164
|
+
max_iterations: 6,
|
|
165
|
+
verify_command: "ruby -Ilib -Itest test/roman_numeral_test.rb",
|
|
166
|
+
stop_when: "test/roman_numeral_test.rb passes with 0 failures and 0 errors"
|
|
167
|
+
)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
For the meaning of each option, see the
|
|
171
|
+
[Settings Reference](https://madbomber.github.io/robot_lab-to/configuration/settings/)
|
|
172
|
+
and, for the local-model specifics, the
|
|
173
|
+
[Local Models guide](https://madbomber.github.io/robot_lab-to/local-models/).
|