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.



56
57
58
# File 'lib/silas/ledger.rb', line 56

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

Settle EVERY invocation in the step, then report whether the turn must park. When the model emits parallel tool calls, an ungated one must not be blocked by a gated sibling's approval — and it wouldn't be protected anyway: an unsettled sibling executes on resume regardless of whether the human approves or declines the gated call, so stopping at the first park only delays independent work. Execute what can proceed; park if any invocation is awaiting a human.



44
45
46
47
48
49
50
# File 'lib/silas/ledger.rb', line 44

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