Class: Silas::ToolInvocation

Inherits:
ApplicationRecord show all
Includes:
Inbox::Broadcastable
Defined in:
app/models/silas/tool_invocation.rb

Constant Summary collapse

STATUSES =
%w[pending started completed failed in_doubt].freeze
EFFECT_MODES =
%w[transactional at_most_once idempotent].freeze
APPROVAL_STATES =
[ nil, "required", "approved", "declined", "expired" ].freeze

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.expire_stale!(now: Time.current) ⇒ Object

Sweep for the rescuer: expire parked invocations past their TTL and fail their turns (parked-forever ghosts are a bug, not a feature).



55
56
57
58
59
60
61
# File 'app/models/silas/tool_invocation.rb', line 55

def self.expire_stale!(now: Time.current)
  where(approval_state: "required").where(approval_expires_at: ..now).find_each do |inv|
    inv.update!(approval_state: "expired", status: "failed",
                result: { "denied" => "approval expired" })
    inv.turn.finish!(:failed, reason: "approval_expired")
  end
end

Instance Method Details

#approve!(by: nil) ⇒ Object

Approve a parked invocation and resume the turn with a FRESH job (the parked job exited normally; its continuation is consumed). For an in-doubt invocation, approval means "it did not run — re-execute".



36
37
38
39
40
# File 'app/models/silas/tool_invocation.rb', line 36

def approve!(by: nil)
  assert_parked!
  update!(status: "pending", approval_state: "approved", approved_by: by)
  resume_turn!
end

#awaiting_approval?Boolean

Returns:

  • (Boolean)


19
# File 'app/models/silas/tool_invocation.rb', line 19

def awaiting_approval? = approval_state == "required"

#completed?Boolean

Returns:

  • (Boolean)


17
# File 'app/models/silas/tool_invocation.rb', line 17

def completed? = status == "completed"

#decline!(reason:, by: nil) ⇒ Object

Decline: for an approval gate, eve's shape — the tool is not executed and the model sees reason as the result, then the loop continues. For an in-doubt invocation, decline means "assume it ran / abandon" — the operator-supplied reason becomes the recorded outcome.



46
47
48
49
50
51
# File 'app/models/silas/tool_invocation.rb', line 46

def decline!(reason:, by: nil)
  assert_parked!
  update!(status: "failed", approval_state: "declined", approved_by: by,
          decline_reason: reason, result: { "denied" => reason })
  resume_turn!
end

#in_doubt?Boolean

Returns:

  • (Boolean)


18
# File 'app/models/silas/tool_invocation.rb', line 18

def in_doubt?  = status == "in_doubt"

#notify_channel_approvalObject



29
30
31
# File 'app/models/silas/tool_invocation.rb', line 29

def notify_channel_approval
  ChannelDeliveryJob.perform_later("approval", id)
end

#should_notify_approval?Boolean

Returns:

  • (Boolean)


25
26
27
# File 'app/models/silas/tool_invocation.rb', line 25

def should_notify_approval?
  saved_change_to_approval_state? && approval_state == "required" && turn.session.channel.present?
end