module RubyLLM::AgUI::Rails::Streaming
The controller concern that serves one AG-UI run as one SSE response.
class AgentRunsController < ApplicationController include RubyLLM::AgUI::Rails::Streaming def create stream_ag_ui_run do |input| chat = ag_ui_chats.find_by!(ag_ui_thread_id: input.thread_id) authorize! chat chat end end end
What the host owns, and the concern never guesses:
-
authentication (a ‘before_action`, or the action itself);
-
‘#ag_ui_scope`, the tenant/actor key the store is bounded by — the concern refuses to run without one, so an unauthenticated request cannot reserve a run;
-
the chat the block returns, built with only the tools and the model that actor may use. The client’s ‘messages`, `tools` and `state` are data for that chat; they authorize nothing.
Refusals are decided before the response is committed, so a client that cannot read an event stream, or that sent an input the gem will not accept, gets a status code instead of half a run.
Constants
- CONFLICT
The run cannot be served: a live run already has a stream, or the transport is gone.
- NOT_ACCEPTABLE
A client that cannot read an event stream, and tells us so.
- NOT_FOUND
‘resolve_chat` found no chat for the thread this actor owns.
Public Instance Methods
The tenant/actor key every reservation is bounded by.
It must come from the authenticated request, not from the input: a client-chosen scope would let one actor attach to another’s run.
# File lib/ruby_llm/ag_ui/rails/streaming.rb, line 79 def ag_ui_scope raise ConfigurationError, "#{self.class} must define #ag_ui_scope, built from the authenticated tenant and actor" end
The state to publish for a reconnect, or nil. The host’s own run state is not the gem’s to guess.
# File lib/ruby_llm/ag_ui/rails/streaming.rb, line 86 def ag_ui_state nil end
Streams one run and returns the run’s record, or nil when the client disconnected. The block receives the parsed input and returns the chat for it.
# File lib/ruby_llm/ag_ui/rails/streaming.rb, line 51 def stream_ag_ui_run(&resolve_chat) if resolve_chat.nil? raise ConfigurationError, 'stream_ag_ui_run needs a block that returns the chat for the input' end return reject_ag_ui_request(NOT_ACCEPTABLE, accept_message) unless ag_ui_accept? input = ag_ui_input chat = resolve_chat.call(input) run = ag_ui_run(chat, input) ag_ui_stream(run) rescue ::RubyLLM::AgUI::InputError => e # Every refusal the gem itself decides carries the status it should be # answered with: 400 for an input that cannot be accepted, 409 for an # identity claimed with different input or a resume that answers # nothing, 413 for a body past the configured bound. reject_ag_ui_request(e.http_status, e.message) rescue ::ActiveRecord::RecordNotFound reject_ag_ui_request(NOT_FOUND, 'There is no such thread.') rescue ::RubyLLM::AgUI::StreamError => e reject_ag_ui_request(CONFLICT, e.message) end