soma.cell

Support-ticket triage agent

Demonstrates: a verified lifecycle state machine, set_budget cap, tool handlers with deterministic keyword classification (no LLM call — see services/refund_agent.cell for think() + validated JSON output), and a test cell that runs without any network. Run: SOMA_LLM_MOCK=echo soma run triage_agent.cell --signal triage "login broken" Test: SOMA_LLM_MOCK=echo soma test triage_agent.cell

A Soma program from the agents part of the verified corpus: 65 lines, cells Triage, TriageTests. It passes soma check with soma 3.2.4.

How it is built

The cell Triage declares 1 signal in its face: triage(text: String) -> Map; keeps 1 memory slot: tickets (Map<String, Map>, persistent). Its state machine lifecycle starts in idle and allows 4 transitions: idle -> reading, reading -> classifying, classifying -> routed and * -> failed. failed and routed are terminal. Handlers: classify (line 30) and triage (line 37).

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

triage_agent.cell

// Support-ticket triage agent.
// Demonstrates: a verified lifecycle state machine, set_budget cap, tool
// handlers with deterministic keyword classification (no LLM call — see
// services/refund_agent.cell for think() + validated JSON output), and a
// test cell that runs without any network.
//
// Run:  SOMA_LLM_MOCK=echo soma run triage_agent.cell --signal triage "login broken"
// Test: SOMA_LLM_MOCK=echo soma test triage_agent.cell

cell agent Triage {
    face {
        signal triage(text: String) -> Map
        tool classify(text: String) -> String "Classify ticket urgency: low, medium, high"
        promise "agent always reaches routed or failed"
    }

    memory {
        tickets: Map<String, Map> [persistent]
    }

    state lifecycle {
        initial: idle
        idle      -> reading
        reading   -> classifying
        classifying -> routed
        *         -> failed
    }

    // Deterministic tool: keyword-based urgency, no LLM needed.
    on classify(text: String) {
        let t = lowercase(text)
        if contains(t, "broken") || contains(t, "down") { return "high" }
        if contains(t, "slow") { return "medium" }
        "low"
    }

    on triage(text: String) {
        set_budget(2000)
        // Per-ticket state-machine instance, so each triage runs its own
        // verified lifecycle from idle to routed.
        let id = to_string(next_id())
        transition(id, "reading")

        transition(id, "classifying")
        let urgency = classify(text)

        let queue = if urgency == "high" { "escalation" } else { "standard" }
        tickets.set(id, map("id", id, "text", text, "urgency", urgency, "queue", queue))

        transition(id, "routed")
        map("id", id, "urgency", urgency, "queue", queue, "status", "routed")
    }
}

cell test TriageTests {
    rules {
        assert classify("the server is down") == "high"
        assert classify("page loads slow") == "medium"
        assert classify("how do I rename a file") == "low"
        assert triage("login broken").urgency == "high"
        assert triage("login broken").queue == "escalation"
        assert triage("minor typo").status == "routed"
    }
}

What the toolchain says

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

$ soma check triage_agent.cell
note: promise on 'Triage' is documentation (not machine-verifiable): "agent always reaches routed or failed"
  --> triage_agent.cell:14:17
   |
14 |         promise "agent always reaches routed or failed"
   |                 ^
✓ All checks passed (1 note above).
$ soma test triage_agent.cell
test TriageTests ...
  ✓ assert classify("the server is down") == "high"
  ✓ assert classify("page loads slow") == "medium"
  ✓ assert classify("how do I rename a file") == "low"
  ✓ assert triage("login broken").urgency == "high"
  ✓ assert triage("login broken").queue == "escalation"
  ✓ assert triage("minor typo").status == "routed"

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

State machine 'lifecycle': 5 states, initial 'idle'
  States: [classifying, failed, idle, reading, routed]

  ✓ 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 routed -> failed — so routed is not final. If it should stay final, write `* -> failed except [routed]`
  ✓ think-isolated: CTL safety properties hold regardless of LLM output (1 handlers, 3 literal transitions, 0 dynamic)
  ✓ termination: all 2 handlers structurally terminate
  ⚠ refinement: declared transition `* → failed` is never reached by any handler — spec may be aspirational or stale
  ✓ refinement: handler `triage` ⟶ {reading, reading → classifying, routed}
  · transition sources: `triage` 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)

9 passed, 2 warnings, 0 failures

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

contains, lowercase, map, next_id, set_budget, to_string, transition

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