soma.cell

Bounded-retry agent — the verified-termination poster child

A naive `failed -> working` retry arrow creates a livelock the model checker would reject. Here the retry COUNT lives in memory and the loop is bounded, so the machine always reaches succeeded or exhausted. Run: SOMA_LLM_MOCK=echo soma run bounded_retry.cell --signal attempt "flaky-op" 2 Test: SOMA_LLM_MOCK=echo soma test bounded_retry.cell

A Soma program from the agents part of the verified corpus: 60 lines, cells Retrier, RetryTests. It passes soma check with soma 3.2.4.

How it is built

The cell Retrier declares 1 signal in its face: attempt(name: String, fail_times: Int) -> Map. Its state machine run starts in idle and allows 4 transitions: idle -> working, working -> succeeded, working -> exhausted and * -> failed. exhausted, failed and succeeded are terminal. Handlers: attempt (line 24).

The file also holds 1 test cell (RetryTests) with 5 assertions, run by soma test.

Generated from soma describe on this file.

The source

soma.toml

[package]
name = "agents-corpus"
version = "0.1.0"
description = "Verified AI-agent example cells (training corpus)"

# Agent LLM config. `mock = "echo"` keeps every example in this directory
# hermetic: `soma test` / `soma run` need no network and no API key, and
# results are deterministic. For a live run, delete the `mock` line (or
# override with SOMA_LLM_MOCK) and provide the key via env.
[agent]
provider = "anthropic"
model    = "claude-opus-4-6"
key      = "${ANTHROPIC_API_KEY}"
mock     = "echo"

# Termination proof for verified_termination.cell's `workflow` machine:
# every path eventually reaches a terminal, no deadlocks.
# `cells` scopes these properties to Solver — the other agents in this
# directory have their own state names and must not inherit them.
[verify]
cells = ["Solver"]
deadlock_free = true
eventually    = ["done", "failed"]

[verify.after.understanding]
eventually = ["solving", "failed"]

[verify.after.solving]
eventually = ["verifying", "failed"]

[verify.after.verifying]
eventually = ["done", "failed"]

bounded_retry.cell

// Bounded-retry agent — the verified-termination poster child.
// A naive `failed -> working` retry arrow creates a livelock the model
// checker would reject. Here the retry COUNT lives in memory and the loop
// is bounded, so the machine always reaches succeeded or exhausted.
//
// Run:  SOMA_LLM_MOCK=echo soma run bounded_retry.cell --signal attempt "flaky-op" 2
// Test: SOMA_LLM_MOCK=echo soma test bounded_retry.cell

cell agent Retrier {
    face {
        signal attempt(name: String, fail_times: Int) -> Map
    }

    state run {
        initial: idle
        idle      -> working
        working   -> succeeded
        working   -> exhausted
        *         -> failed
    }

    // Simulates an operation that fails `fail_times`, then succeeds.
    // The loop is bounded by max_tries, so it cannot run forever.
    on attempt(name: String, fail_times: Int) {
        let id = to_string(next_id())
        transition(id, "working")

        let max_tries = 5
        let tries = 0
        let ok = false
        while tries < max_tries {
            tries = tries + 1
            if tries > fail_times {
                ok = true
                break
            }
        }

        if ok {
            transition(id, "succeeded")
            return map("status", "succeeded", "op", name, "tries", tries)
        }
        transition(id, "exhausted")
        map("status", "exhausted", "op", name, "tries", tries)
    }
}

cell test RetryTests {
    rules {
        // Fails twice then succeeds on the 3rd try.
        assert attempt("op", 2).status == "succeeded"
        assert attempt("op", 2).tries == 3
        // Fails more than the bound → exhausted, but still terminates.
        assert attempt("op", 9).status == "exhausted"
        assert attempt("op", 9).tries == 5
        // Succeeds immediately.
        assert attempt("op", 0).tries == 1
    }
}

What the toolchain says

Run on a fresh copy of the program at build time; the outputs are pasted unedited.

$ soma check bounded_retry.cell
✓ All checks passed.
$ soma test bounded_retry.cell
test RetryTests ...
  ✓ assert attempt("op", 2).status == "succeeded"
  ✓ assert attempt("op", 2).tries == 3
  ✓ assert attempt("op", 9).status == "exhausted"
  ✓ assert attempt("op", 9).tries == 5
  ✓ assert attempt("op", 0).tries == 1

5 tests: 5 passed, 0 failed
$ soma verify bounded_retry.cell
Verifying bounded_retry.cell...
note: soma.toml [verify] cells names 'Solver', which this file does not define — its properties are not checked here

State machine 'run': 5 states, initial 'idle'
  States: [exhausted, failed, idle, succeeded, working]

  ✓ 5 states, 7 transitions
  ✓ all states reachable from 'idle'
  ✓ terminal states: [failed]
  ✓ no deadlocks
  ✓ liveness: every state can eventually reach a terminal state
  ✓ wildcard transitions: * -> [failed]
  ⚠ `* -> failed` also adds exhausted -> failed, succeeded -> failed — so exhausted, succeeded are not final. If they should stay final, write `* -> failed except [exhausted, succeeded]`
  ✓ think-isolated: CTL safety properties hold regardless of LLM output (1 handlers, 3 literal transitions, 0 dynamic)
  ⚠ handler `attempt`: while-loop without provable termination bound (write `while [loop_bound(N)] cond { … }`, or a for-loop over a bounded range)
  ⚠ refinement: declared transition `* → failed` is never reached by any handler — spec may be aspirational or stale
  ✓ refinement: handler `attempt` ⟶ {working, succeeded [if ok], exhausted}
  · transition sources: `attempt` calls transition(id, target) without declaring the source — the runtime refuses a move the graph forbids, but the proof does not see where the handler starts; `transition(id, from, to)` names the edge (checked by `soma check`, refused at run time from any other state)

8 passed, 3 warnings, 0 failures

  Temporal properties for 'run':
  ✓ deadlock_free — no deadlocks in any reachable state
soma.toml: 4 user-defined properties loaded — NONE applies to this file ([verify] cells = ["Solver"] names no cell of it)
Temporal: 1 passed, 0 failed
VERIFY OK

Builtins used

map, next_id, to_string, transition

Source file: /corpus/agents/bounded_retry.cell · examples/corpus/agents/bounded_retry.cell