Class: SolidLoop::Base
- Inherits:
-
Object
- Object
- SolidLoop::Base
- Defined in:
- app/models/solid_loop/base.rb
Class Attribute Summary collapse
-
.llm_provider ⇒ Object
Returns the value of attribute llm_provider.
Instance Attribute Summary collapse
-
#loop_record ⇒ Object
readonly
Returns the value of attribute loop_record.
-
#subject ⇒ Object
readonly
Returns the value of attribute subject.
Class Method Summary collapse
- .create!(subject:, message:, system_prompt: nil, **state) ⇒ Object
- .start!(subject:, message:, system_prompt: nil, **state) ⇒ Object
Instance Method Summary collapse
- #configure_llm_middlewares(builder) ⇒ Object
- #configure_tool_middlewares(builder) ⇒ Object
-
#initialize(loop_record) ⇒ Base
constructor
A new instance of Base.
- #llm_adapter_name ⇒ Object
- #llm_dialect_name ⇒ Object
- #max_cost ⇒ Object
- #max_duration ⇒ Object
- #max_steps ⇒ Object
- #max_tokens ⇒ Object
- #max_total_tokens ⇒ Object
- #mcp_principal ⇒ Object
- #mcps ⇒ Object
- #on_mcp_session_initialized(mcp_session) ⇒ Object
- #on_message_created(message) ⇒ Object
- #pause! ⇒ Object
- #reasoning_strategies ⇒ Object
- #refresh_mcp_tools? ⇒ Boolean
- #resume! ⇒ Object
-
#sequential_tool_calls? ⇒ Boolean
When false (default), all of a turn's tool calls are enqueued at once and run in parallel across workers.
- #streaming? ⇒ Boolean
- #system_prompt ⇒ Object
Constructor Details
#initialize(loop_record) ⇒ Base
Returns a new instance of Base.
9 10 11 12 |
# File 'app/models/solid_loop/base.rb', line 9 def initialize(loop_record) @loop_record = loop_record @subject = loop_record.subject end |
Class Attribute Details
.llm_provider ⇒ Object
Returns the value of attribute llm_provider.
4 5 6 |
# File 'app/models/solid_loop/base.rb', line 4 def llm_provider @llm_provider end |
Instance Attribute Details
#loop_record ⇒ Object (readonly)
Returns the value of attribute loop_record.
7 8 9 |
# File 'app/models/solid_loop/base.rb', line 7 def loop_record @loop_record end |
#subject ⇒ Object (readonly)
Returns the value of attribute subject.
7 8 9 |
# File 'app/models/solid_loop/base.rb', line 7 def subject @subject end |
Class Method Details
.create!(subject:, message:, system_prompt: nil, **state) ⇒ Object
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
# File 'app/models/solid_loop/base.rb', line 14 def self.create!(subject:, message:, system_prompt: nil, **state) loop_record = SolidLoop::Loop.create!( subject: subject, agent_class_name: name, state: state ) agent_instance = new(loop_record) system_prompt_content = system_prompt || begin agent_instance.system_prompt rescue NotImplementedError nil end if system_prompt_content.present? loop_record..create!(role: "system", content: system_prompt_content) end loop_record..create!(role: "user", content: ) loop_record end |
.start!(subject:, message:, system_prompt: nil, **state) ⇒ Object
38 39 40 41 42 43 44 45 46 47 48 49 |
# File 'app/models/solid_loop/base.rb', line 38 def self.start!(subject:, message:, system_prompt: nil, **state) loop_record = create!(subject: subject, message: , system_prompt: system_prompt, **state) # State advancement and dispatch commit atomically (same-database # transactional job backend): no queued loop without its job. loop_record.transaction do loop_record.update!(status: "queued") SolidLoop.enqueue!(LlmCompletionJob, loop_record.id) end loop_record end |
Instance Method Details
#configure_llm_middlewares(builder) ⇒ Object
55 56 |
# File 'app/models/solid_loop/base.rb', line 55 def configure_llm_middlewares(builder) end |
#configure_tool_middlewares(builder) ⇒ Object
58 59 |
# File 'app/models/solid_loop/base.rb', line 58 def configure_tool_middlewares(builder) end |
#llm_adapter_name ⇒ Object
80 81 82 |
# File 'app/models/solid_loop/base.rb', line 80 def llm_adapter_name :native end |
#llm_dialect_name ⇒ Object
84 85 86 |
# File 'app/models/solid_loop/base.rb', line 84 def llm_dialect_name :open_ai end |
#max_cost ⇒ Object
109 110 111 |
# File 'app/models/solid_loop/base.rb', line 109 def max_cost 10.0 end |
#max_duration ⇒ Object
113 114 115 |
# File 'app/models/solid_loop/base.rb', line 113 def max_duration 2.hours end |
#max_steps ⇒ Object
101 102 103 |
# File 'app/models/solid_loop/base.rb', line 101 def max_steps 100 end |
#max_tokens ⇒ Object
97 98 99 |
# File 'app/models/solid_loop/base.rb', line 97 def max_tokens 16 * 1024 # Default end |
#max_total_tokens ⇒ Object
105 106 107 |
# File 'app/models/solid_loop/base.rb', line 105 def max_total_tokens 1_000_000 end |
#mcp_principal ⇒ Object
93 94 95 |
# File 'app/models/solid_loop/base.rb', line 93 def mcp_principal subject end |
#mcps ⇒ Object
51 52 53 |
# File 'app/models/solid_loop/base.rb', line 51 def mcps [] end |
#on_mcp_session_initialized(mcp_session) ⇒ Object
125 126 127 |
# File 'app/models/solid_loop/base.rb', line 125 def on_mcp_session_initialized(mcp_session) # Hook called when an MCP session is established for the agent. end |
#on_message_created(message) ⇒ Object
121 122 123 |
# File 'app/models/solid_loop/base.rb', line 121 def () # Hook called whenever a new message is created in the loop. end |
#pause! ⇒ Object
129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 |
# File 'app/models/solid_loop/base.rb', line 129 def pause! # Cleanup 6 — pausing synchronously fails+hides any orphaned `processing` # assistant shell (born-hidden shell protocol) inside the same transition # txn, so no paused loop retains a hidden `processing` shell. The block runs # under the loop lock. loop_record.transition_status( from: SolidLoop::Loop::ACTIVE_STATUSES, to: :paused, execution_token: nil, lease_expires_at: nil ) do loop_record..where(role: "assistant", status: "processing").find_each do |shell| shell.update!(status: "failed", is_hidden: true) end end end |
#reasoning_strategies ⇒ Object
88 89 90 91 |
# File 'app/models/solid_loop/base.rb', line 88 def reasoning_strategies # Default to the most robust method for returning thoughts: XML wrapping [ :xml ] end |
#refresh_mcp_tools? ⇒ Boolean
61 62 63 |
# File 'app/models/solid_loop/base.rb', line 61 def refresh_mcp_tools? false end |
#resume! ⇒ Object
146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 |
# File 'app/models/solid_loop/base.rb', line 146 def resume! # State advancement and dispatch commit atomically inside the lock's # transaction (same-database transactional job backend): a crash leaves # neither a running loop without jobs nor jobs for a rolled-back resume. loop_record.with_lock do raise StandardError, "Unfreeze in admin UI before resuming this loop" if loop_record.admin_frozen? return false unless loop_record.init? || loop_record.paused? || loop_record.failed? || loop_record.completed? pending_tool_calls = loop_record.unresolved_tool_calls.to_a if pending_tool_calls.any? token = SecureRandom.uuid # Tool phase: running with a token but NO LLM lease (lease NULL marks # "not an in-flight LLM turn"; the reaper's case 3 guard leaves a # tool-phase loop with unresolved calls untouched — the per-tool lease # reclaim owns that path). loop_record.update!(status: :running, error_message: nil, execution_token: token, lease_expires_at: nil) # Revoke any per-tool lease left by the paused/failed generation. The # fresh generation owns these calls now; without this a stale (but not # yet time-expired) lease from the previous worker would make the # re-enqueued job's claim CAS no-op, stranding the resumed tool. Only # UNEXECUTED calls carry a live-ish lease worth clearing; executed ones # already NULL it at their checkpoint. (Finding 9: resume uses a fresh # generation + the stable key; the old worker's late write is already # fenced out by the rotated execution_token.) SolidLoop::ToolCall .where(id: pending_tool_calls.map(&:id), executed_at: nil) .update_all(lease_token: nil, leased_until: nil, updated_at: Time.current) # Mirror the emission policy: sequential agents re-enter one call at a # time (ResponseCreation chains the rest); parallel agents re-enter # every unresolved call so a multi-call turn cannot stall on resume. calls = sequential_tool_calls? ? pending_tool_calls.first(1) : pending_tool_calls calls.each { |tool_call| SolidLoop.enqueue!(ToolExecutionJob, tool_call.id, token) } else # Cleanup 4 — the no-unresolved-tools branch re-queues an LLM turn; the # non-`running` target (queued) must NULL lease_expires_at so the DB # CHECK holds and the reaper doesn't treat it as an in-flight LLM turn. loop_record.update!(status: :queued, error_message: nil, execution_token: nil, lease_expires_at: nil) SolidLoop.enqueue!(LlmCompletionJob, loop_record.id) end end true end |
#sequential_tool_calls? ⇒ Boolean
When false (default), all of a turn's tool calls are enqueued at once and run in parallel across workers. When true, they run strictly one-at-a-time in model order (the next call is enqueued only after the previous result lands), and the execution-boundary guard rejects out-of-order jobs.
HITL / per-tool approval gating is cleanest with true: a single in-flight
tool at a time means an approval decision has one unambiguous step to gate.
76 77 78 |
# File 'app/models/solid_loop/base.rb', line 76 def sequential_tool_calls? false end |
#streaming? ⇒ Boolean
65 66 67 |
# File 'app/models/solid_loop/base.rb', line 65 def streaming? true # Default backwards compatibility end |
#system_prompt ⇒ Object
117 118 119 |
# File 'app/models/solid_loop/base.rb', line 117 def system_prompt raise NotImplementedError, "You must implement #system_prompt in #{self.class.name}" end |