soma.cell

Agent with preconditions and a memory invariant — refuses bad input

Demonstrates: `require ... else` guards on a handler, a memory invariant that rejects out-of-range writes, and assert_fails proving the negative paths actually raise. The agent never enters its work state on bad input. Run: SOMA_LLM_MOCK=echo soma run guarded_agent.cell --signal spend "rent" 50 Test: SOMA_LLM_MOCK=echo soma test guarded_agent.cell

A Soma program from the agents part of the verified corpus: 59 lines, cells Treasurer, TreasurerTests. It passes soma check with soma 3.2.4.

How it is built

The cell Treasurer declares 2 signals in its face: spend(item: String, amount: Int) -> Map and set_limit(n: Int) -> Int; keeps 2 memory slots: ledger (Map<String, Map>, persistent) and limit (Map<String, Int>, persistent) under the invariant limit >= 1 && limit <= 1000. Its state machine txn starts in idle and allows 3 transitions: idle -> approved, approved -> recorded and * -> rejected. recorded and rejected are terminal. Handlers: set_limit (line 29) and spend (line 35).

The file also holds 1 test cell (TreasurerTests) with 6 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"]

guarded_agent.cell

// Agent with preconditions and a memory invariant — refuses bad input.
// Demonstrates: `require ... else` guards on a handler, a memory invariant
// that rejects out-of-range writes, and assert_fails proving the negative
// paths actually raise. The agent never enters its work state on bad input.
//
// Run:  SOMA_LLM_MOCK=echo soma run guarded_agent.cell --signal spend "rent" 50
// Test: SOMA_LLM_MOCK=echo soma test guarded_agent.cell

cell agent Treasurer {
    face {
        signal spend(item: String, amount: Int) -> Map
        signal set_limit(n: Int) -> Int
    }

    memory {
        ledger: Map<String, Map> [persistent]
        limit:  Map<String, Int>    [persistent]
        // Invariant scoped to `limit`: any stored cap stays in [1, 1000].
        invariant limit >= 1 && limit <= 1000
    }

    state txn {
        initial: idle
        idle    -> approved
        approved -> recorded
        *       -> rejected
    }

    on set_limit(n: Int) {
        require n > 0 else limit_must_be_positive
        limit.set("max", n)
        n
    }

    on spend(item: String, amount: Int) {
        // Preconditions guard entry to the work states.
        require amount > 0 else amount_must_be_positive
        require amount <= 1000 else amount_exceeds_cap

        let id = to_string(next_id())
        transition(id, "approved")
        ledger.set(id, map("item", item, "amount", amount))
        transition(id, "recorded")
        map("status", "recorded", "id", id, "item", item, "amount", amount)
    }
}

cell test TreasurerTests {
    rules {
        assert spend("rent", 50).status == "recorded"
        assert spend("rent", 50).amount == 50
        assert set_limit(500) == 500
        // Negative paths never reach the ledger.
        assert_fails spend("hack", -5)
        assert_fails spend("yacht", 99999)
        assert_fails set_limit(0)
    }
}

What the toolchain says

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

$ soma check guarded_agent.cell
✓ All checks passed.
$ soma test guarded_agent.cell
test TreasurerTests ...
  ✓ assert spend("rent", 50).status == "recorded"
  ✓ assert spend("rent", 50).amount == 50
  ✓ assert set_limit(500) == 500
  ✓ assert_fails spend("hack", -5) — raised require failed: amount_must_be_positive: constraint violated
  ✓ assert_fails spend("yacht", 99999) — raised require failed: amount_exceeds_cap: constraint violated
  ✓ assert_fails set_limit(0) — raised require failed: limit_must_be_positive: constraint violated

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

State machine 'txn': 4 states, initial 'idle'
  States: [approved, idle, recorded, rejected]

  ✓ 4 states, 5 transitions
  ✓ all states reachable from 'idle'
  ✓ terminal states: [rejected]
  ✓ no deadlocks
  ✓ liveness: every state can eventually reach a terminal state
  ✓ wildcard transitions: * -> [rejected]
  ⚠ `* -> rejected` also adds recorded -> rejected — so recorded is not final. If it should stay final, write `* -> rejected except [recorded]`
  ✓ think-isolated: CTL safety properties hold regardless of LLM output (1 handlers, 2 literal transitions, 0 dynamic)
  ✓ termination: all 2 handlers structurally terminate
  ⚠ refinement: declared transition `* → rejected` is never reached by any handler — spec may be aspirational or stale
  ✓ refinement: handler `spend` ⟶ {approved, approved → recorded}
  · transition sources: `spend` 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)

Treasurer/invariants:

  ✓ invariant limit >= 1 — writer 'set_limit' proven by induction (writes n)
  ⚠ invariant limit >= 1 && limit <= 1000 — runtime-checked (computed values): set_limit → limit [limit <= 1000] because `n` is only known to lie in [1, ∞) — narrow it: `require n <= 1000 else …`

10 passed, 3 warnings, 0 failures

  Temporal properties for 'txn':
  ✓ 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/guarded_agent.cell · examples/corpus/agents/guarded_agent.cell