[FEATURE] A public API for running a persisted chat's turns outside RubyLLM's loop
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 5/5
- Estimated time
- Over a week
- Newbie friendliness
- 25/100
Research direction
Start by reading the public chat and provider APIs alongside the internal pieces named in the issue: RunClaims, ActiveRecord::ChatQueue#take, Skill::Loader#listing, and Tool.split_result. The request spans several persisted turn operations and asks which should become public, so first map their existing behavior and tests. Done means a supported, documented API lets an external agent loop drive turns without depending on :nodoc: internals.
Written by the indexing model from the issue text.
Description
Scope check
- This is core LLM communication (not application logic)
- This benefits most users (not just my use case)
- This can't be solved in application code with current RubyLLM
- I read the Contributing Guide
I left the second box open on purpose: this is for people whose agent loop runs somewhere else. I think there will be more of them, but I would rather ask than claim it.
Due diligence
- I searched existing issues
- I checked the documentation
What problem does this solve?
I run persisted RubyLLM chats on the claude CLI, through the Claude Agent SDK, so they use a Claude subscription. The CLI owns the agent loop; RubyLLM keeps everything around it, and its Rails integration does almost all of the work: the records, approvals and their cards, the queue and steering, cancellation, the usage ledger, skills, code execution, MCP servers (OAuth included) and the chat's files. The chat's tools are served to the CLI as an MCP server and run in the app, each as the ToolCall it is recorded as.
What does not work through public API is driving a turn the way RubyLLM's own loop does. Today that takes these :nodoc: pieces:
| What the turn needs | What it uses today |
|---|---|
Claim the chat, so running? sees the run and a second process steps aside |
writes claim_token and claimed_until the way RunClaims does |
| Take queued messages once each, steering or not | ActiveRecord::ChatQueue#take |
| Record the turn's usage, with a status | chat.ruby_llm_usages.create! with the ledger's columns |
| The system text RubyLLM would send: instructions, then the skill listing | Skill::Loader#listing |
| Long tool results saved to the chat's files, attachments described | Chat::FileReferences (spill, describe) |
| Tool progress while a tool runs outside the loop | Support::ProgressReporter.listen |
| A tool's result split into text and attachments | Tool.split_result |
| A provider that searches deferred tools itself | overrides Provider#tool_search? (since #1283) |
Each works, and I keep them behind one module with a test per piece. #1283 already broke one: Chat#tools now asks the provider about tool search, and my provider raised there, so every turn failed until I caught it.
Proposed solution
Mostly naming and documenting what exists. For example:
chat.claim_run { ... } # false when another process holds the chat
chat.take_queued(steering_only: false) # the messages, once each
chat.record_usage(provider:, model:, tokens:, status:, message:)
chat.system_prompt # what the next request's system part would be
chat.with_files { |files| files.spill(tool, call, text) }
and a documented way for a provider to say "this provider's runtime searches tools itself", which #1283's tool_search? almost is.
Why this belongs in RubyLLM
These pieces are RubyLLM's own state: the claim columns, the queue, the ledger, the system text. Application code can only reach them by copying RubyLLM's internals, which is what I do now, and which breaks on internal changes like #1283.
If the whole is out of scope, the two that would help most on their own are the run claim and recording usage for work done elsewhere.
- Dominant language
- Ruby
- Stars
- 4.5k
- Forks
- 523
- Avg merge
- 13h 49m
- Merged PRs (30d)
- 221
Getting set up
- No Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from crmne/ruby_llm
-
[BUG] Message#parsed reads the commentary items of a Responses answer, unlike the OpenAI SDKPossibly taken @crmne claimed this 1 day ago. Openbug
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
crmne/ruby_llm#1281 · 2 reactions ·
Maintainers usually reply within 1 day
-
[BUG] macOS code execution cannot load Homebrew-linked Ruby librariesPossibly taken @yorzi claimed this 3 days ago. Openbug
Difficulty 4/5 3-5 days Newbie friendliness 35/100
crmne/ruby_llm#1278 · 2 comments · 1 reaction ·
Maintainers usually reply within 1 day
-
[FEATURE] Continue Gemini long-decode responses as one generationPossibly taken @yorzi claimed this 7 days ago. Openenhancement
Difficulty 5/5 Over a week Newbie friendliness 35/100
crmne/ruby_llm#1246 · 1 reaction ·
Maintainers usually reply within 1 day
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
Maintainers usually reply within 1 day
-
Difficulty 1/5 Under an hour Newbie friendliness 88/100
TheOdinProject/curriculum#31493 ·
Maintainers usually reply within 1 day
-
enhancement
Difficulty 1/5 Under an hour Newbie friendliness 62/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
rmosolgo/graphql-ruby#5843 ·
Maintainers usually reply within 1 day
-
Difficulty 1/5 1-3 hours Newbie friendliness 66/100
betagouv/conseillers-entreprises#4770 ·
Maintainers usually reply within 1 day