Commit 8d4423c
2026-08-29 22:34:41
1 parent
5649997
Changed files (22)
spec
evals
harness
prompts
spec/evals/harness/ablator_spec.rb
@@ -1,74 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.describe Elelem::Evals::Ablator do
- # A case passes only while every required marker is still present in the prompt.
- # This lets a test describe which lines are load-bearing purely by content.
- def scorer_for(requirements)
- lambda do |prompt|
- results = requirements.map do |id, marker|
- status = prompt.include?(marker) ? "PASS" : "FAIL"
- Elelem::Evals::Result.new(
- id: id, group: "g", status: status, failures: status == "PASS" ? [] : ["missing #{marker}"],
- turns: 1, duration: 0.0, tools: [], response: ""
- )
- end
- Elelem::Evals::Score.new(results: results)
- end
- end
-
- subject(:ablator) { described_class.new(scorer_for: scorer_for(requirements)) }
-
- describe "#minimize" do
- let(:requirements) { { "keep" => "KEEP" } }
-
- it "removes a line whose deletion regresses nothing" do
- result = ablator.minimize("KEEP the case\nDEAD weight\n")
-
- expect(result.prompt).to eq("KEEP the case\n")
- end
-
- it "keeps a load-bearing line" do
- result = ablator.minimize("KEEP the case\n")
-
- expect(result.prompt).to eq("KEEP the case\n")
- end
-
- it "reaches a fixpoint, dropping every dead line" do
- result = ablator.minimize("dead one\nKEEP the case\ndead two\ndead three\n")
-
- expect(result.prompt).to eq("KEEP the case\n")
- end
-
- it "never removes an ERB line even when it defends no case" do
- result = ablator.minimize("<%= pwd %>\nDEAD weight\nKEEP the case\n")
-
- expect(result.prompt).to eq("<%= pwd %>\nKEEP the case\n")
- end
-
- it "leaves blank lines in place" do
- result = ablator.minimize("KEEP the case\n\nDEAD weight\n")
-
- expect(result.prompt).to eq("KEEP the case\n\n")
- end
-
- context "with two load-bearing lines" do
- let(:requirements) { { "keep" => "KEEP", "hold" => "HOLD" } }
-
- it "keeps both and drops only the dead line" do
- result = ablator.minimize("KEEP one\nDEAD weight\nHOLD two\n")
-
- expect(result.prompt).to eq("KEEP one\nHOLD two\n")
- end
- end
-
- describe "the defends map" do
- let(:requirements) { { "keep" => "KEEP" } }
-
- it "maps each surviving line to the cases that regress without it" do
- result = ablator.minimize("KEEP the case\nDEAD weight\n")
-
- expect(result.defends).to eq("KEEP the case\n" => ["g/keep"])
- end
- end
- end
-end
spec/evals/harness/assertions_spec.rb
@@ -1,105 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.describe Elelem::Evals::Assertions do
- def failures(expect, workspace: nil, response: "", tools: [])
- described_class.new(expect).failures(workspace: workspace, response: response, tools: tools)
- end
-
- describe "verify" do
- it "passes when the command exits 0" do
- Elelem::Evals::Workspace.open(fixture: "hello") do |workspace|
- expect(failures({ verify: "true" }, workspace: workspace)).to be_empty
- end
- end
-
- it "fails when the command exits non zero" do
- Elelem::Evals::Workspace.open(fixture: "hello") do |workspace|
- expect(failures({ verify: "false" }, workspace: workspace).first).to include("verify failed")
- end
- end
- end
-
- describe "files" do
- it "fails when a required string is missing" do
- Elelem::Evals::Workspace.open(fixture: "hello") do |workspace|
- result = failures({ files: { "hello.rb" => { "contains" => ["Goodbye"] } } }, workspace: workspace)
-
- expect(result.first).to include("hello.rb missing")
- end
- end
-
- it "fails when a forbidden string is present" do
- Elelem::Evals::Workspace.open(fixture: "hello") do |workspace|
- result = failures({ files: { "hello.rb" => { "not_contains" => ["Hello"] } } }, workspace: workspace)
-
- expect(result.first).to include("still contains")
- end
- end
-
- it "fails when the file is missing" do
- Elelem::Evals::Workspace.open(fixture: "hello") do |workspace|
- result = failures({ files: { "nope.rb" => { "contains" => ["x"] } } }, workspace: workspace)
-
- expect(result).to eq(["nope.rb missing"])
- end
- end
- end
-
- describe "response" do
- it "passes when every string is present" do
- expect(failures({ response_contains: %w[alpha beta] }, response: "alpha and beta")).to be_empty
- end
-
- it "fails when a string is absent" do
- expect(failures({ response_contains: ["gamma"] }, response: "alpha").first).to include("response missing")
- end
-
- it "fails when a forbidden string is present" do
- result = failures({ response_not_contains: [".git"] }, response: "the biggest file is .git/description")
-
- expect(result.first).to include("response contains")
- end
-
- it "passes when no forbidden string is present" do
- expect(failures({ response_not_contains: [".git"] }, response: "data/report.csv")).to be_empty
- end
-
- it "fails when a regex does not match" do
- expect(failures({ response_matches: ['\d+'] }, response: "none").first).to include("does not match")
- end
- end
-
- describe "tools" do
- let(:tools) { [Elelem::Evals::ToolCall.new(name: "read", args: { "path" => "config/loader.rb" })] }
-
- it "passes when a named tool was called" do
- expect(failures({ tools_used: ["read"] }, tools: tools)).to be_empty
- end
-
- it "fails when a named tool was never called" do
- expect(failures({ tools_used: ["write"] }, tools: tools).first).to include("never called")
- end
-
- it "passes when argument constraints match" do
- expect(failures({ tools_used: [{ "read" => { "path" => "loader.rb" } }] }, tools: tools)).to be_empty
- end
-
- it "fails when argument constraints do not match" do
- result = failures({ tools_used: [{ "read" => { "path" => "other.rb" } }] }, tools: tools)
-
- expect(result.first).to include("never called")
- end
-
- it "fails when a forbidden tool was called" do
- expect(failures({ tools_not_used: ["read"] }, tools: tools).first).to include("called read")
- end
- end
-
- it "does not score turn count -- correctness only" do
- expect(failures({ max_turns: 1 }, response: "anything")).to be_empty
- end
-
- it "reports every failure at once" do
- expect(failures({ response_contains: %w[a b c] }, response: "").length).to eq(3)
- end
-end
spec/evals/harness/bounded_client_spec.rb
@@ -1,31 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.describe Elelem::Evals::BoundedClient do
- let(:inner) { double("client", fetch: []) }
-
- subject(:client) { described_class.new(inner, max_turns: 2) }
-
- it "delegates to the wrapped client" do
- client.fetch([{ role: "user", content: "hi" }])
-
- expect(inner).to have_received(:fetch)
- end
-
- it "counts turns" do
- 2.times { client.fetch([]) }
-
- expect(client.turns).to eq(2)
- end
-
- it "raises once past the limit" do
- 2.times { client.fetch([]) }
-
- expect { client.fetch([]) }.to raise_error(described_class::TurnLimitExceeded)
- end
-
- it "reports the overrun" do
- 3.times { client.fetch([]) rescue nil }
-
- expect(client.turns).to eq(3)
- end
-end
spec/evals/harness/case_spec.rb
@@ -1,61 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.describe Elelem::Evals::Case do
- let(:yaml) do
- <<~YAML
- - id: patch-single-line
- fixture: hello
- setup:
- - git init -q
- turns:
- - change the greeting to Goodbye
- expect:
- verify: ruby hello.rb
- max_turns: 4
- - id: no-expectations
- fixture: hello
- turns:
- - say hello
- YAML
- end
-
- let(:dir) { Dir.mktmpdir }
- let(:file) do
- path = File.join(dir, "editing.yml")
- File.write(path, yaml)
- path
- end
-
- after { FileUtils.remove_entry(dir) if File.directory?(dir) }
-
- subject(:cases) { described_class.load_file(file) }
-
- it "loads every case in the file" do
- expect(cases.map(&:id)).to eq(%w[patch-single-line no-expectations])
- end
-
- it "takes the group from the file name" do
- expect(cases.first.group).to eq("editing")
- end
-
- it "reads setup commands" do
- expect(cases.first.setup).to eq(["git init -q"])
- end
-
- it "symbolizes the top level of expect" do
- expect(cases.first.expect[:verify]).to eq("ruby hello.rb")
- end
-
- it "reads max_turns from expect" do
- expect(cases.first.max_turns).to eq(4)
- end
-
- it "defaults max_turns to the generous safety ceiling" do
- expect(cases.last.max_turns).to eq(30)
- end
-
- it "defaults setup and expect to empty" do
- expect(cases.last.setup).to eq([])
- expect(cases.last.expect).to eq({})
- end
-end
spec/evals/harness/cases_lint_spec.rb
@@ -1,19 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.describe "eval cases are well formed" do
- Elelem::Evals::Case.load_all.each do |kase|
- describe kase.to_s do
- it "references a fixture that exists" do
- expect(File).to be_directory(File.join(Elelem::Evals::Workspace::FIXTURES, kase.fixture))
- end
-
- it "has at least one turn" do
- expect(kase.turns).not_to be_empty
- end
-
- it "has at least one expectation" do
- expect(kase.expect).not_to be_empty
- end
- end
- end
-end
spec/evals/harness/improver_spec.rb
@@ -1,161 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.describe Elelem::Evals::Improver do
- # Replays a canned JSON body the way Ollama streams it.
- class CannedClient
- attr_reader :messages
-
- def initialize(body)
- @body = body
- end
-
- def fetch(messages, _tools = [], &block)
- @messages = messages
- block.call(type: "saying", text: @body)
- []
- end
- end
-
- let(:body) do
- JSON.generate(
- "analysis" => "the prompt never mentions git",
- "changes" => [
- { "old_text" => "# Policy", "new_text" => "# Git\nCommit with a 50 character subject.\n\n# Policy", "rationale" => "teach the 50/72 rule" }
- ]
- )
- end
-
- let(:client) { CannedClient.new(body) }
- let(:prompt) { "Terminal coding agent.\n\n# Policy\n- Verify your work\n" }
-
- subject(:improver) { described_class.new(client: client) }
-
- describe "#plan" do
- it "parses the model's JSON" do
- plan = improver.plan(prompt: prompt, failures: [])
-
- expect(plan["changes"].first["rationale"]).to eq("teach the 50/72 rule")
- end
-
- it "sends the prompt and the failures to the model" do
- failure = Elelem::Evals::Result.new(
- id: "commit-50-72", group: "git", status: "FAIL", failures: ["verify failed (exit 1)"],
- turns: 3, duration: 1.0, tools: [], response: "committed"
- )
-
- improver.plan(prompt: prompt, failures: [failure])
- sent = client.messages.map { |m| m[:content] }.join("\n")
-
- expect(sent).to include("commit-50-72")
- expect(sent).to include("verify failed")
- expect(sent).to include("# Policy")
- end
-
- it "returns no changes when the model returns junk" do
- plan = described_class.new(client: CannedClient.new("not json")).plan(prompt: prompt, failures: [])
-
- expect(plan).to eq("analysis" => "", "changes" => [])
- end
-
- it "returns no changes when the model returns a JSON array" do
- plan = described_class.new(client: CannedClient.new("[1, 2, 3]")).plan(prompt: prompt, failures: [])
-
- expect(plan).to eq("analysis" => "", "changes" => [])
- end
-
- it "returns no changes when the model returns JSON null" do
- plan = described_class.new(client: CannedClient.new("null")).plan(prompt: prompt, failures: [])
-
- expect(plan).to eq("analysis" => "", "changes" => [])
- end
-
- it "returns no changes when changes is a JSON object" do
- body = JSON.generate("analysis" => "x", "changes" => { "old_text" => "a", "new_text" => "b" })
- plan = described_class.new(client: CannedClient.new(body)).plan(prompt: prompt, failures: [])
-
- expect(plan["changes"]).to be_empty
- end
-
- it "does not share the changes array between fallback results" do
- improver = described_class.new(client: CannedClient.new("not json"))
-
- first = improver.plan(prompt: prompt, failures: [])
- first["changes"] << "mutated"
-
- expect(improver.plan(prompt: prompt, failures: [])["changes"]).to be_empty
- end
- end
-
- describe "#apply" do
- it "applies a change and reports the rationale" do
- changes = [{ "old_text" => "# Policy", "new_text" => "# Git\n\n# Policy", "rationale" => "teach git" }]
- updated, applied = improver.apply(prompt, changes)
-
- expect(updated).to include("# Git")
- expect(applied).to eq(["teach git"])
- end
-
- it "skips a change whose old_text is absent" do
- changes = [{ "old_text" => "# Nowhere", "new_text" => "x", "rationale" => "nope" }]
- updated, applied = improver.apply(prompt, changes)
-
- expect(updated).to eq(prompt)
- expect(applied).to be_empty
- end
-
- it "keeps regex escapes in new_text literal" do
- changes = [{ "old_text" => "# Policy", "new_text" => "Use sed -i'' 's/a\\&b/c/' and \\0", "rationale" => "sed" }]
- updated, = improver.apply(prompt, changes)
-
- expect(updated).to include("s/a\\&b/c/' and \\0")
- end
-
- it "skips a change that is not an object" do
- updated, applied = improver.apply(prompt, [["old_text", "# Policy"]])
-
- expect(updated).to eq(prompt)
- expect(applied).to be_empty
- end
-
- it "replaces only the first occurrence" do
- changes = [{ "old_text" => "a", "new_text" => "b", "rationale" => "once" }]
- updated, = improver.apply("a a a", changes)
-
- expect(updated).to eq("b a a")
- end
-
- it "appends new_text to grow the prompt when old_text is empty" do
- changes = [{ "old_text" => "", "new_text" => "# Git\nCommit small.", "rationale" => "add git" }]
- updated, applied = improver.apply("Terminal coding agent.\n", changes)
-
- expect(updated).to eq("Terminal coding agent.\n# Git\nCommit small.")
- expect(applied).to eq(["add git"])
- end
-
- it "separates an appended section with a newline when the prompt lacks a trailing one" do
- changes = [{ "old_text" => "", "new_text" => "more", "rationale" => "grow" }]
- updated, = improver.apply("seed", changes)
-
- expect(updated).to eq("seed\nmore")
- end
-
- it "grows from an empty prompt by appending" do
- changes = [
- { "old_text" => "", "new_text" => "line one", "rationale" => "first" },
- { "old_text" => "", "new_text" => "line two", "rationale" => "second" }
- ]
- updated, applied = improver.apply("", changes)
-
- expect(updated).to eq("line one\nline two")
- expect(applied).to eq(["first", "second"])
- end
-
- it "ignores an empty old_text with empty new_text so it counts as no change" do
- changes = [{ "old_text" => "", "new_text" => "", "rationale" => "noop" }]
- updated, applied = improver.apply("seed", changes)
-
- expect(updated).to eq("seed")
- expect(applied).to be_empty
- end
- end
-end
spec/evals/harness/loop_spec.rb
@@ -1,202 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.describe Elelem::Evals::Loop do
- let(:dir) { Dir.mktmpdir }
- let(:champion) { File.join(dir, "champion.erb") }
- let(:cases) { [Elelem::Evals::Case.new(id: "a", group: "g", fixture: "hello", turns: ["hi"])] }
- let(:out) { StringIO.new }
- let(:no_preflight) { -> {} }
- let(:changes) { [{ "old_text" => "original", "new_text" => "improved", "rationale" => "better" }] }
-
- before { File.write(champion, "original prompt") }
- after { FileUtils.remove_entry(dir) if File.directory?(dir) }
-
- def result(id: "a", group: "g", status: "FAIL")
- Elelem::Evals::Result.new(
- id: id, group: group, status: status, failures: status == "PASS" ? [] : ["nope"],
- turns: 1, duration: 0.1, tools: [], response: ""
- )
- end
-
- def score(*results)
- Elelem::Evals::Score.new(results: results)
- end
-
- def score_of(status)
- score(result(status: status))
- end
-
- def scoring(by_prompt)
- ->(prompt) { by_prompt.fetch(prompt) }
- end
-
- # Always proposes the same edit.
- def improver_returning(changes)
- instance_double(
- Elelem::Evals::Improver,
- plan: { "analysis" => "x", "changes" => changes },
- apply: ["improved prompt", changes.map { |c| c["rationale"] }]
- )
- end
-
- def build_loop(scorer_for:, improver: improver_returning(changes), preflight: no_preflight, budget: described_class::BUDGET)
- described_class.new(
- cases: cases, champion: champion, workdir: dir, improver: improver,
- scorer_for: scorer_for, out: out, preflight: preflight, budget: budget
- )
- end
-
- it "promotes a challenger that fixes a case" do
- build_loop(scorer_for: scoring("original prompt" => score_of("FAIL"), "improved prompt" => score_of("PASS"))).run(rounds: 1)
-
- expect(File.read(champion)).to eq("improved prompt")
- end
-
- it "rejects a challenger that regresses a case" do
- build_loop(scorer_for: scoring("original prompt" => score_of("PASS"), "improved prompt" => score_of("FAIL"))).run(rounds: 1)
-
- expect(File.read(champion)).to eq("original prompt")
- end
-
- it "stops early when everything already passes" do
- improver = improver_returning(changes)
-
- build_loop(improver: improver, scorer_for: ->(_prompt) { score_of("PASS") }).run(rounds: 3)
-
- expect(improver).not_to have_received(:plan)
- end
-
- it "writes each challenger to its own file" do
- build_loop(scorer_for: scoring("original prompt" => score_of("FAIL"), "improved prompt" => score_of("PASS"))).run(rounds: 1)
-
- expect(File).to exist(File.join(dir, "challenger-1.erb"))
- expect(File.read(File.join(dir, "challenger-1.erb"))).to eq("improved prompt")
- end
-
- it "keeps the champion unchanged and consumes every round when no change can ever be applied" do
- improver = instance_double(
- Elelem::Evals::Improver,
- plan: { "analysis" => "x", "changes" => changes },
- apply: ["improved prompt", []]
- )
-
- result = build_loop(improver: improver, scorer_for: ->(_prompt) { score_of("FAIL") }).run(rounds: 3)
-
- expect(result).to be(false)
- expect(File.read(champion)).to eq("original prompt")
- expect(File).not_to exist(File.join(dir, "challenger-1.erb"))
- expect(improver).to have_received(:plan).exactly(3).times
- end
-
- describe "size budget" do
- it "keeps the champion when the challenger is over budget" do
- loop = build_loop(scorer_for: ->(_prompt) { score_of("FAIL") }, budget: "improved prompt".length - 1)
-
- expect(loop.run(rounds: 1)).to be(false)
- expect(File.read(champion)).to eq("original prompt")
- end
-
- it "does not spend a scoring pass on a challenger that is over budget" do
- scorer = lambda do |prompt|
- raise "scored an over budget challenger" if prompt == "improved prompt"
- score_of("FAIL")
- end
-
- expect { build_loop(scorer_for: scorer, budget: 5).run(rounds: 1) }.not_to raise_error
- end
-
- it "reports the size and the cap when rejecting" do
- build_loop(scorer_for: ->(_prompt) { score_of("FAIL") }, budget: 5).run(rounds: 1)
-
- expect(out.string).to include("template 15 chars, cap 5")
- end
-
- it "records an over budget rejection in history" do
- build_loop(scorer_for: ->(_prompt) { score_of("FAIL") }, budget: 5).run(rounds: 1)
-
- entry = JSON.parse(File.read(File.join(dir, "history.jsonl")).lines.first)
-
- expect(entry).to include("promoted" => false, "reason" => "over_budget", "size" => 15)
- end
-
- it "reports the size change when promoting" do
- build_loop(scorer_for: scoring("original prompt" => score_of("FAIL"), "improved prompt" => score_of("PASS"))).run(rounds: 1)
-
- expect(out.string).to include("template 15 -> 15 chars")
- end
-
- it "prints the lines the promoted challenger changed" do
- build_loop(scorer_for: scoring("original prompt" => score_of("FAIL"), "improved prompt" => score_of("PASS"))).run(rounds: 1)
-
- expect(out.string).to include("- original prompt", "+ improved prompt")
- end
-
- it "warns when the champion itself is already over budget" do
- build_loop(scorer_for: ->(_prompt) { score_of("PASS") }, budget: 5).run(rounds: 1)
-
- expect(out.string).to include("over cap")
- end
- end
-
- it "appends a round to history" do
- build_loop(scorer_for: scoring("original prompt" => score_of("FAIL"), "improved prompt" => score_of("PASS"))).run(rounds: 1)
-
- entry = JSON.parse(File.read(File.join(dir, "history.jsonl")).lines.first)
-
- expect(entry).to include("round" => 1, "promoted" => true)
- end
-
- it "records cost in history" do
- build_loop(scorer_for: scoring("original prompt" => score_of("FAIL"), "improved prompt" => score_of("PASS"))).run(rounds: 1)
-
- entry = JSON.parse(File.read(File.join(dir, "history.jsonl")).lines.first)
-
- expect(entry).to include("turns" => 1, "duration" => 0.1)
- end
-
- it "withholds holdout failures from the improver" do
- holdout = result(id: "h", group: "holdout")
- visible = result
- improver = improver_returning(changes)
-
- build_loop(improver: improver, scorer_for: ->(_p) { score(holdout, visible) }).run(rounds: 1)
-
- expect(improver).to have_received(:plan).with(prompt: anything, failures: [visible])
- end
-
- it "does not promote a challenger that only improves a holdout case" do
- baseline = score(result, result(id: "h", group: "holdout"))
- holdout_only = score(result, result(id: "h", group: "holdout", status: "PASS"))
-
- build_loop(scorer_for: scoring("original prompt" => baseline, "improved prompt" => holdout_only)).run(rounds: 1)
-
- expect(File.read(champion)).to eq("original prompt")
- end
-
- it "does not promote a challenger that regresses a holdout case" do
- baseline = score(result, result(id: "h", group: "holdout", status: "PASS"))
- regressed = score(result(status: "PASS"), result(id: "h", group: "holdout"))
-
- build_loop(scorer_for: scoring("original prompt" => baseline, "improved prompt" => regressed)).run(rounds: 1)
-
- expect(File.read(champion)).to eq("original prompt")
- end
-
- it "sends the improver only one failure per case even when every repeat fails" do
- fails = Array.new(3) { result }
- improver = improver_returning(changes)
-
- build_loop(improver: improver, scorer_for: ->(_p) { score(*fails) }).run(rounds: 1)
-
- expect(improver).to have_received(:plan).with(prompt: anything, failures: [fails.first])
- end
-
- it "raises before scoring anything when the preflight check fails" do
- loop = build_loop(
- scorer_for: ->(_prompt) { raise "scoring should not have run" },
- preflight: -> { raise "connection refused" }
- )
-
- expect { loop.run(rounds: 1) }.to raise_error(/connection refused/)
- end
-end
spec/evals/harness/null_terminal_spec.rb
@@ -1,10 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.describe Elelem::Evals::NullTerminal do
- it "auto-allows tools the shipped rules would otherwise ask about" do
- terminal = described_class.new
-
- expect(terminal).not_to be_interactive
- expect(Elelem::Permissions.new.check("execute", {}, terminal: terminal)).to be(true)
- end
-end
spec/evals/harness/runner_spec.rb
@@ -1,90 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.describe Elelem::Evals::Runner do
- # Replies with a write tool call on the first fetch, then plain text.
- class ScriptedClient
- def initialize
- @calls = 0
- end
-
- def fetch(_messages, _tools = [], &block)
- @calls += 1
- return first_turn(&block) if @calls == 1
-
- block.call(type: "saying", text: "done, wrote goodbye.rb")
- []
- end
-
- private
-
- def first_turn(&block)
- call = { id: "1", name: "write", arguments: { "path" => "goodbye.rb", "content" => "puts 'Goodbye'\n" } }
- block.call(call.merge(type: "doing"))
- [call]
- end
- end
-
- let(:kase) do
- Elelem::Evals::Case.new(
- id: "writes-a-file",
- group: "editing",
- fixture: "hello",
- turns: ["write goodbye.rb"],
- expect: {
- verify: "ruby goodbye.rb",
- files: { "goodbye.rb" => { "contains" => ["Goodbye"] } },
- response_contains: ["goodbye.rb"],
- tools_used: [{ "write" => { "path" => "goodbye.rb" } }],
- tools_not_used: ["read"],
- max_turns: 5
- }
- )
- end
-
- subject(:runner) { described_class.new(prompt: "test prompt", client: -> { ScriptedClient.new }) }
-
- it "passes a case the agent satisfies" do
- result = runner.run(kase)
-
- expect(result.failures).to be_empty
- expect(result.status).to eq("PASS")
- end
-
- it "records the case identity" do
- result = runner.run(kase)
-
- expect(result.id).to eq("writes-a-file")
- expect(result.group).to eq("editing")
- end
-
- it "records the tool calls" do
- result = runner.run(kase)
-
- expect(result.tools.map(&:name)).to eq(["write"])
- end
-
- it "counts turns" do
- expect(runner.run(kase).turns).to eq(2)
- end
-
- it "fails a case whose assertions do not hold" do
- failing = Elelem::Evals::Case.new(
- id: "wants-more",
- fixture: "hello",
- turns: ["write goodbye.rb"],
- expect: { response_contains: ["never said this"] }
- )
-
- result = runner.run(failing)
-
- expect(result.status).to eq("FAIL")
- expect(result.failures.first).to include("response missing")
- end
-
- it "leaves the working directory where it found it" do
- before = Dir.pwd
- runner.run(kase)
-
- expect(Dir.pwd).to eq(before)
- end
-end
spec/evals/harness/scorer_spec.rb
@@ -1,146 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.describe Elelem::Evals::Scorer do
- def result(id, status)
- Elelem::Evals::Result.new(
- id: id, group: "g", status: status, failures: status == "PASS" ? [] : ["nope"],
- turns: 1, duration: 0.1, tools: [], response: ""
- )
- end
-
- # Returns the statuses it was given, in order, one per run call.
- class FakeRunner
- def initialize(statuses)
- @statuses = statuses.dup
- end
-
- def run(kase)
- status = @statuses.shift
- Elelem::Evals::Result.new(
- id: kase.id, group: kase.group, status: status, failures: status == "PASS" ? [] : ["nope"],
- turns: 1, duration: 0.1, tools: [], response: ""
- )
- end
- end
-
- let(:kase) { Elelem::Evals::Case.new(id: "a", group: "g", fixture: "hello", turns: ["hi"]) }
-
- it "passes a case only when every repeat passes" do
- scorer = described_class.new(runner: FakeRunner.new(%w[PASS PASS PASS]), repeat: 3)
-
- expect(scorer.call([kase]).passed).to eq(1)
- end
-
- it "fails a case when any repeat fails" do
- scorer = described_class.new(runner: FakeRunner.new(%w[PASS FAIL PASS]), repeat: 3)
- score = scorer.call([kase])
-
- expect(score.passed).to eq(0)
- expect(score.failed).to eq(1)
- end
-
- it "keeps every result" do
- scorer = described_class.new(runner: FakeRunner.new(%w[PASS PASS PASS]), repeat: 3)
-
- expect(scorer.call([kase]).results.length).to eq(3)
- end
-
- # A single failure decides a case, so there is no reason to spend the remaining
- # repeats -- this is what keeps a failing case cheap during overnight rounds.
- class CountingRunner
- attr_reader :calls
-
- def initialize(statuses)
- @statuses = statuses.dup
- @calls = 0
- end
-
- def run(kase)
- @calls += 1
- status = @statuses.shift
- Elelem::Evals::Result.new(
- id: kase.id, group: kase.group, status: status, failures: status == "PASS" ? [] : ["nope"],
- turns: 1, duration: 0.1, tools: [], response: ""
- )
- end
- end
-
- it "stops repeating a case as soon as one repeat fails" do
- runner = CountingRunner.new(%w[FAIL PASS PASS])
- described_class.new(runner: runner, repeat: 3).call([kase])
-
- expect(runner.calls).to eq(1)
- end
-
- it "runs every repeat while the case keeps passing" do
- runner = CountingRunner.new(%w[PASS PASS PASS])
- described_class.new(runner: runner, repeat: 3).call([kase])
-
- expect(runner.calls).to eq(3)
- end
-
- it "reports only failing results as failures" do
- scorer = described_class.new(runner: FakeRunner.new(%w[PASS FAIL PASS]), repeat: 3)
-
- expect(scorer.call([kase]).failures.length).to eq(1)
- end
-
- it "detects a regression against an earlier score" do
- before = described_class.new(runner: FakeRunner.new(["PASS"]), repeat: 1).call([kase])
- after = described_class.new(runner: FakeRunner.new(["FAIL"]), repeat: 1).call([kase])
-
- expect(after.regressions_from(before)).to eq(["g/a"])
- end
-
- it "does not call an improvement a regression" do
- before = described_class.new(runner: FakeRunner.new(["FAIL"]), repeat: 1).call([kase])
- after = described_class.new(runner: FakeRunner.new(["PASS"]), repeat: 1).call([kase])
-
- expect(after.regressions_from(before)).to be_empty
- end
-
- it "keeps cases with the same id in different groups apart" do
- a = Elelem::Evals::Case.new(id: "x", group: "search", fixture: "hello", turns: ["hi"])
- b = Elelem::Evals::Case.new(id: "x", group: "json", fixture: "hello", turns: ["hi"])
- scorer = described_class.new(runner: FakeRunner.new(%w[PASS FAIL]), repeat: 1)
-
- score = scorer.call([a, b])
-
- expect(score.total).to eq(2)
- expect(score.passed).to eq(1)
- end
-
- describe "cost" do
- def counted(turns, duration)
- Elelem::Evals::Result.new(
- id: "a", group: "g", status: "PASS", failures: [], turns: turns, duration: duration, tools: [], response: ""
- )
- end
-
- it "sums turns across results" do
- score = Elelem::Evals::Score.new(results: [counted(2, 0.1), counted(3, 0.2)])
-
- expect(score.turns).to eq(5)
- end
-
- it "sums duration across results" do
- score = Elelem::Evals::Score.new(results: [counted(2, 0.1), counted(3, 0.25)])
-
- expect(score.duration).to eq(0.35)
- end
- end
-
- describe "#excluding" do
- it "filters out every result from the given group" do
- holdout = Elelem::Evals::Result.new(
- id: "h", group: "holdout", status: "PASS", failures: [], turns: 1, duration: 0.1, tools: [], response: ""
- )
- score = Elelem::Evals::Score.new(results: [result("a", "PASS"), holdout])
-
- excluded = score.excluding("holdout")
-
- expect(excluded.total).to eq(1)
- expect(excluded.results).to eq([result("a", "PASS")])
- end
- end
-end
spec/evals/harness/tasks_spec.rb
@@ -1,31 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.describe "Elelem::Evals tasks" do
- let(:dir) { Dir.mktmpdir }
- let(:out) { StringIO.new }
-
- after { FileUtils.remove_entry(dir) if File.directory?(dir) }
-
- describe ".minimize!" do
- let(:champion) { File.join(dir, "champion.erb") }
- let(:ablation) do
- Elelem::Evals::Ablator::Ablation.new(prompt: "KEEP\n", defends: { "KEEP\n" => ["g/a"] })
- end
- let(:ablator) { instance_double(Elelem::Evals::Ablator, minimize: ablation) }
-
- before { File.write(champion, "KEEP\nDEAD\n") }
-
- it "writes the minimized prompt and the defends map" do
- Elelem::Evals.minimize!(champion: champion, workdir: dir, ablator: ablator, out: out)
-
- expect(File.read(File.join(dir, "minimized.erb"))).to eq("KEEP\n")
- expect(JSON.parse(File.read(File.join(dir, "defends.json")))).to eq("KEEP\n" => ["g/a"])
- end
-
- it "minimizes the champion contents" do
- Elelem::Evals.minimize!(champion: champion, workdir: dir, ablator: ablator, out: out)
-
- expect(ablator).to have_received(:minimize).with("KEEP\nDEAD\n")
- end
- end
-end
spec/evals/harness/workspace_spec.rb
@@ -1,62 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.describe Elelem::Evals::Workspace do
- it "copies the fixture into a fresh directory" do
- described_class.open(fixture: "hello") do |workspace|
- expect(workspace.read("hello.rb")).to include("greet")
- end
- end
-
- it "leaves the fixture free of harness files" do
- described_class.open(fixture: "hello") do |workspace|
- expect(Dir.children(workspace.path)).to contain_exactly("hello.rb")
- end
- end
-
- it "runs setup commands in the workspace" do
- described_class.open(fixture: "hello", setup: ["echo hi > setup.txt"]) do |workspace|
- expect(workspace.read("setup.txt")).to eq("hi\n")
- end
- end
-
- it "removes the directory afterwards" do
- path = described_class.open(fixture: "hello", &:path)
-
- expect(File).not_to exist(path)
- end
-
- it "strips bundler variables inside chdir" do
- saved = ENV["BUNDLE_GEMFILE"]
- ENV["BUNDLE_GEMFILE"] = "/somewhere/Gemfile"
-
- described_class.open(fixture: "hello") do |workspace|
- workspace.chdir { expect(ENV["BUNDLE_GEMFILE"]).to be_nil }
- end
-
- expect(ENV["BUNDLE_GEMFILE"]).to eq("/somewhere/Gemfile")
- ensure
- saved ? (ENV["BUNDLE_GEMFILE"] = saved) : ENV.delete("BUNDLE_GEMFILE")
- end
-
- it "returns nil for a file that does not exist" do
- described_class.open(fixture: "hello") do |workspace|
- expect(workspace.read("nope.rb")).to be_nil
- end
- end
-
- it "raises SetupFailed when a setup command fails" do
- expect do
- described_class.open(fixture: "hello", setup: ["exit 1"]) { |workspace| workspace }
- end.to raise_error(described_class::SetupFailed)
- end
-
- it "still removes the directory when a setup command fails" do
- before = Dir.glob(File.join(Dir.tmpdir, "elelem-evals-*"))
-
- expect do
- described_class.open(fixture: "hello", setup: ["exit 1"]) { |workspace| workspace }
- end.to raise_error(described_class::SetupFailed)
-
- expect(Dir.glob(File.join(Dir.tmpdir, "elelem-evals-*"))).to eq(before)
- end
-end
spec/evals/prompts/seed.erb
@@ -1,1 +0,0 @@
-<%= agents_md %>
spec/evals/support/ablator.rb
@@ -2,10 +2,6 @@
module Elelem
module Evals
- # The "refactor" half of red-green-refactor: once the prompt is green, drop
- # every line whose removal regresses nothing. What survives is the minimal
- # set of lines, each load-bearing for some case. Deterministic; it never
- # invents text, so it does not depend on a weak improver's judgement.
class Ablator
Ablation = Data.define(:prompt, :defends)
@@ -27,10 +23,6 @@ module Elelem
private
- # Scans every ablatable line, scoring its removal once. Returns as soon as a
- # line is removable, as [[prompt_without_it, its_score], nil]. If none can be
- # removed (a fixpoint), returns [nil, defends] where defends is the
- # line -> cases-it-protects map the scan just built for free.
def remove_one(prompt, score)
lines = prompt.lines
defends = {}
@@ -49,8 +41,6 @@ module Elelem
[nil, defends]
end
- # ERB tags render the environment and blank lines carry the layout; neither
- # defends a case, so exclude both from ablation.
def ablatable?(line)
!line.strip.empty? && !line.include?("<%")
end
spec/evals/support/assertions.rb
@@ -7,9 +7,6 @@ module Elelem
@expect = expect || {}
end
- # Scores correctness only -- files, response, tools. Turn count is NOT
- # scored: max_turns is a generous safety ceiling (a hard stop against
- # runaway loops), not a speed gate, so a correct-but-slow run still passes.
def failures(workspace:, response:, tools:)
[
verify_failure(workspace),
spec/evals/support/case.rb
@@ -6,8 +6,6 @@ module Elelem
module Evals
class Case
CASES = File.expand_path("../cases", __dir__)
- # A generous safety ceiling so a runaway loop still stops, not a speed gate.
- # Correctness (files/response/tools) decides pass/fail, not turn count.
DEFAULT_MAX_TURNS = 30
def self.load_all(dir = CASES)
spec/evals/support/client.rb
@@ -10,23 +10,19 @@ module Elelem
Elelem::Net::Ollama.new(model: model, host: host, keep_alive: "30m", **params)
end
- # In-process GGUF model, run greedy (temp 0) for deterministic eval scoring.
- # Memoized: the runner asks for a client per case, but generation is stateless
- # (fresh context each call), so one resident model serves every case -- loading
- # a fresh 4GB+ model per case would exhaust GPU/unified memory.
- def self.gguf(model_path: ENV.fetch("GGUF_MODEL"))
+ def self.gguf(model: ENV.fetch("GGUF_MODEL"))
@gguf ||= begin
gpu = %w[vulkan cuda metal].include?(Elelem::Net::GGUF.backend)
Elelem::Net::GGUF.new(
- model_path: File.expand_path(model_path),
+ model: File.expand_path(model),
n_ctx: Integer(ENV.fetch("GGUF_N_CTX", "8192")),
n_gpu_layers: Integer(ENV.fetch("GGUF_N_GPU_LAYERS", gpu ? "999" : "0")),
- temp: 0.0, seed: 42
+ temp: 0.0,
+ seed: 42
)
end
end
- # EVALS_PROVIDER=gguf points the runner at the local model instead of Ollama.
def self.client(model: MODEL)
return gguf if ENV["EVALS_PROVIDER"] == "gguf"
spec/evals/support/improver.rb
@@ -8,30 +8,18 @@ module Elelem
class Improver
SYSTEM = <<~PROMPT
- You are a prompt engineer improving the system prompt of elelem, a terminal
- coding agent. The agent's tools are: read(path), write(path, content),
- edit(path, old, new), execute(command), grep(pattern), glob(pattern),
- list(path), git(command), task(prompt) and verify(path).
-
- You are shown the current prompt and the eval cases it failed. Suggest
- targeted find/replace edits to the prompt that would fix those failures.
-
- Rules:
- - Each change must use an exact old_text string that appears in the prompt.
- - To add a NEW section, set old_text to "" and put the whole section in
- new_text; it is appended to the end of the prompt.
- - Keep changes minimal. Do not rewrite whole sections.
- - Do not remove or alter the ERB tags, they render the environment.
- - Do not change what is already working.
- - Focus on the highest impact failures first.
+ You are a prompt engineer improving the system prompt of a terminal coding agent.
+
+ You are shown the current prompt and the eval cases it failed. Suggest targeted find/replace edits to the prompt that would fix those failures.
Respond with JSON in this exact format:
+
+ ```json
{
"analysis": "what is failing and why",
- "changes": [
- {"old_text": "exact text from the prompt", "new_text": "replacement", "rationale": "why this helps"}
- ]
+ "changes": [{ "old_text": "exact text from the prompt", "new_text": "replacement", "rationale": "why this helps" }]
}
+ ```
Return an empty changes array if no edit would help.
PROMPT
@@ -56,8 +44,6 @@ module Elelem
old_text = change["old_text"].to_s
new_text = change["new_text"].to_s
- # An empty old_text means "grow the prompt": append the new section.
- # This is what lets the loop build a prompt up from an empty seed.
if old_text.empty?
next text if new_text.empty?
@@ -68,7 +54,6 @@ module Elelem
next text unless text.include?(old_text)
applied << change["rationale"].to_s
- # Block form, so \0 and \& in the model's text stay literal.
text.sub(old_text) { new_text }
end
spec/evals/support/runner.rb
@@ -10,11 +10,8 @@ module Elelem
end
end
- # The prompt under test is the one elelem actually ships, not a copy that
- # can drift from it. Challengers and history stay out of lib/.
CHAMPION = File.expand_path("../../../lib/elelem/prompts/default.erb", __dir__)
WORKDIR = File.expand_path("../prompts", __dir__)
- SEED = File.join(WORKDIR, "seed.erb")
class Runner
def initialize(prompt:, client: -> { Evals.client })
spec/evals/support/scorer.rb
@@ -53,8 +53,6 @@ module Elelem
private
- # A case passes only if every repeat passes, so the first failure settles
- # it -- stop early and spend no more runs on a case already known to fail.
def runs_for(kase)
results = []
@repeat.times do
spec/evals/support/tasks.rb
@@ -21,7 +21,7 @@ module Elelem
ablation
end
- def self.regenerate(rounds:, seed: SEED, workdir: WORKDIR, out: $stdout, loop_for: ->(champion) { Loop.new(champion: champion, out: out) })
+ def self.regenerate(rounds:, seed:, workdir: WORKDIR, out: $stdout, loop_for: ->(champion) { Loop.new(champion: champion, out: out) })
FileUtils.mkdir_p(workdir)
candidate = File.join(workdir, "candidate.erb")
FileUtils.cp(seed, candidate)
spec/evals/cases_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-RSpec.describe "eval cases", :eval do
+RSpec.describe "eval cases" do
runner = Elelem::Evals::Runner.new(prompt: File.read(Elelem::Evals::CHAMPION))
Elelem::Evals::Case.load_all.each do |kase|