class RubyLLM::AgUI::Interrupt

One approval request attached to a reserved run, and the answer to it.

A RubyLLM ‘requires_approval` pause becomes one interrupt per pending tool call. The interrupt is the only thing that ties a client resume entry to a tool call, and it is deliberately narrow:

An interrupt is not a permission grant. The host re-authorizes the actor before the tool runs, whatever this record says.

Constants

APPROVED

The actor approved the tool call.

CANCELLED

The actor abandoned the interrupt.

DECISIONS

The statuses that count as an answer.

DENIED

The actor declined the tool call.

PENDING

No decision yet.

REASON_TOOL_CALL

The protocol’s reason for a tool approval.

STATUSES

Attributes

arguments_digest[R]

The SHA-256 digest of the arguments RubyLLM asked to run. Compared before execution so changed arguments cannot reuse an approval.

created_at[R]

When the interrupt was created.

decided_at[R]

When the decision was recorded, or nil while pending.

expires_at[R]

When the interrupt stops being answerable.

id[R]

An id unique among the interrupts of one thread. A resume entry answers an interrupt by this id, so it must be unguessable enough that a client cannot answer someone else’s approval.

message[R]

A human-readable prompt for whoever answers.

reason[R]

Why the run stopped. An open protocol string.

status[R]

:pending, :approved, :denied or :cancelled.

tool_call_id[R]

The RubyLLM tool call this interrupt concerns.

Public Class Methods

create(tool_call_id:, arguments_digest:, expires_at:, reason: REASON_TOOL_CALL, message: nil, created_at: nil) click to toggle source

Builds an interrupt with an unguessable id.

# File lib/ruby_llm/ag_ui/interrupt.rb, line 85
def self.create(tool_call_id:, arguments_digest:, expires_at:, reason: REASON_TOOL_CALL, message: nil,
                created_at: nil)
  new(
    id: SecureRandom.uuid, tool_call_id: tool_call_id, arguments_digest: arguments_digest,
    expires_at: expires_at, reason: reason, message: message, created_at: created_at
  )
end
from_storage(hash) click to toggle source

Rebuilds an interrupt stored by to_storage.

# File lib/ruby_llm/ag_ui/interrupt.rb, line 179
def self.from_storage(hash)
  new(
    id: hash.fetch('id'), tool_call_id: hash.fetch('toolCallId'),
    arguments_digest: hash.fetch('argumentsDigest'), reason: hash.fetch('reason'),
    message: hash['message'], expires_at: parse_time(hash['expiresAt']),
    status: hash.fetch('status').to_sym, decided_at: parse_time(hash['decidedAt']),
    created_at: parse_time(hash['createdAt'])
  )
end
new(id:, tool_call_id:, arguments_digest:, expires_at:, reason: REASON_TOOL_CALL, message: nil, status: PENDING, decided_at: nil, created_at: nil) click to toggle source

rubocop:disable Metrics/ParameterLists – one keyword per field the value object owns

# File lib/ruby_llm/ag_ui/interrupt.rb, line 70
def initialize(id:, tool_call_id:, arguments_digest:, expires_at:, reason: REASON_TOOL_CALL,
               message: nil, status: PENDING, decided_at: nil, created_at: nil)
  @id = id
  @tool_call_id = tool_call_id
  @arguments_digest = arguments_digest
  @expires_at = expires_at
  @reason = reason
  @message = message
  @status = assert_status!(status)
  @decided_at = decided_at
  @created_at = created_at
  freeze
end
parse_time(value) click to toggle source

The time value names, or nil. Storage writes ISO 8601 strings.

# File lib/ruby_llm/ag_ui/interrupt.rb, line 190
def self.parse_time(value)
  value.nil? ? nil : Time.iso8601(value)
end

Public Instance Methods

answerable?(now) click to toggle source

Whether the interrupt can still be answered at now.

# File lib/ruby_llm/ag_ui/interrupt.rb, line 125
def answerable?(now)
  pending? && !expired?(now)
end
approved?() click to toggle source

Whether the actor approved the call.

# File lib/ruby_llm/ag_ui/interrupt.rb, line 105
def approved?
  status == APPROVED
end
cancelled?() click to toggle source

Whether the actor abandoned the interrupt.

# File lib/ruby_llm/ag_ui/interrupt.rb, line 115
def cancelled?
  status == CANCELLED
end
decide(decision, now:) click to toggle source

Returns a copy carrying decision. Refuses to overwrite an existing decision: a second answer is a replay or a conflict, never a new grant.

# File lib/ruby_llm/ag_ui/interrupt.rb, line 131
def decide(decision, now:)
  raise ConfigurationError, "decision must be one of #{DECISIONS.join(', ')}" unless DECISIONS.include?(decision)
  raise StreamError, "interrupt #{id} was already #{status}" if decided?

  self.class.new(
    id: id, tool_call_id: tool_call_id, arguments_digest: arguments_digest, expires_at: expires_at,
    reason: reason, message: message, status: decision, decided_at: now, created_at: created_at
  )
end
decided?() click to toggle source

Whether a decision has been recorded.

# File lib/ruby_llm/ag_ui/interrupt.rb, line 100
def decided?
  DECISIONS.include?(status)
end
denied?() click to toggle source

Whether the actor declined the call.

# File lib/ruby_llm/ag_ui/interrupt.rb, line 110
def denied?
  status == DENIED
end
expired?(now) click to toggle source

Whether the host-owned expiry has passed at now.

# File lib/ruby_llm/ag_ui/interrupt.rb, line 120
def expired?(now)
  !expires_at.nil? && now >= expires_at
end
pending?() click to toggle source

Whether the interrupt still waits for an answer.

# File lib/ruby_llm/ag_ui/interrupt.rb, line 95
def pending?
  status == PENDING
end
to_h() click to toggle source

The interrupt as plain data, with camelCase keys. The arguments digest and the caller-supplied ids stay out of it: they are host bookkeeping.

# File lib/ruby_llm/ag_ui/interrupt.rb, line 155
def to_h
  {
    'id' => id,
    'toolCallId' => tool_call_id,
    'reason' => reason,
    'message' => message,
    'expiresAt' => iso8601(expires_at),
    'status' => status.to_s,
    'decidedAt' => iso8601(decided_at)
  }
end
to_protocol() click to toggle source

The protocol’s Interrupt shape, ready for a RUN_FINISHED interrupt outcome. Only the fields the pinned schema defines are set.

# File lib/ruby_llm/ag_ui/interrupt.rb, line 143
def to_protocol
  ::AgUiProtocol::Core::Types::Interrupt.new(
    id: id,
    reason: reason,
    message: message,
    tool_call_id: tool_call_id,
    expires_at: expires_at&.utc&.iso8601
  )
end
to_storage() click to toggle source

The interrupt in the form a durable store keeps, which to_h is not: a store that dropped the arguments digest could not re-verify that a resume answers the call it approved, and one that dropped the creation time could not order two interrupts on a thread.

# File lib/ruby_llm/ag_ui/interrupt.rb, line 171
def to_storage
  to_h.merge(
    'argumentsDigest' => arguments_digest,
    'createdAt' => iso8601(created_at)
  )
end