soma.cell

Self-auditing agent: it inspects its own execution trace

Demonstrates: trace() returning the structured log of every think()/tool/ approval, clear_trace() resetting it, and using the trace length as an observable the agent reports back. No network needed. Run: SOMA_LLM_MOCK=echo soma run trace_audit.cell --signal investigate "fraud" Test: SOMA_LLM_MOCK=echo soma test trace_audit.cell

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

How it is built

The cell Auditor declares 1 signal in its face: investigate(topic: String) -> Map. Its state machine probe starts in idle and allows 4 transitions: idle -> gathering, gathering -> reporting, reporting -> done and * -> failed. done and failed are terminal. Handlers: fetch (line 23) and investigate (line 27).

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

trace_audit.cell

// Self-auditing agent: it inspects its own execution trace.
// Demonstrates: trace() returning the structured log of every think()/tool/
// approval, clear_trace() resetting it, and using the trace length as an
// observable the agent reports back. No network needed.
//
// Run:  SOMA_LLM_MOCK=echo soma run trace_audit.cell --signal investigate "fraud"
// Test: SOMA_LLM_MOCK=echo soma test trace_audit.cell

cell agent Auditor {
    face {
        signal investigate(topic: String) -> Map
        tool fetch(source: String) -> String "Fetch a record from a source"
    }

    state probe {
        initial: idle
        idle      -> gathering
        gathering -> reporting
        reporting -> done
        *         -> failed
    }

    on fetch(source: String) {
        "record[{source}]"
    }

    on investigate(topic: String) {
        set_budget(3000)
        clear_trace()
        let id = to_string(next_id())

        transition(id, "gathering")
        let a = try { think("Investigate {topic}; use fetch to gather records.") }
        if a.error != () {
            transition(id, "failed")
            return map("status", "failed", "error", a.error)
        }

        transition(id, "reporting")
        let b = try { think("Summarize the records you gathered.") }
        if b.error != () {
            transition(id, "failed")
            return map("status", "failed", "error", b.error)
        }

        transition(id, "done")
        let log = trace()
        // Two think() calls → at least two trace entries.
        map("status", "done", "topic", topic, "trace_entries", len(log))
    }
}

cell test AuditTests {
    rules {
        assert investigate("fraud").status == "done"
        assert investigate("fraud").trace_entries >= 2
    }
}

What the toolchain says

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

$ soma check trace_audit.cell
✓ All checks passed.
$ soma test trace_audit.cell
test AuditTests ...
  ✓ assert investigate("fraud").status == "done"
  ✓ assert investigate("fraud").trace_entries >= 2

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

State machine 'probe': 5 states, initial 'idle'
  States: [done, failed, gathering, idle, reporting]

  ✓ 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 done -> failed — so done is not final. If it should stay final, write `* -> failed except [done]`
  ✓ think-isolated: CTL safety properties hold regardless of LLM output (1 handlers, 5 literal transitions, 0 dynamic)
  ✓ termination: all 2 handlers structurally terminate
  ✓ refinement: handler `investigate` ⟶ {gathering, failed [if a.error != ()], reporting, failed [if b.error != ()], done}
  · transition sources: `investigate` 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)
  ✓ effects: handler `investigate` calls think() at 3 sites (the cost line counts loop rounds) — can dispatch [fetch]

10 passed, 1 warnings, 0 failures

  Temporal properties for 'probe':
  ✓ 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

clear_trace, len, map, next_id, set_budget, think, to_string, trace, transition

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