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
- .assert_no_checkpoint! ⇒ Object
-
.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!.
-
.in_transaction? ⇒ Boolean
True while a ledger transaction is open on this thread.
-
.settle!(step, resolver:) ⇒ Object
Settle EVERY invocation in the step, then report whether the turn must park.
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.
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 |