Module: Silas::Ledger

Defined in:
lib/silas/ledger.rb

Overview

The three-state tool executor — the exactly-once machinery the spike proved.

settle!(step, resolver:) walks the step's invocations in creation order and drives each to a terminal state. Returns :completed when every invocation settled, or :parked when one is awaiting a human (approval or in-doubt).

State machine per invocation:

pending  --approval fires--> pending + approval_state=required  (PARK)
pending  --transactional-->  completed        (tool + row in ONE txn)
pending  --at_most_once/idempotent--> started --> completed
started  --found on replay, at_most_once--> in_doubt            (PARK)
started  --found on replay, idempotent-->  re-run --> completed
completed --> no-op (replay feeds the persisted result forward)

Concurrency: claims are compare-and-swap UPDATEs (status='pending' in the WHERE clause), so two executions racing the same invocation cannot both run the tool. The unique (step_id, tool_call_id) index backstops row creation.

Constant Summary collapse

GUARD_KEY =
:silas_ledger_transaction

Class Method Summary collapse

Class Method Details

.assert_no_checkpoint!Object



28
29
30
31
32
33
34
35
# File 'lib/silas/ledger.rb', line 28

def assert_no_checkpoint!
  return unless in_transaction?

  raise CheckpointInLedgerError,
        "A continuation checkpoint was attempted inside a ledger transaction. " \
        "Checkpoints raise Interrupt, which would roll back tool side effects " \
        "the continuation believes are committed."
end

.execute_invocation!(invocation, resolver:) ⇒ Object

Drive a SINGLE freshly-created invocation to a terminal state — the :agent_sdk MCP endpoint creates one invocation per tools/call and needs exactly the same exactly-once/effect-mode machinery as settle!. Returns :done or :parked; the invocation carries its .result.



49
50
51
# File 'lib/silas/ledger.rb', line 49

def execute_invocation!(invocation, resolver:)
  settle_invocation!(invocation, resolver)
end

.in_transaction?Boolean

True while a ledger transaction is open on this thread. A continuation checkpoint inside would raise Interrupt and roll back committed-looking progress (spike finding #5) — AgentLoopJob asserts against this.

Returns:

  • (Boolean)


26
# File 'lib/silas/ledger.rb', line 26

def in_transaction? = Thread.current[GUARD_KEY] == true

.settle!(step, resolver:) ⇒ Object



37
38
39
40
41
42
43
# File 'lib/silas/ledger.rb', line 37

def settle!(step, resolver:)
  step.tool_invocations.order(:id).each do |invocation|
    outcome = settle_invocation!(invocation, resolver)
    return :parked if outcome == :parked
  end
  :completed
end