Class: SolidLoop::ToolMiddlewares::ResponseCreation
- Inherits:
-
Object
- Object
- SolidLoop::ToolMiddlewares::ResponseCreation
- Defined in:
- app/services/solid_loop/tool_middlewares/response_creation.rb
Instance Method Summary collapse
- #call(env) ⇒ Object
-
#initialize(app) ⇒ ResponseCreation
constructor
A new instance of ResponseCreation.
Constructor Details
#initialize(app) ⇒ ResponseCreation
Returns a new instance of ResponseCreation.
4 5 6 |
# File 'app/services/solid_loop/tool_middlewares/response_creation.rb', line 4 def initialize(app) @app = app end |
Instance Method Details
#call(env) ⇒ Object
8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
# File 'app/services/solid_loop/tool_middlewares/response_creation.rb', line 8 def call(env) = nil # LOCK ORDER (the tool canonical-write fence): loop row → tool_call → message, # continuing the order ToolExecution began. The LOOP row lock is held # across BOTH the both-token validation AND the canonical response + # continuation, so a pause/resume/stop (which only takes the loop lock) # cannot slip between validation and the write: with the loop FOR # UPDATE-locked, `loop.execution_token` and `tool_call.lease_token` are # frozen for the whole write. A stale/revoked worker writes NOTHING — # no tool response message, no completion transition, no successor job. # # Response persistence, state advancement and successor-job dispatch # commit atomically inside this transaction (SolidLoop requires a # same-database transactional job backend): a crash or rollback leaves # neither the state change nor a dangling job. env.loop.with_lock do env.tool_call.with_lock do env..with_lock do # Finding 9 — the canonical tool RESPONSE message + continuation is the # second half of the tool completion write; fence it with BOTH tokens # too. A worker whose generation was rotated or whose per-tool lease was # revoked between ToolExecution and here must write no canonical # response and dispatch no successor. The `executed_at` checkpoint # (written under the tool_call lock in ToolExecution, itself fenced) # remains the terminal exactly-once guard; this guards the message + # continuation half against the same stale-worker race. return unless canonical_continuation_owned?(env) unless env.tool_call. = env.loop..create!( role: "tool", content: env.result, tool_call_id: env.tool_call.tool_call_id, status: "success" ) end # Explicit ordered scope — never rely on association default order. tool_calls = env..tool_calls.ordered.to_a response_ids = env.loop. .where(role: :tool, tool_call_id: tool_calls.map(&:tool_call_id)) .where("id > ?", env..id) .distinct .pluck(:tool_call_id) next_tool_call = tool_calls.find { |tool_call| !response_ids.include?(tool_call.tool_call_id) } all_resolved = next_tool_call.nil? # Sequential agents chain the next unresolved call here; parallel # agents already enqueued every call up front, so each job only # advances the loop once *all* results have landed. The completion # gate is identical in both modes and fenced by the execution token, # so whichever job commits the final result queues the next LLM turn. sequential = (env.agent || env.loop.agent)&.sequential_tool_calls? if sequential && next_tool_call SolidLoop.enqueue!(SolidLoop::ToolExecutionJob, next_tool_call.id, env.execution_token) elsif all_resolved && env.loop.transition_status( from: :running, to: :queued, expected_execution_token: env.execution_token, execution_token: nil, lease_expires_at: nil ) SolidLoop.enqueue!(SolidLoop::LlmCompletionJob, env.loop.id) end end # env.message.with_lock end # env.tool_call.with_lock end # env.loop.with_lock if && env.loop.observe_enabled? SolidLoop::ObserveBroadcastJob.perform_later(.id) end @app.call(env) end |