Class: AgentLoop
- Inherits:
-
Object
- Object
- AgentLoop
- Defined in:
- lib/agent_loop.rb
Overview
Not thread-safe. Callers must serialize concurrent calls to #process (e.g. TUI uses a loading flag, future callers should use session-level locks).
Orchestrates the LLM agent loop: accepts user input, runs the tool-use cycle via LLM::Client, and emits events through Events::Bus.
Extracted from TUI::Screens::Chat so the same agent logic can run from the TUI, a background job, or an Action Cable channel.
Constant Summary collapse
- STANDARD_TOOLS =
Tool classes available to all sessions by default.
[Tools::Bash, Tools::Read, Tools::Write, Tools::Edit, Tools::WebGet, Tools::Think, Tools::Remember].freeze
- STANDARD_TOOLS_BY_NAME =
Name-to-class mapping for tool restriction validation and registry building.
STANDARD_TOOLS.index_by(&:tool_name).freeze
Instance Attribute Summary collapse
-
#session ⇒ Session
readonly
The conversation session this loop operates on.
Instance Method Summary collapse
-
#deliver! ⇒ void
Makes the first LLM API call to verify delivery.
-
#finalize ⇒ Object
Clean up the underlying ShellSession PTY and resources.
-
#initialize(session:, shell_session: nil, client: nil, registry: nil) ⇒ AgentLoop
constructor
A new instance of AgentLoop.
-
#process(input) ⇒ String?
Runs the agent loop for a single user input.
-
#run ⇒ String?
Runs the LLM tool-use loop on persisted session messages.
Constructor Details
#initialize(session:, shell_session: nil, client: nil, registry: nil) ⇒ AgentLoop
Returns a new instance of AgentLoop.
36 37 38 39 40 41 |
# File 'lib/agent_loop.rb', line 36 def initialize(session:, shell_session: nil, client: nil, registry: nil) @session = session @shell_session = shell_session || ShellSession.new(session_id: session.id) @client = client @registry = registry end |
Instance Attribute Details
#session ⇒ Session (readonly)
Returns the conversation session this loop operates on.
27 28 29 |
# File 'lib/agent_loop.rb', line 27 def session @session end |
Instance Method Details
#deliver! ⇒ void
This method returns an undefined value.
Makes the first LLM API call to verify delivery. Called inside the Bounce Back transaction — if this raises, the user event rolls back.
Caches the first response so the subsequent #run call can continue from it without duplicating the API call.
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
# File 'lib/agent_loop.rb', line 73 def deliver! @client ||= LLM::Client.new @registry ||= build_tool_registry = @session. = @first_response = @client.provider.( model: @client.model, messages: , max_tokens: @client.max_tokens, tools: @registry.schemas, ** ) end |
#finalize ⇒ Object
Clean up the underlying ShellSession PTY and resources. Safe to call multiple times — subsequent calls are no-ops.
124 125 126 |
# File 'lib/agent_loop.rb', line 124 def finalize @shell_session&.finalize end |
#process(input) ⇒ String?
Runs the agent loop for a single user input.
Persists the user event directly (the global Persister skips non-pending user messages because AgentRequestJob owns their lifecycle). Then emits a bus notification and delegates to #run. On error emits Events::AgentMessage with the error text.
52 53 54 55 56 57 58 59 60 61 62 63 |
# File 'lib/agent_loop.rb', line 52 def process(input) text = input.to_s.strip return if text.empty? persist_user_event(text) Events::Bus.emit(Events::UserMessage.new(content: text, session_id: @session.id)) run rescue => error = "#{error.class}: #{error.}" Events::Bus.emit(Events::AgentMessage.new(content: , session_id: @session.id)) end |
#run ⇒ String?
Runs the LLM tool-use loop on persisted session messages.
When a cached first response exists (from #deliver!), continues from that response without a redundant API call. Otherwise makes a fresh call — used for pending message processing and the standard path.
Lets errors propagate — designed for callers like AgentRequestJob that handle retries and need errors to bubble up.
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 |
# File 'lib/agent_loop.rb', line 102 def run @client ||= LLM::Client.new @registry ||= build_tool_registry = @session. = first_resp = @first_response @first_response = nil response = @client.chat_with_tools( , registry: @registry, session_id: @session.id, first_response: first_resp, ** ) return unless response Events::Bus.emit(Events::AgentMessage.new(content: response, session_id: @session.id)) response end |