class RubyLLM::Browser::Rails::OperationJob

Active Job wrapper for one bounded browser operation.

The job is deliberately not a scheduler: it never reserves a second operation and never retries an effectful call. The host reserves (or attaches to) a durable operation with {.enqueue} and this job attaches to that exact row. On delivery it leases the row (bumping its fence), refuses the wrong actor or an already-terminal operation, and runs the read-only operation through the {Engine}. A duplicated or resumed delivery finds a non-‘:reserved` row and returns without a remote call; a worker that lost the lease is refused as stale.

The context (backend, source policy, stores, budget, consent) is host-supplied and cannot be serialized, so the job builds it from a configured factory:

RubyLLM::Browser::Rails.context_factory = lambda do |actor_id:, run_id:|
  RubyLLM::Browser::Context.new(
    actor_id: actor_id, run_id: run_id, backend: qualified_backend,
    source_policy: host_policy, operation_store: RubyLLM::Browser::Rails.operation_store
  )
end

Attributes

context_factory[RW]

->(actor_id:, run_id:) { RubyLLM::Browser::Context }

operation_store[RW]

Overrides the module store for this job class. Defaults to the module store.

Public Class Methods

enqueue(actor_id:, run_id:, kind:, url:, budget:, idempotency_key: nil, instruction: nil, schema: nil, max_bytes: nil, store: nil, now: nil) click to toggle source

Reserves (or attaches to) the durable operation for idempotency_key and enqueues exactly one job for it. A second call with the same key attaches to the same operation instead of reserving a new one.

@return [Operation] the reserved or attached operation

# File lib/ruby_llm/browser/rails/operation_job.rb, line 40
def enqueue(actor_id:, run_id:, kind:, url:, budget:, idempotency_key: nil,
            instruction: nil, schema: nil, max_bytes: nil, store: nil, now: nil)
  configured_store = operation_store || RubyLLM::Browser::Rails.operation_store
  store ||= configured_store
  raise InvalidInput, "enqueue must use the job's operation store" unless store.equal?(configured_store)
  now ||= RubyLLM::Browser::Rails.clock.call
  unless %w[inspect extract].include?(kind.to_s)
    raise InvalidInput, "browser jobs support inspect and extract only"
  end
  candidate = Operation.new(
    id: Operation.generate_id, actor_id: actor_id, run_id: run_id, budget: budget,
    state: :reserved, idempotency_key: idempotency_key, created_at: now, updated_at: now,
    request_digest: request_digest(kind: kind, url: url, instruction: instruction,
                                   schema: schema, max_bytes: max_bytes, budget: budget)
  )
  operation = store.attach_or_reserve(candidate)
  perform_later(
    operation_id: operation.id, actor_id: actor_id, run_id: run_id, kind: kind.to_s,
    url: url, instruction: instruction, schema: schema, max_bytes: max_bytes
  )
  operation
end
request_digest(kind:, url:, instruction:, schema:, max_bytes:, budget:) click to toggle source

Binds an idempotency key to the exact host-issued payload and budget.

# File lib/ruby_llm/browser/rails/operation_job.rb, line 64
def request_digest(kind:, url:, instruction:, schema:, max_bytes:, budget:)
  Redactor.digest(kind: kind.to_s, url: url, instruction: instruction, schema: schema,
                  max_bytes: max_bytes, budget: budget.to_h)
end

Public Instance Methods

perform(operation_id:, actor_id:, run_id:, kind:, url:, instruction: nil, schema: nil, max_bytes: nil) click to toggle source
# File lib/ruby_llm/browser/rails/operation_job.rb, line 72
def perform(operation_id:, actor_id:, run_id:, kind:, url:, instruction: nil,
            schema: nil, max_bytes: nil)
  operation = store.fetch(operation_id)
  return :missing if operation.nil?

  assert_scope!(operation, actor_id, run_id)
  digest = self.class.request_digest(kind: kind, url: url, instruction: instruction,
                                     schema: schema, max_bytes: max_bytes, budget: operation.budget)
  if operation.request_digest && operation.request_digest != digest
    raise NotAuthorized, "job payload does not match the reserved request"
  end
  return :cancelled if operation.state == :cancelled
  return :terminal if operation.terminal?
  return :attached unless operation.state == :reserved

  leased = store.lease(operation_id, actor_id: actor_id, run_id: run_id, now: now)
  return :cancelled if leased.state == :cancelled

  result = run(kind, leased, url: url, instruction: instruction, schema: schema,
                             max_bytes: max_bytes)

  # Keep unknown action/start results, even when cleanup later closes a session.
  final = settled(operation_id)
  return :cancelled if final == :cancelled

  result.ok? ? (final || result.to_h) : result.to_h
rescue InvalidInput => e
  raise unless fenced?(e)

  :fenced_out
end