The Meshy → meshcheck loop
Generate with Meshy, validate with meshcheck, read the failing checks, regenerate. A closed loop an agent can run unattended.
This is a pattern for wiring a text-to-3D generator to a deterministic QA gate. The shape is the same for any generator; only the generation call changes. The point: an agent decides whether to accept, retry, or escalate a model from the report alone — no human in the loop.
The report blocks below are real meshcheck output. The generator step stands in for your Meshy call — swap
in your own GLB and the meshcheck calls are identical. Each report is reproducible against the named corpus
fixture with meshcheck-corpus check <file> --profile web (see the reproduce lines).
The loop
- Generate. Ask Meshy for an asset and download the GLB to disk.
- Validate.
validate_model({ path, profile: "web" }). This runschecks_only— the fast, 1-credit path that skips the renderer. Addmode: "full"when you also want screenshots. - Branch on the verdict.
pass— accept the asset.warn— accept, but record the warnings for review.fail— read the failing checks, translate them into generation constraints, and retry.
- Inspect (optional). Once the measurable checks pass, ask
/inspectwhether the model actually looks like what you asked for. This is the one non-deterministic step; keep it separate from the gate. - Stop on
pass, or after a retry budget (say, three attempts), then escalate to a human.
What a failing report tells the agent
meshcheck failures are structured, so the agent maps a check ID to a concrete next action instead of guessing:
| Failing check | What it measured | The retry constraint |
|---|---|---|
PERF-001 |
Triangle count over the profile’s max_tris |
Lower the target poly count, or enable decimation |
MAT-001 |
Texture dimension over max_texture_dim |
Request a smaller texture resolution |
GEO-003 |
Flipped winding past the threshold | Re-export with consistent winding / recompute normals |
UV-001 |
Textured mesh missing TEXCOORD_0 |
Regenerate with UVs, or drop the texture |
Worked example: a budget failure
Attempt one comes back over the web triangle budget. This is real output for the corpus fixture
Box__explode_tris.glb (an over-tessellated export standing in for a raw generation):
validate_model({ path: "./attempt-1.glb", profile: "web" })
→ verdict: "fail"
PERF-001 fail 196,608 triangles exceeds profile budget of 25,000
PERF-002 warn 196,620 vertices exceeds profile budget of 40,000
(all other checks pass or skip)
# reproduce
meshcheck-corpus check corpus/broken/Box__explode_tris.glb --profile web
The agent reads PERF-001, sets a triangle target (or enables decimation), and regenerates. Attempt two
is within budget — real output for the clean Box.glb:
validate_model({ path: "./attempt-2.glb", profile: "web" })
→ verdict: "pass"
summary: 0 errors, 0 warnings, 9 info, 14 skipped
PERF-001 pass
# reproduce
meshcheck-corpus check corpus/clean/Box.glb --profile web
(The 14 skipped are the render checks and any UV/material checks that don’t apply — this run was
checks_only, so the RND-* family reports skipped rather than running the renderer.)
A texture-budget failure looks the same. Real output for Duck__upsize_textures.glb on the web profile:
validate_model({ path: "./textured.glb", profile: "web" })
→ verdict: "fail"
MAT-001 fail largest texture is 8192px, exceeding the profile budget of 2048px
MAT-002 warn estimated texture memory 357,019,156 bytes exceeds the profile budget of 33,554,432 bytes
The agent’s retry constraint is unambiguous: request a texture at or under 2048px.
Add a semantic check with /inspect
The checks above are deterministic measurements — they never judge whether the model looks right. Once
the budget checks pass, /inspect closes that gap by asking a vision model, over the standard render
angles, whether the asset matches your prompt:
inspect_model({
path: "./attempt-2.glb",
check: "VIS-001",
prompt: "low-poly wooden crate, game prop"
})
The response is schema-constrained and flagged non-deterministic (this is the only such surface — it costs
5 credits and carries "deterministic": false and the model version):
{
"inspect_id": "ins_...",
"deterministic": false,
"model": "claude-haiku-4-5",
"answer": {
"match": true,
"confidence": "high",
"mismatches": [],
"notes": "..."
}
}
Confidence is categorical (low | medium | high), never a number — numbers would imply a precision a
vision opinion doesn’t have. Keep the measurable and the semantic judgments separate: the first is
deterministic and gates the pipeline; the second is advisory.
Notes
- The full report (
mode: "full") carries the renderer’s screenshots — six standard angles on signed, expiring URLs. An agent can hand those straight to/inspect, or reuse them withrender_id. - Because
/validateis deterministic, cache the report by the model’ssha256and skip re-validating an unchanged asset.