Language guide

AgentSpaces for Ruby

Write agents as Ruby classes, with one class macro above each method, and run them as full members of an AgentSpaces fleet. A pure-Ruby peer dials a seed over tcp, tls, WebSocket, or HTTP, and on JRuby the same classes bind through the Java binder.

Ruby 3.3 or newer with OpenSSL 1.1.1 or newerone runtime dependency, loggerdials tcp, tls, ws, wss, http, and httpsJRuby face for every transportfive gems at 0.5.0, installed from a clone
aspace://guide/ruby/intro

Introduction

AgentSpaces lets programs coordinate through shared, replicated memory with no broker or scheduler in the middle. This part walks you from an empty directory to two Ruby processes that share work, survive a kill, and take part in a vote.

A few words come up on every page:

  • A peer is one running process in the fleet. It holds its own Ed25519 identity and signs what it writes.
  • A group is the set of peers that trust each other. They share a founding text or a signed founding document.
  • A space is a named, replicated store inside a group, such as orders.
  • An entry is a typed value in a space. In Ruby it is a Data class.
  • A lease is how long a value lives, or how long a peer may hold an entry it took.
  • A take claims one entry for one peer. If that peer dies, the lease lapses and the entry reappears.
  • An agent is a class whose methods take, watch, or vote on entries. The fleet sees it through its published card.
  • A capability is a fleet service a peer advertises, such as vote counting or a fleet-wide average.

Install

You need Ruby 3.3 or newer, built against OpenSSL 1.1.1 or newer. The macOS system Ruby (2.6, on LibreSSL) cannot run the gem. Install a current Ruby with Homebrew, rbenv, or mise.

Homebrew installs it as /opt/homebrew/opt/ruby/bin/ruby, so put that directory first on PATH. The agentspaces gems are not yet on RubyGems, so run them from a clone of agentspaces-rb. The core gem needs no native extension and no other gem at run time beyond logger.

brew install ruby
git clone https://github.com/badmonkeyai/agentspaces-rb
export RUBYLIB=$PWD/agentspaces-rb/gems/agentspaces/lib
ruby agentspaces-rb/gems/agentspaces/exe/agentspaces doctor

doctor runs every crypto primitive the peer uses and names the OpenSSL build. A good result ends in five ok lines:

ok       Ed25519
ok       Ed25519 PKCS#8
ok       X25519
ok       HKDF-SHA-256
ok       HMAC-SHA-256

For a Bundler project, point the Gemfile at the clone with a path: option until the gems are published.

Start a seed

A Ruby peer dials out and does not listen, so it needs a seed: any peer with a listener that the others dial first. The Node service host is a seed with no code. Build it from a clone of agentspaces-typescript: run npm install and npm run build at the root, then npm run build in packages/node.

# seed.yaml
agentspaces:
  security:
    profile: dev-local          # signed frames on loopback TCP; never on a shared network
  bind: 127.0.0.1:7700
  tick-millis: 250
  groups:
    - name: shop
      founding: shop-v1
      spaces: [ orders ]
node agentspaces-typescript/packages/node/build/src/cli.js run seed.yaml

The dev-local profile signs every frame over plain loopback TCP and encrypts nothing. Use it on a laptop, never on a shared network. Production fleets use mtls, and a Ruby peer reaches them through a tls://host:port seed.

Entries

AgentSpaces::Entry.define makes a Data class and registers it under a schema name. The schema name is what every language on the wire agrees on.

# fulfiller.rb
Order    = AgentSpaces::Entry.define("shop.Order#v1",    order_id: String, item: String)
Shipment = AgentSpaces::Entry.define("shop.Shipment#v1", order_id: String, item: String, by: String)
  • Members are snake_case in Ruby and travel under camelCase wire names, so order_id is the Java record component orderId.
  • A field type is String, Integer, Float, :boolean, :bytes, :instant, an Array of one type such as [String], Hash, Object, or another entry class.
  • A Float field widens an Integer on assignment, which is what a Java double needs.
  • An unknown member raises ArgumentError and names the fields the entry has.

Entries are ordinary Data values, so ==, inspect, and pattern matching (in { winner: "yes" }) work as usual.

Your first agent

An agent is a class that includes AgentSpaces::Agent. The agent macro names it. Each verb macro, such as take, binds to the next def in the class, and the method's return value is the next entry in the space.

# fulfiller.rb
require "agentspaces"

Order    = AgentSpaces::Entry.define("shop.Order#v1",    order_id: String, item: String)
Shipment = AgentSpaces::Entry.define("shop.Shipment#v1", order_id: String, item: String, by: String)

class Fulfiller
  include AgentSpaces::Agent
  agent "fulfiller", description: "Ships orders"

  take Order, space: "orders", lease: "30s", produces: Shipment
  def ship(order) = Shipment.new(order_id: order.order_id, item: order.item, by: "fulfiller")
end

fleet = { group: "shop", founding: "shop-v1", seeds: ["127.0.0.1:7700"] }

if ARGV.empty?
  AgentSpaces.serve(**fleet) { |peer| peer.bind(Fulfiller.new) }
else
  AgentSpaces.start(**fleet) do |peer|
    peer.bind(Fulfiller.new)
    orders = peer.space("orders")
    orders << Order.new(order_id: "o-1", item: ARGV[0])
    puts orders.read(Shipment, timeout: "30s").inspect
    peer.close
  end
end
  • take Order, space: "orders", lease: "30s" takes each Order written to orders and holds it for 30 seconds while ship runs.
  • AgentSpaces.serve starts a peer, yields it so you can bind agents, and blocks until SIGINT or SIGTERM. Then it unbinds, drains, and closes.
  • AgentSpaces.start starts a peer and yields it. Use it for scripts that write, read, and exit.
  • orders << value writes an entry, and orders.read(Shipment, timeout: "30s") waits for one to appear.

Run two instances and kill one

Start the seed, then run the file twice in two terminals. The first instance only ships. The second also writes an order, prints the shipment, and exits.

ruby fulfiller.rb
ruby fulfiller.rb kite
# => #<data Shipment order_id="o-1", item="kite", by="fulfiller">

Either instance can take the order, because both run the same agent. To watch a take recover from a crash:

  1. Add puts "shipping #{order.order_id}" and sleep 10 at the top of ship, written as a multi-line def.
  2. Start both instances, so the second writes the order.
  3. Run kill -9 on whichever instance prints shipping.
  4. If you killed the first, the second prints the shipment. If you killed the second, the first prints shipping o-1 again once the 30-second lease lapses.

The live peer finishes the order without a restart, because the lease on the take lapsed and the entry reappeared.

A second verb: notify and bid

take is exclusive: one peer gets each entry. notify runs a method on every matching entry that appears, without taking it. bid prices an entry before the take; when several peers claim the same entry, the claim with the lowest bid takes it. The agent below, from the core test suite, uses all three. A macro can also take a block in place of a def, as bid does here.

# agent_test.rb
module Shop
  Task = AgentSpaces::Entry.define("com.acme.Task#v1", topic: String, priority: Integer)
  Finding = AgentSpaces::Entry.define("com.acme.Finding#v1", topic: String, summary: String)
end

class Researcher
  include AgentSpaces::Agent
  agent "researcher", description: "Researches topics", goals: ["research"]
  # ...
  take Shop::Task, space: "tasks", lease: "1m", poll_timeout: "500ms", where: { priority: 1.. },
                   produces: Shop::Finding
  def research(task)
    # ...
    Shop::Finding.new(topic: task.topic, summary: "researched #{task.topic}")
  end

  notify Shop::Finding, space: "tasks"
  def audit(finding) = (@audited << finding.summary) && nil

  bid(Shop::Task, space: "tasks") { |task| task.priority > 3 ? 1.0 : 10.0 }
end
  • where: { priority: 1.. } limits the take to entries whose priority is at least 1. A Range, an Array, a plain value, or a clause string such as "priority>=3" all work.
  • A notify method that returns nil writes nothing.
  • Binding the class publishes its card, so other peers can find the research and audit actions.

Where to read next

aspace://guide/ruby/features

Key features

The pieces in this part are specific to Ruby: the class macros, the Ruby idioms in the Space API, the two runtimes, and the hosts and AI gems.

Class macros bound to the next def

Every verb is a macro whose options are the Java annotation attributes in snake_case. The macro records a clause, and the next def in the class becomes the method that clause calls.

MacroThe method receivesRuns on
take Type, space:the taken entryMRI and JRuby
notify Type, space:, on:each matching entry (WRITTEN by default)MRI and JRuby
bid Type, space:an entry to price, returns a numberMRI and JRuby
ballot space:, prefix:a Votes::Proposal, returns an option or nilMRI and JRuby
on_decision space:, prefix:a Votes::Decision once the quorum closesMRI and JRuby
propose Type, space:, vote:, options:, quorum:an entry, returns the questionMRI and JRuby
join space:, key:, parts:a Joined once every part is presentMRI (mode: "local"), JRuby (all modes)
on_estimate epochs:an aggregate estimate once it settlesMRI and JRuby
ordered_take Type, space:an entry claimed through the ordered logJRuby only
reduce Type, space:an entry folded into an accumulatorJRuby only

space_ref and capability declare readers that the binder fills with a space or a capability client at bind time.

A method can return one entry, an Array of entries, AgentSpaces::Tagged.of(entry, region: "eu") for a tagged result, an AgentSpaces::Motion to open a vote, or nil for no result. A method that raises leaves its take to lapse, and the binder counts the failure.

Every macro checks its options when the class body evaluates. A misspelled option, a duration that does not parse, a where: key that names no field, or a macro with no def below it raises AgentSpaces::DefinitionError with the file and line. For scripts and tests, AgentSpaces.agent("name") { take(...) { |order| ... } } builds an agent from a block.

The Space API in Ruby

peer.space("orders") returns a space. Its verbs take an entry class and keyword options, and some take a block.

# space_test.rb
space.write(Shop::Task.new(topic: "eu-task", priority: 1), tags: { region: "eu" })
space << Shop::Task.new(topic: "plain", priority: 2)
# ...
assert_equal "plain", view.read(Shop::Task, where: { priority: 2 }).topic
# ...
finding = space.take(Shop::Task, where: { priority: 5.. }, lease: "1m", timeout: "3s") do |task|
  # ...
  Shop::Finding.new(topic: task.topic, summary: "done")
end
  • write and << write an entry. write also takes lease: and tags:.
  • read waits for a matching entry, all returns every match now, and watch calls a block on each event.
  • take with a block completes the take with the block's result. Without a block, it returns a Taken; call complete(result) on it when you finish.
  • A Regexp or a lambda in where: runs on this replica only. A Regexp runs under a timeout.
Note. Numbers are seconds. lease: 30 is thirty seconds, as with sleep. Strings such as "250ms", "10m", and "PT10M" mean what they mean in Java. The Python client reads a bare integer as milliseconds, so check the units when you port code.

Starting a peer: start, serve, and the fleet file

AgentSpaces.start and AgentSpaces.serve accept three forms of configuration:

  • Keyword shorthand for one group: group:, founding:, seeds:, spaces:, and join: for a group founded with a signed document.
  • A path to a YAML file, or YAML text.
  • A Hash in the same shape.

The YAML is the same agentspaces: subtree the Spring starter, Micronaut, Groovy, and the Node host read, in kebab-case or snake_case keys. The Ruby peer dials out only, so it logs once and ignores the listener keys bind, advertise, and roles.

On a network, the peer behaves as follows:

  • A tls://host:port seed dials TLS 1.3 and attests the remote end with its identity key. It has been tested against a Java seed that requires attestation.
  • A ws:// or wss:// seed dials WebSocket with the aspace.1 subprotocol (default path /aspace). An http:// or https:// seed runs the long-poll exchange (default path /aspace/h) for networks that pass only HTTP.
  • On a connection the transport cannot attest (tcp, ws, http), the peer runs session attestation with a member that offers it: one signed hello each way, then a shared key tags every frame.
  • A plain tcp, ws, or http seed on a non-loopback host is refused unless the configuration sets allow_plain: true.
  • A lost connection re-dials every 5 seconds for 10 minutes, failing over across the seeds.
  • keystore: keeps the identity in peer.key and peer.pub, the layout a Java peer reads. Without it, each run gets a fresh identity.
  • A forked child closes the connections it inherited. Using an inherited peer raises AgentSpaces::ForkError; call AgentSpaces.after_fork in the child.

See security in the developer guide for the profiles and keys.

The MRI wire peer and the JRuby face

Ruby has two ways into a fleet, and both run the same agent classes.

agentspaces on MRIagentspaces-jruby on JRuby
What it isa pure-Ruby wire peer, byte-identical to the Java codec on every golden vectorthe Java binder of agentspaces-runtime, driven from Ruby
Transportsdials tcp, tls, ws, wss, http, and httpsevery transport the Java node offers, and a listener
Verbstake, notify, bid, ballot, decision, propose, local join, estimateall of those, plus ordered_take, reduce, and leased and ordered joins
NeedsRuby 3.3 or newer and OpenSSLJRuby, a JDK 21 or newer, and the jars rake jruby:jars vendors

The MRI peer has no listener, so a Ruby process cannot be a seed. It also has no quic, dtls, udp, or webrtc dialer, no content swarm, and no gossip-learning learner. Use the Node host as the seed, and the JRuby face or a Java peer for the ordered verbs. Binding an ordered_take agent on MRI raises EngineUnsupported and names the JRuby face.

On JRuby, Fleet.start takes the starter's whole key set, including bind:

# jruby_test.rb
AgentSpaces::JRuby::Fleet.start(
  "security" => { "profile" => "dev-local" }, "bind" => "127.0.0.1:#{port}", "tick-millis" => 100,
  "groups" => [{ "name" => "shop", "founding" => "rbjr-shop-v1", "seeds" => seeds, "spaces" => %w[orders votes] }]
)
# ...
bound = worker.bind(fulfiller)
# ...
client.write("orders", JShop::Order.new(order_id: "o-1", item: "kite", quantity: 2, notes: ["fragile"]))
shipment = eventually { client.all("orders", JShop::Shipment).first }

The face writes a Java interface for each agent shape and a record for each entry the classpath lacks, compiles them with the JDK's compiler, and binds a proxy. An ordered log still needs a Java-side voter, as described in ordered takes.

Capability clients

A capability is a fleet service a peer advertises. The core gem includes clients for the ones Java providers serve, under AgentSpaces::Capabilities, all over one pipe multiplexer, Pipes.new(peer).

  • Votes. AgentSpaces::Votes.propose, cast, tally, and decision run over a space and need no provider. The ballot and on_decision macros use them.
  • Aggregate. Push-sum averages and rosters across the fleet: start(epoch, value), run, await_settled, and on_estimate.
  • Semantic. query(text) ranks the cards this peer has cached, and remote_query(text) asks the fleet.
  • RemoteActions. actions(peer) lists the actions every card declares, and invoke writes the input and waits for the correlated result.
  • OrderedLog. A client of the Raft ordered log: space.take(Type, through: log) claims through it. The voters run in Java or TypeScript.
  • DataQueryClient and AssetInvocations. The agent side of the connector protocol.

The council worker of the cross-language example sets up three of them:

# council_worker.rb
pipes = AgentSpaces::Capabilities::Pipes.new(peer)
aggregate = AgentSpaces::Capabilities::Aggregate.new(pipes)
AgentSpaces::Capabilities::Semantic.new(pipes) # answers the coordinator's semantic queries from this cache
bound = peer.bind(Councillor.new, agent: identity.renewing_subordinate("rb-worker", "PT24H"))
aggregate.start("load", value)
aggregate.run(interval_seconds: 0.25)

A remote action call, from the capability tests:

# capabilities_test.rb
found = RemoteActions.actions(b)
# ...
result = RemoteActions.invoke(b, found.first, L4::Task.new(topic: "crdts", priority: 2),
                              result_type: L4::Finding, timeout_seconds: 15)

The developer guide's capabilities chapter explains each service.

Rails

agentspaces-rails is a Railtie. It reads config/agentspaces.yml through config_for, so ERB and environments work. The file holds the shared agentspaces: subtree:

# config/agentspaces.yml
default: &default
  agentspaces:
    tick-millis: 250
    groups:
      - name: shop
        founding: shop-v1
        seeds: <%= ENV.fetch("SHOP_SEEDS", "127.0.0.1:7700").split(",") %>
        spaces: [orders]
  • Put agent classes in app/agents. Zeitwerk loads them.
  • Run them with bin/rails agentspaces:work, for example as a Procfile line. It binds every agent class and serves until SIGTERM. bin/rails agentspaces:config prints the configuration it reads.
  • Write from controllers and jobs with AgentSpaces::Rails.peer.space("orders") << order. The web process starts its peer lazily, once per process.
  • A forked Puma, Unicorn, or Passenger worker starts its own peer.
  • A fleet outage raises AgentSpaces::Unavailable when a request needs the fleet. It never stops the application from booting.
  • write.agentspaces, take.agentspaces, and complete.agentspaces notifications carry the space, the schema, and the entry id.

RubyLLM tools and MCP

agentspaces-ruby_llm turns every action on the fleet's cards into a RubyLLM::Tool. The tool's parameters come from the entry class this process registered for the action's input schema, so a model sees typed fields. Pass them to a chat with RubyLLM.chat.with_tools(*tools).

# ruby_llm_test.rb
tools = AgentSpaces::RubyLLM.tools(@assistant, timeout: 10)
# ...
result = tool.call(order_id: "o-1", item: "kite", quantity: 2)
# ...
value = AgentSpaces::Lease.keepalive(taken, every: 0.2) do
  sleep 0.7
  :model_answer
end

AgentSpaces::Lease.keepalive renews a take's lease on a timer while its block runs. Wrap a long model call inside a take body with it, so the entry stays yours while the model thinks. Inside a take body it finds the current take on its own.

agentspaces-mcp serves the same actions as MCP tools on the official Ruby SDK, over stdio or Streamable HTTP. For stdio, open the server with MCP::Server::Transports::StdioTransport.new(server).open.

# mcp_test.rb
server = AgentSpaces::MCP.server(gateway, name: "shop", timeout: 10)

The agentspaces command

The core gem ships an agentspaces executable in gems/agentspaces/exe.

agentspaces id [keystore]                    # print the PeerID of a keystore, creating it when absent
agentspaces check <file-or-dir>...           # load agent classes without a peer and check every clause
agentspaces doctor                           # run every crypto primitive and name the OpenSSL build
agentspaces status <fleet.yaml>              # join, print members, spaces, and cards, and leave
agentspaces work <fleet.yaml> <files>...     # bind every agent class and serve until SIGTERM
agentspaces console <fleet.yaml>             # IRB with peer and space(name) bound

check needs no fleet. Point it at files that define classes and start no peer, such as an app/agents directory:

$ agentspaces check examples/ruby-fleet/agents
Researcher (agent rb-worker): 1 clause(s)
  research: take ai.badmonkey.agentspaces.examples.fleet.ResearchFleet$ResearchTask#v1 in tasks -> ...

A mistake prints as file:line: error: message, for example fulfiller.rb:8: error: take has no option :lese; accepted: space, group, lease, ..., and the command exits 1.

aspace://guide/ruby/examples

Examples

The first two examples run three in-memory fleets inside one Ruby process, with no seed, no network, and no model key. The third drains a Java coordinator's tasks over TCP.

Hand work across peers, then survive a crash

This example shows an agent on one peer finishing work that another peer writes, and a take that reappears after its holder dies. AgentSpaces::Testing::Network.fleet builds connected in-memory peers, and TestClock lets the test move time forward.

# readme_examples_test.rb
Ticket = AgentSpaces::Entry.define("readme.Ticket#v1", ticket_id: String, text: String, priority: Integer)
Triage = AgentSpaces::Entry.define("readme.Triage#v1", ticket_id: String, queue: String, by: String)

class Triager
  include AgentSpaces::Agent
  agent "triager", description: "Routes support tickets to a queue", goals: ["triage"]

  take Ticket, space: "tickets", lease: "30s", where: { priority: 1.. }, produces: Triage
  def route(ticket)
    queue = ticket.text.match?(/refund|charge/i) ? "billing" : "support"
    Triage.new(ticket_id: ticket.ticket_id, queue: queue, by: "triager")
  end
end

worker, desk = AgentSpaces::Testing::Network.fleet("zReadme", 2)
bound = worker.bind(Triager.new, agent: worker.identity.renewing_subordinate("triager", "PT24H"))

desk.space("tickets") << Ticket.new(ticket_id: "t-1", text: "Refund my last charge", priority: 2)
triage = desk.space("tickets").read(Triage, timeout: "10s")
# => #<data Triage ticket_id="t-1", queue="billing", by="triager">

The agent signs as its own subordinate key (renewing_subordinate), so every peer stores its work as attested by that agent. The method body is where a model call goes: whatever it returns is the next entry.

The second half takes a ticket by hand, closes the peer before it completes, and moves the clock past the lease:

# readme_examples_test.rb
clock = AgentSpaces::Testing::TestClock.new
crasher, survivor, desk = AgentSpaces::Testing::Network.fleet("zReadmeCrash", 3, clock: clock)
desk.space("tickets") << Ticket.new(ticket_id: "t-2", text: "Password reset", priority: 1)

# The first worker takes the ticket and dies before it completes the take.
taken = crasher.space("tickets").take(Ticket, lease: "5s", timeout: "5s")
crasher.close

# Nobody else can take it while the lease holds; once it lapses, the entry reappears.
assert_nil survivor.space("tickets").take(Ticket, lease: "5s", timeout: 1)
clock.advance(6_000)
retaken = survivor.space("tickets").take(Ticket, lease: "30s", timeout: "5s")
retaken.complete(Triage.new(ticket_id: retaken.value.ticket_id, queue: "support", by: "survivor"))

Run both from the agentspaces-rb clone:

ruby -Igems/agentspaces/lib -Igems/agentspaces/test gems/agentspaces/test/readme_examples_test.rb

The full source is gems/agentspaces/test/readme_examples_test.rb in agentspaces-rb.

Two agents decide a vote

This example shows the ballot macro. Two reviewers answer a proposal, and a third peer reads the decision once the quorum of two closes.

# readme_examples_test.rb
class Reviewer
  include AgentSpaces::Agent
  agent "reviewer", description: "Votes on releases"

  def initialize(answer) = @answer = answer

  ballot space: "votes", prefix: "release:"
  def judge(proposal) = proposal.question.include?("0.5.0") ? @answer : nil
end

a, b, chair = AgentSpaces::Testing::Network.fleet("zReadmeVote", 3)
bound = [a.bind(Reviewer.new("ship")), b.bind(Reviewer.new("ship"))]
AgentSpaces::Votes.propose(chair, "votes", "release:0.5.0", "Ship 0.5.0?", %w[ship hold], 2)

decision = until_true { AgentSpaces::Votes.decision(chair.states, "release:0.5.0") }
# => {"proposalId" => "release:0.5.0", "winner" => "ship", "tally" => {"ship" => 2, "hold" => 0}}
  • prefix: "release:" limits the ballot to proposals whose id starts with release:.
  • A ballot method that returns nil abstains.
  • until_true is the test's polling helper. It retries the block until it returns a value or ten seconds pass.

To act on the result in an agent, add an on_decision space: "votes", prefix: "release:" macro above a method that receives the decision. The same test file runs this example. Read votes in the developer guide for quorums and tallies.

A Ruby worker drains a Java coordinator

This example shows a Ruby peer working beside Java. The Java coordinator of the research fleet publishes six ResearchTask entries and waits for six Finding entries. The Ruby worker takes each task under the same claim rules as a Java worker.

The entries name the Java records' schemas, so both sides read the same values:

# agents/researcher.rb
ResearchTask = AgentSpaces::Entry.define(
  "ai.badmonkey.agentspaces.examples.fleet.ResearchFleet$ResearchTask#v1", topic: String, priority: Integer
)
Finding = AgentSpaces::Entry.define(
  "ai.badmonkey.agentspaces.examples.fleet.ResearchFleet$Finding#v1", topic: String, summary: String, worker: String
)

class Researcher
  include AgentSpaces::Agent
  agent "rb-worker", description: "Researches a topic in Ruby", goals: ["research"]

  take ResearchTask, space: "tasks", lease: "30s", poll_timeout: "2s", produces: Finding
  def research(task)
    puts "ruby worker: took #{task.topic} (priority #{task.priority})"
    Finding.new(topic: task.topic, summary: "researched in ruby: #{task.topic}", worker: "rb-worker")
  end
end
# research_worker.rb
seed = ARGV.fetch(0, "127.0.0.1:7467")
AgentSpaces.serve(group: "research-fleet", founding: "research-fleet-demo-v1", seeds: [seed]) do |peer|
  puts "ruby worker #{peer.identity.peer_id} joined #{peer.group[0, 12]}... through #{seed}"
  peer.bind(Researcher.new, agent: peer.identity.renewing_subordinate("rb-worker", "PT24H"))
end

Build the Java side first with mvn install in the agentspaces clone: the library is on Maven Central, but the example modules are not published. Then start the coordinator from the agentspaces Maven root, and the worker from agentspaces-rb/examples/ruby-fleet:

mvn -q -pl examples/example-02-research-fleet exec:java -Dexec.args="coordinator 7467"
ruby research_worker.rb 127.0.0.1:7467

The worker prints each task it takes, and the coordinator prints finding: topic-1 by rb-worker for each of the six. Pass tls://host:port in place of the address to reach a TLS seed.

The same directory holds council_worker.rb. It joins the Java CouncilFleet coordinator, takes its tasks, votes on its proposal, contributes to a push-sum average, and answers semantic queries. The comment at the top of the file gives both commands. The Python guide and TypeScript guide run the same coordinators from their languages.