cd /news/ai-tools/rubyllm-2-0-the-agentic-loop-exposed · home topics ai-tools article
[ARTICLE · art-114127] src=paolino.me ↗ pub= topic=ai-tools verified=true sentiment=↑ positive

RubyLLM 2.0: The Agentic Loop, Exposed

RubyLLM 2.0, the Ruby gem for AI chat, exposes the agentic loop as a set of composable verbs—ask_later, generate, run_tools, step, complete?, and complete—so developers can control each model call and tool execution, with cancelable generation via chat.cancel! and resumable runs across processes. The update removes the halt mechanism from version 1.x, moving loop control to the caller, and supports batch generation and Rails 8.1 ActiveJob Continuations for checkpointing.

read3 min views1 publishedAug 27, 2026
RubyLLM 2.0: The Agentic Loop, Exposed
Image: Paolino (auto-discovered)

Strip any agent framework down and you find the same loop: call the model, run the tools it asked for, call the model again, stop when it answers without wanting a tool. In RubyLLM 1.x that loop lived inside ask

, sealed. In RubyLLM 2.0, you can also make it yours.

chat = RubyLLM.chat(model: "claude-sonnet-4-6")
  .with_tools(Weather)
  .ask("What's the weather in Paris?")
chat = RubyLLM.chat(model: "claude-sonnet-4-6")
  .with_tools(Weather)
  .ask_later("What's the weather in Paris?")

chat.step until chat.complete?  # generate, run_tools, generate
chat.messages.last.content
=> "Here's the current weather in **Paris, France**:\n\n- 🌡️ **Tempera...

ask

still works exactly as before: one method call runs the conversation to completion. But now it decomposes into verbs you can call yourself:

ask_later

stages your message without sending anything.generate

makes one model call and appends the response. The model’s move.run_tools

executes the pending tool calls and appends their results. Your move. No model call.step

does whichever move is next: tools if any are unanswered, otherwise a model call.complete?

tells you when the conversation is settled: the model answered without calling a tool.complete

steps until done.ask

isask_later

followed bycomplete

.

Why bother? Because sometimes you need finer control about what happens between or around steps. Iteration budgets. Batch generation. Human approval before a tool runs. Logging each move. Persisting the conversation and picking it up somewhere else. In 1.x you worked around a sealed loop. In 2.0 the loop is plain Ruby in your code if you want it.

One Move Per Job #

Each verb decides what to do next by reading the persisted messages. That means the loop doesn’t need to live in one process, or one machine, or one deploy:

class AgentTurnJob < ApplicationJob
  def perform(chat_id)
    chat = Chat.find(chat_id)
    chat.step
    AgentTurnJob.perform_later(chat_id) unless chat.complete?
  end
end

Every turn is its own job. Your queue gets granular retries, your agents survive restarts, and a long run never monopolizes a worker.

The loop is now resumable mid-tool-round too. run_tools

skips tool calls that already have results, so if a process dies after finishing one tool call of three, re the chat and calling step

executes only the remaining two. On Rails 8.1 and later, you can use ActiveJob Continuations to build on this: checkpoint after each move and an agent run survives a redeploy, resuming from the persisted messages with no cursor to manage.

Batches are the same idea at scale: a batch is generate

deferred for many chats at once, with run_tools

run locally between rounds.

Cancelable generation #

chat.cancel!

cancels a run from another thread. At the next checkpoint, before a model call, before a tool executes, or between streamed chunks, the run raises RubyLLM::CancelledError

and clears the flag so the chat can be reused.

In Rails, acts_as_chat

stores the cancellation request on the chat record, so the signal travels through the database. A stop button in your web process halts a background job mid-stream:

class ChatsController < ApplicationController
  def cancel
    Chat.find(params[:id]).cancel!
    head :no_content
  end
end

No pub/sub channel, no Redis flag, no process signals. The job checks the record it already has and stops.

Halt Is Gone #

RubyLLM 1.x let a tool terminate the loop from the inside: return halt("done")

and the conversation ended. That put control flow inside a return value, and it’s gone in 2.0, along with RubyLLM::Tool::Halt

. Tools return results. Stopping belongs to the caller:

until chat.complete?
  chat.step
  break if handed_off? # your halt, in your code
end

If what you want is one tool call per model response rather than a condition, chat.with_tool_options(calls: :one)

does that. For a total round budget, count step

or generate

calls in the loop you control.

The full guide, including the workflow patterns built on these verbs, is at https://rubyllm.com/next/agentic-workflows/.

── more in #ai-tools 4 stories · sorted by recency
── more on @rubyllm 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/rubyllm-2-0-the-agen…] indexed:0 read:3min 2026-08-27 ·