Problem
Chat#complete loops until the model stops calling tools. There's no way to run one iteration at a time — one provider call, execute any tool calls returned, then stop.
This matters for applications that want to run each LLM iteration as a separate background job. Without step, there's no clean way to do this:
complete holds the thread for the entire agentic loop
- you can't set an iteration budget, and
- you can't re-enqueue between iterations.
Proposed solution
Add Chat#step — identical to one iteration of complete, but returns the response instead of recursing:
response = chat.step # one provider call + tool execution
response.tool_call? # => true if the model wants to continue
complete would be refactored to delegate to step:
def complete(&)
response = step(&)
return response if response.is_a?(Tool::Halt)
response.tool_call? ? complete(&) : response
end
No behaviour change to complete. Tool::Halt semantics preserved.
Why this belongs in the gem
The split between "one iteration" and "loop until done" is a property of the protocol, not application logic. Any application running agents in background jobs needs this. Putting it in user code means wrapping or monkey-patching Chat, which is worse.
Alternatives considered
- Monkey-patching in an initializer — works but fragile across upgrades
- Subclassing
Chat — doesn't work cleanly with acts_as_chat
max_turns on complete — would require a new parameter and internal counter; step is simpler and more composable
P.S. I already have this in my fork to see if it addressed something I was working on
Problem
Chat#completeloops until the model stops calling tools. There's no way to run one iteration at a time — one provider call, execute any tool calls returned, then stop.This matters for applications that want to run each LLM iteration as a separate background job. Without
step, there's no clean way to do this:completeholds the thread for the entire agentic loopProposed solution
Add
Chat#step— identical to one iteration ofcomplete, but returns the response instead of recursing:completewould be refactored to delegate tostep:No behaviour change to
complete.Tool::Haltsemantics preserved.Why this belongs in the gem
The split between "one iteration" and "loop until done" is a property of the protocol, not application logic. Any application running agents in background jobs needs this. Putting it in user code means wrapping or monkey-patching
Chat, which is worse.Alternatives considered
Chat— doesn't work cleanly withacts_as_chatmax_turnsoncomplete— would require a new parameter and internal counter;stepis simpler and more composableP.S. I already have this in my fork to see if it addressed something I was working on