Class: Silas::Turn
- Inherits:
-
ApplicationRecord
- Object
- ActiveRecord::Base
- ApplicationRecord
- Silas::Turn
- Includes:
- Inbox::Broadcastable
- Defined in:
- app/models/silas/turn.rb
Constant Summary collapse
- STATUSES =
%w[queued running waiting in_doubt completed failed canceled].freeze
- ACTIVE_STATUSES =
%w[queued running waiting in_doubt].freeze
Instance Method Summary collapse
- #active? ⇒ Boolean
-
#answer_text ⇒ Object
The agent's answer for this turn: the last completed step's text blocks.
-
#budget_parked? ⇒ Boolean
Parked by a budget cap (failure_reason doubles as the park reason while the turn is waiting; it is cleared on resume).
-
#cancel!(reason: "canceled") ⇒ Object
Cancel a turn.
- #canceled? ⇒ Boolean
- #completed? ⇒ Boolean
- #expire_pending_approvals!(reason) ⇒ Object
- #finish!(new_status, reason: nil) ⇒ Object
- #parked? ⇒ Boolean
-
#raise_budget!(max_cost: nil, max_input_tokens: nil, timeout: nil) ⇒ Object
Human top-up for a budget-parked turn: record the raised limit(s) and resume with a fresh job — completed steps replay from rows, no model re-calls, no re-effects (the same resume path approvals use).
Instance Method Details
#active? ⇒ Boolean
20 |
# File 'app/models/silas/turn.rb', line 20 def active? = ACTIVE_STATUSES.include?(status) |
#answer_text ⇒ Object
The agent's answer for this turn: the last completed step's text blocks.
79 80 81 82 83 84 |
# File 'app/models/silas/turn.rb', line 79 def answer_text step = steps.where(status: "completed").order(:index).last return "" unless step Array(step.response_blocks).select { |b| b["type"] == "text" }.map { |b| b["text"] }.join end |
#budget_parked? ⇒ Boolean
Parked by a budget cap (failure_reason doubles as the park reason while the turn is waiting; it is cleared on resume).
57 58 59 |
# File 'app/models/silas/turn.rb', line 57 def budget_parked? waiting? && Budget::REASONS.include?(failure_reason) end |
#cancel!(reason: "canceled") ⇒ Object
Cancel a turn. A PARKED or QUEUED turn (no live execution) settles to canceled immediately, expiring its pending approvals so a later approve! can't zombie-resume it. A RUNNING turn is flagged; the loop honors the flag at the next step boundary — the in-flight model call completes and its step commits (aborting mid-step would forfeit paid work and create an in-doubt tool window for nothing).
35 36 37 38 39 40 41 42 43 44 45 46 |
# File 'app/models/silas/turn.rb', line 35 def cancel!(reason: "canceled") raise Error, "turn #{id} is already terminal (#{status})" unless active? if running? update!(cancel_requested_at: Time.current) :cancel_requested else expire_pending_approvals!(reason) finish!(:canceled, reason: reason) :canceled end end |
#canceled? ⇒ Boolean
27 |
# File 'app/models/silas/turn.rb', line 27 def canceled? = status == "canceled" |
#completed? ⇒ Boolean
19 |
# File 'app/models/silas/turn.rb', line 19 def completed? = status == "completed" |
#expire_pending_approvals!(reason) ⇒ Object
48 49 50 51 52 53 |
# File 'app/models/silas/turn.rb', line 48 def expire_pending_approvals!(reason) tool_invocations.where(approval_state: "required").find_each do |inv| inv.update!(approval_state: "expired", status: "failed", result: { "denied" => reason }) end end |
#finish!(new_status, reason: nil) ⇒ Object
23 24 25 |
# File 'app/models/silas/turn.rb', line 23 def finish!(new_status, reason: nil) update!(status: new_status.to_s, failure_reason: reason, finished_at: Time.current) end |
#parked? ⇒ Boolean
21 |
# File 'app/models/silas/turn.rb', line 21 def parked? = status == "waiting" || status == "in_doubt" |
#raise_budget!(max_cost: nil, max_input_tokens: nil, timeout: nil) ⇒ Object
Human top-up for a budget-parked turn: record the raised limit(s) and resume with a fresh job — completed steps replay from rows, no model re-calls, no re-effects (the same resume path approvals use). turn.raise_budget!(max_cost: 1.50) # dollars turn.raise_budget!(max_input_tokens: 200_000, timeout: 3600)
66 67 68 69 70 71 72 73 74 75 76 |
# File 'app/models/silas/turn.rb', line 66 def raise_budget!(max_cost: nil, max_input_tokens: nil, timeout: nil) raise Error, "turn #{id} is not budget-parked (#{status}/#{failure_reason})" unless budget_parked? raises = { "max_cost" => max_cost, "max_input_tokens" => max_input_tokens, "timeout" => timeout }.compact raise ArgumentError, "pass at least one limit to raise" if raises.empty? update!(budget_overrides: (budget_overrides || {}).merge(raises), failure_reason: nil, status: "queued") AgentLoopJob.perform_later(id) end |