Reflective tool-dispatch agent: routes a named action to the right tool
Demonstrates: several `tool` declarations, a manual dispatcher that mirrors what think() does automatically (so the routing logic is unit-testable), and an unknown-action path that lands in failed. Run: SOMA_LLM_MOCK=echo soma run tool_dispatcher.cell --signal act "reverse" "abc" Test: SOMA_LLM_MOCK=echo soma test tool_dispatcher.cell
A Soma program from the agents part of the verified corpus: 77 lines, cells Toolbox, ToolboxTests. It passes soma check with soma 3.2.4.
How it is built
The cell Toolbox declares 2 signals in its face: act(action: String, arg: String) -> Map and ask(prompt: String) -> Map. Its state machine task starts in idle and allows 3 transitions: idle -> running, running -> done and * -> failed. done and failed are terminal. Handlers: upper (line 25), reverse_str (line 26), wordcount (line 31), act (line 35) and ask (line 52).
The file also holds 1 test cell (ToolboxTests) with 7 assertions, run by soma test.
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"]tool_dispatcher.cell
// Reflective tool-dispatch agent: routes a named action to the right tool.
// Demonstrates: several `tool` declarations, a manual dispatcher that mirrors
// what think() does automatically (so the routing logic is unit-testable),
// and an unknown-action path that lands in failed.
//
// Run: SOMA_LLM_MOCK=echo soma run tool_dispatcher.cell --signal act "reverse" "abc"
// Test: SOMA_LLM_MOCK=echo soma test tool_dispatcher.cell
cell agent Toolbox {
face {
signal act(action: String, arg: String) -> Map
signal ask(prompt: String) -> Map
tool upper(s: String) -> String "Uppercase a string"
tool reverse_str(s: String) -> String "Reverse a string"
tool wordcount(s: String) -> String "Count words"
}
state task {
initial: idle
idle -> running
running -> done
* -> failed
}
on upper(s: String) { uppercase(s) }
on reverse_str(s: String) {
let cs = list()
for i in range(0, len(s)) { cs = push(cs, s[len(s) - 1 - i]) }
join(cs, "")
}
on wordcount(s: String) { to_string(len(split(s, " "))) }
// Manual dispatcher: same shape as think()'s auto tool-calling, but
// deterministic and testable.
on act(action: String, arg: String) {
let id = to_string(next_id())
transition(id, "running")
let result = match action {
"upper" -> upper(arg)
"reverse" -> reverse_str(arg)
"count" -> wordcount(arg)
_ -> ()
}
if result == () {
transition(id, "failed")
return map("status", "failed", "error", "unknown action: {action}")
}
transition(id, "done")
map("status", "done", "action", action, "result", result)
}
on ask(prompt: String) {
set_budget(2000)
let id = to_string(next_id())
transition(id, "running")
let r = try { think("Use a tool to handle: {prompt}") }
if r.error != () {
transition(id, "failed")
return map("status", "failed", "error", r.error)
}
transition(id, "done")
map("status", "done", "answer", r.value)
}
}
cell test ToolboxTests {
rules {
assert upper("abc") == "ABC"
assert reverse_str("abc") == "cba"
assert wordcount("a b c") == "3"
assert act("reverse", "hello").result == "olleh"
assert act("upper", "hi").result == "HI"
assert act("bogus", "x").status == "failed"
assert ask("anything").status == "done"
}
}What the toolchain says
$ soma check tool_dispatcher.cell
✓ All checks passed.$ soma test tool_dispatcher.cell
test ToolboxTests ...
✓ assert upper("abc") == "ABC"
✓ assert reverse_str("abc") == "cba"
✓ assert wordcount("a b c") == "3"
✓ assert act("reverse", "hello").result == "olleh"
✓ assert act("upper", "hi").result == "HI"
✓ assert act("bogus", "x").status == "failed"
✓ assert ask("anything").status == "done"
7 tests: 7 passed, 0 failed$ soma verify tool_dispatcher.cell
Verifying tool_dispatcher.cell...
note: soma.toml [verify] cells names 'Solver', which this file does not define — its properties are not checked here
State machine 'task': 4 states, initial 'idle'
States: [done, failed, idle, running]
✓ 4 states, 5 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 (2 handlers, 6 literal transitions, 0 dynamic)
✓ termination: all 5 handlers structurally terminate
✓ refinement: handler `act` ⟶ {running, failed [if result == ()], done}
✓ refinement: handler `ask` ⟶ {running, failed [if r.error != ()], done}
· transition sources: `act`, `ask` call 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 `ask` calls think() at 2 sites (the cost line counts loop rounds) — can dispatch [reverse_str, upper, wordcount]
11 passed, 1 warnings, 0 failures
Temporal properties for 'task':
✓ 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 OKBuiltins used
join, len, list, map, next_id, push, range, set_budget, split, think, to_string, transition, uppercase