Manifold — constrained-random coverage testing for AI agents
Point Manifold at an agent and it generates seeded, fault-injected scenarios, measures functional coverage of a behavior space you declare, and biases new runs toward the gaps — reaching full coverage in roughly 20× fewer scenarios than uniform random, every failure replayable from its exact seed.
Hardware verification answered "how do you test something with too many behaviors to enumerate?" decades ago: don't hand-write cases, generate them under constraints, and measure functional coverage — a declared map of the behaviors that matter — to know what you actually exercised. Manifold ports that discipline to tool-using AI agents. Its one-line creed is the honest one: coverage is evidence of thoroughness, not a proof of correctness.
The problem
Agents are tested the way LLM features usually are: run a few hand-picked scenarios, watch them succeed, ship. That leaves two questions unanswered — what didn't you try? and would it pass again? An agent that works once may retry-storm, loop, or mishandle a tool error on an input you never generated, and a single green run says nothing about a flaky one.
Approach — the UVM loop, aimed at agents
- Declare a behavior space, then sample it. You describe the axes that matter (task types, tool-failure modes, input shapes); Manifold generates seeded random scenarios across them — with fault injection: a tool that errors, times out, returns garbage, or lags.
- Measure functional coverage. Every run reports which bins of the declared space were hit, as an HTML heatmap with named holes — not a pass rate, a map of thoroughness.
- Steer toward the gaps.
--coverage-directedbiases generation toward uncovered bins instead of re-rolling the same easy cases. - Surface flakiness honestly. Re-running a seed k times exposes the pass^k signal — an agent that passes 1-of-2 is flaky, and that variance is data, not noise.
- Make every failure cheap to debug. A failing scenario hands back its exact
seed (
manifold repro <seed>) and a delta-debug shrinker that minimizes it — in one run, a size-9 failing scenario shrank to size 2 (−78%).
Why it's trustworthy — checks, not vibes
Invariants are enforced mechanically: a library of seven generic checks
(terminates within budget, no infinite retry, no duplicate identical calls, and
more) fires against every run, so a "pass" means declared properties held, not
that the output looked fine. Manifold has been live-verified against a real
claude-haiku-4-5 agent and an unmodified off-the-shelf agent from the
claude-agent-sdk (verified unmodified via an in-process tool server). The
coverage-direction claim is quantified rather than asserted: on a 26-bin space,
directed search reaches 90% of achievable coverage at 8 scenarios and 100% at 11,
where uniform random needs 35 and 217 respectively — roughly 4× fewer scenarios
to 90% and ~20× fewer to full coverage at the default seed (median across seeds:
~2.9× and ~16×). And the harness reports its own limits loudly: coverage measures
only the space you declared, so a high number is thoroughness against your model,
never a correctness proof.
The catch it surfaced — an agent framework's retry defaults
Pointed at three third-party agent frameworks running the same live model
(claude-haiku-4-5, same tasks, same fault space), Manifold surfaced a real
reliability difference. Where LangGraph and pydantic-ai's agents passed every
scenario, HuggingFace smolagents' agent failed several — and flakily. The
cause wasn't a crash in smolagents' code or a bug Manifold planted; it was an
interaction of two framework defaults: smolagents feeds each tool error back to
the model with a coaching nudge ("take care not to repeat previous errors!") and
forces a tool call on every step. The same model that gives up gracefully after
about two retries elsewhere would retry-storm here — on one seed, calling a
dead search tool five times in a row and burning its entire step budget, tripping
the terminates-within-budget and no-infinite-retry invariants.
The honest caveats, which are the point: this is one framework, the failure is flaky (roughly one run in two), the sample is small (fifteen scenarios), and the model itself was healthy (a clean 10-of-10 sweep, and 60-of-60 on a dedicated flakiness campaign). It's a defaults story, not a defect indictment — exactly the kind of thing constrained-random coverage is built to catch and a handful of happy-path demos never would.
Status & what's next
The core is complete (generation, fault injection, coverage, directed search,
invariants, flakiness, shrinker) and hardened across two review passes,
live-verified end to end against real agents. Packaged for PyPI as manifold-cov
(the CLI stays manifold). Deliberately open, and stated as such: the coverage
model is only as good as the space you declare; the framework finding deserves a
proper upstream issue with the seeded repro attached; and richer directed
selectors and more built-in invariants are natural next steps, not shipped
claims.