Gem Scout
Gem Scout: describe what you need, get a ranked shortlist of gems. Search and scoring are separate capabilities; the search backend is the pluggable seam - offline it's a bundled index, online swap in the real WebSearch DuckDuckGo backend with one assignment.
Developer Experience
Round 2
Andrew Kane
exit 0
bundle exec ruby examples/gem_scout.rb
a real captured run
GEM SCOUT: "background jobs" -> sidekiq 97.4 widely adopted (950M downloads); recently released (20d ago) good_job 70.5 recently released (14d ago) solid_queue 60.2 recently released (30d ago) recommendation: start with sidekiq - background jobs backed by Redis, threads not forks
source
# frozen_string_literal: true # Gem Scout: describe what you need, get a ranked shortlist of gems. # Search and scoring are separate capabilities; the search backend is # the pluggable seam - offline it's a bundled index, online swap in # the real WebSearch DuckDuckGo backend with one assignment. # # bundle exec ruby examples/gem_scout.rb "background jobs" # bundle exec ruby examples/gem_scout.rb "vector search" # # Runs offline by default. require class="s">"bundler/setup" require class="s">"agentic" # A small index standing in for the network - same shape a live search # backend returns, so the rest of the program can't tell the difference CATALOG = [ {name: class="s">"sidekiq", summary: class="s">"background jobs backed by Redis, threads not forks", topics: %w[background jobs queue async workers], downloads_m: 950, last_release_days: 20}, {name: class="s">"solid_queue", summary: class="s">"database-backed background jobs for Active Job", topics: %w[background jobs queue rails database], downloads_m: 15, last_release_days: 30}, {name: class="s">"good_job", summary: class="s">"Postgres-based Active Job backend with dashboard", topics: %w[background jobs queue rails postgres], downloads_m: 40, last_release_days: 14}, {name: class="s">"neighbor", summary: class="s">"nearest neighbor vector search for Rails and Postgres", topics: %w[vector search embeddings pgvector similarity], downloads_m: 8, last_release_days: 45}, {name: class="s">"pgvector", summary: class="s">"pgvector support for Ruby", topics: %w[vector search embeddings postgres], downloads_m: 12, last_release_days: 60}, {name: class="s">"searchkick", summary: class="s">"intelligent search made easy with Elasticsearch/OpenSearch", topics: %w[search elasticsearch full-text ranking], downloads_m: 130, last_release_days: 90}, {name: class="s">"pagy", summary: class="s">"the fastest pagination gem", topics: %w[pagination performance views], downloads_m: 85, last_release_days: 10}, {name: class="s">"strong_migrations", summary: class="s">"catch unsafe migrations in development", topics: %w[migrations database safety postgres], downloads_m: 70, last_release_days: 25} ].freeze # Offline backend for the WebSearch seam built in round 1 Agentic:class="y">:Capabilities:class="y">:WebSearch.backend = lambda do |query:, num_results:| terms = query.downcase.split hits = CATALOG.map { |gem| haystack = class="s">"#{gem[class="y">:name]} #{gem[class="y">:summary]} #{gem[class="y">:topics].join(" class="s">")}" score = terms.count { |t| haystack.include?(t) } [gem, score] }.select { |_, s| s.positive? }.sort_by { |g, s| [-s, -g[class="y">:downloads_m]] }.first(num_results) { results: hits.map { |gem, _| class="s">"#{gem[class="y">:name]}: #{gem[class="y">:summary]}" }, sources: hits.map { |gem, _| class="s">"https://rubygems.org/gems/#{gem[class="y">:name]}" } } end # Scoring is its own capability: search finds candidates, this ranks # them on the things that matter when you have to live with a gem spec = Agentic:class="y">:CapabilitySpecification.new( name: class="s">"score_gem", description: class="s">"Score a gem on adoption and maintenance", version: class="s">"1.0.0", inputs: {name: {type: class="s">"string", required: true}}, outputs: {score: {type: class="s">"number", required: true}, notes: {type: class="s">"array", required: true}} ) Agentic.register_capability(spec, Agentic:class="y">:CapabilityProvider.new( capability: spec, implementation: ->(inputs) { gem = CATALOG.find { |g| g[class="y">:name] == inputs[class="y">:name] } or next({score: 0.0, notes: [class="s">"unknown gem"]}) adoption = Math.log10([gem[class="y">:downloads_m], 1].max) / 3.0 # 0..1 for 1M..1B freshness = [1.0 - gem[class="y">:last_release_days] / 365.0, 0].max notes = [] notes << class="s">"widely adopted (#{gem[class="y">:downloads_m]}M downloads)" if gem[class="y">:downloads_m] > 50 notes << class="s">"recently released (#{gem[class="y">:last_release_days]}d ago)" if gem[class="y">:last_release_days] < 31 notes << class="s">"check maintenance cadence" if gem[class="y">:last_release_days] > 80 {score: ((adoption * 0.6 + freshness * 0.4) * 100).round(1), notes: notes} } )) scout = Agentic:class="y">:Agent.build { |a| a.name = class="s">"GemScout" } Agentic:class="y">:Capabilities.register_standard_capabilities scout.add_capability(class="s">"web_search") scout.add_capability(class="s">"score_gem") need = ARGV.join(class="s">" ") need = class="s">"background jobs" if need.empty? found = scout.execute_capability(class="s">"web_search", {query: need, num_results: 4}) candidates = found[class="y">:results].map { |line| line.split(class="s">":").first } ranked = candidates.map { |name| verdict = scout.execute_capability(class="s">"score_gem", {name: name}) {name: name, score: verdict[class="y">:score], notes: verdict[class="y">:notes]} }.sort_by { |c| -c[class="y">:score] } puts class="s">"GEM SCOUT: \"#{need}\class="s">"" puts if ranked.empty? puts class="s">" no candidates found - try different words, or plug in the live backend:" puts class="s">" Agentic:class="y">:Capabilities:class="y">:WebSearch.backend = Agentic:class="y">:Capabilities:class="y">:WebSearch:class="y">:DuckDuckGo.new" else ranked.each_with_index do |c, i| marker = (i == 0) ? class="s">"->" : class="s">" " puts format(class="s">"%s %-18s %5.1f %s", marker, c[class="y">:name], c[class="y">:score], c[class="y">:notes].join(class="s">"; ")) end puts winner = ranked.first puts class="s">"recommendation: start with #{winner[class="y">:name]} - " \ class="s">"#{CATALOG.find { |g| g[class="y">:name] == winner[class="y">:name] }[class="y">:summary]}" end