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 hosted MCP endpoint (Mcp::Handler) 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 in this execution context.
-
.settle!(step, resolver:) ⇒ Object
Settle EVERY invocation in the step, then report whether the turn must park.
Class Method Details
.assert_no_checkpoint! ⇒ Object
34 35 36 37 38 39 40 41 |
# File 'lib/silas/ledger.rb', line 34 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 hosted MCP endpoint (Mcp::Handler) 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.
62 63 64 |
# File 'lib/silas/ledger.rb', line 62 def execute_invocation!(invocation, resolver:) settle_invocation!(invocation, resolver) end |
.in_transaction? ⇒ Boolean
True while a ledger transaction is open in this execution context. A continuation checkpoint inside would raise Interrupt and roll back committed-looking progress (spike finding #5) — AgentLoopJob asserts against this. Stored in IsolatedExecutionState (not Thread.current, which is fiber-local) so the guard follows the app's configured isolation level, exactly like Silas.current_scope — under the default :thread isolation it survives into internally-created fibers (enumerators, streaming bodies) where a fiber-local flag would silently vanish.
32 |
# File 'lib/silas/ledger.rb', line 32 def in_transaction? = ActiveSupport::IsolatedExecutionState[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.
50 51 52 53 54 55 56 |
# File 'lib/silas/ledger.rb', line 50 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 |