Threshold — a generic voice-agent stress test
Threshold is a fictional voice-based screening/intake platform — the kind of thing that could sit under hiring pre-screens, research-participant recruitment, or program eligibility checks. It's deliberately horizontal: the point isn't the business, it's the risk shape a live, unsupervised conversation with a real person creates, and whether Stoa's detectors hold up against it. Eleven agents, MCP client + server + two deliberate MCP detection gaps, one finding planted per Stoa risk dimension, and a genuine participant-incentive payout agent so all 14 assurance-packet areas — including Economic authority — have real content, not an empty box. The full source is in examples/threshold-voice.
Architecture
caller (voice) → orchestrator → conversation_agent → intake MCP server
↓ ↑
extraction_agent escalation_agent
↓ ↓
(database) (database, no gate)
↓
notifier → analytics
The agents
| Agent | Framework | Planted to exercise |
|---|---|---|
orchestrator | hand-rolled | AI001 — voice transcript → prompt |
conversation_agent | LangGraph | AI005 floating-alias; declares a data class Stoa can't scan for |
extraction_agent | hand-rolled | AI002/sql, CTRL005 — lands indeterminate on the autonomy ladder |
escalation_agent | CrewAI | AI002/sql, AI003 — inferred unrestricted_autonomous, declared human_approved → DECL001 |
incentive_agent | LangChain | pays participants via Stripe (move_funds), a real spend ceiling → inferred bounded_autonomous — undeclared → DECL003 |
consent_manager | LangChain | the well-controlled baseline — zero contradictions |
notifier | hand-rolled (not agentic) | AI006 — participant email → third-party analytics |
intake_tools | MCP server | a genuine agentic surface in its own right |
calendar_client | pure MCP client (py) | invisible on purpose — no other agentic signal |
widget_client | pure MCP client (ts) | misclassified on purpose — labeled "MCP server" despite being a client |
All four autonomy-ladder levels, one fixture
| Level | Agent | Why |
|---|---|---|
recommend_only | consent_manager, conversation_agent, orchestrator | no side-effecting path from model output, or no other correlating signal |
bounded_autonomous | incentive_agent | a real hardcoded spend ceiling, no approval gate |
unrestricted_autonomous | escalation_agent | high-impact database write, no approval, no bounding |
indeterminate | extraction_agent | a real sink and a real high-impact capability, but no tool-binding evidence — the classifier won't guess |
human_approved is the one level not demonstrated here — Meridian's payments.py
already covers it.
Two discovered detection gaps
Building this fixture surfaced two real gaps worth knowing about — the point of a stress-test fixture is to make gaps concrete, not paper over them.
MCP client vs. server isn't disambiguated. intake_tools.py is a
genuine server; widget_client.ts only imports the SDK's client subpath. Both get
frameworks=["mcp"] and render identically as an MCP-server node, because the JS/TS
trigger is a bare substring match on @modelcontextprotocol/sdk with no path
awareness. calendar_client.py shows the inverse on the Python side: pure client glue
with no other agentic signal never clears the agentic-signal floor at all.
A declared data class can be scan-blind. conversation_agent
accurately declares data_classes = ["personal"]. DECL004 (undeclared data class)
only cross-checks the "authentication" class today, tied to a leaked-secret signal —
there's no scan-side pattern for "this code touches personal data" at all. The declaration is
accurate and will never be contradicted, correct or not.
stoa scan examples/threshold-voice, or read the
full README
for the dimension-coverage map. The
run-e2e.sh
driver asserts 26 checks, including both MCP gaps and the DECL001/DECL003 contradictions.