RECIPE / RETRY TRIAGE
Classify a failure before you retry it.
A heartbeat timeout and a broken request call for different moves. Retrying everything wastes spend and can repeat side effects. Failing on everything throws away work that would have recovered. Retry triage asks two bounded questions about the failure. Your code then picks the move.
Try it in the playground with the Retry triage tab.
The request
Put the failure in state: where it happened, the status or exception type, the error text, and anything that changes the answer, such as the time left on a lease or the attempt number. Leave out tokens, keys, and other secrets.
curl https://api.verdictdecision.com/v1/decisions \
-H "Authorization: Bearer $VERDICT_DECISION_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
--data '{
"model": "laya-typed-decisions",
"state": {
"surface": "agent executor",
"status": "TimeoutError",
"error": "Heartbeat to the coordinator timed out after 10 s.",
"lease_seconds_remaining": 71,
"attempt": 1
},
"questions": {
"failure_class": {
"type": "choice",
"instructions": "Classify this failure to decide how the caller should respond.",
"criteria": {
"transient": "Temporary; the same request is likely to succeed shortly",
"deterministic": "Will fail again until code, state, or input changes",
"outcome_unknown": "The request may have completed upstream; the result was not observed",
"auth_or_config": "Credentials, permissions, or configuration are wrong"
}
},
"safe_to_resend": {
"type": "proposition",
"instructions": "Is resending the identical request with the same Idempotency-Key likely to succeed without duplicate side effects?"
}
}
}'Failure classes
transient: Temporary; the same request is likely to succeed shortly.deterministic: Will fail again until code, state, or input changes.outcome_unknown: The request may have completed upstream; the result was not observed.auth_or_config: Credentials, permissions, or configuration are wrong.
The safe_to_resend proposition asks whether sending the same request again with the same Idempotency-Key is likely to succeed without repeating side effects.
Map the answers to a move
Keep the mapping in your code, where you can read and test it. The thresholds below are starting points. Initial abstention thresholds are not yet fitted, so check abstained and send those cases to a person.
// Your policy, not the model's. Permissions and tests stay authoritative.
function nextMove(answers) {
const kind = answers.failure_class;
const resend = answers.safe_to_resend;
if (kind.abstained || resend.abstained) return "show_to_user";
if (kind.decision === "auth_or_config") return "stop_and_reauthenticate";
if (kind.decision === "outcome_unknown") return "reconcile_then_decide";
if (kind.decision === "transient" && resend.probability >= 0.7) return "retry_same_key";
return "stop_and_show_reason";
}def next_move(answers):
kind = answers["failure_class"]
resend = answers["safe_to_resend"]
if kind["abstained"] or resend["abstained"]:
return "show_to_user"
if kind["decision"] == "auth_or_config":
return "stop_and_reauthenticate"
if kind["decision"] == "outcome_unknown":
return "reconcile_then_decide"
if kind["decision"] == "transient" and resend["probability"] >= 0.7:
return "retry_same_key"
return "stop_and_show_reason"- retry_same_key: resend with the same Idempotency-Key, inside whatever lease or deadline you already hold.
- reconcile_then_decide: the first request may have finished. Look up its result before you spend a second run.
- stop_and_show_reason: show the real cause. A message like “connection interrupted” for a 409 hides the fix from the user.
- stop_and_reauthenticate: retrying with a revoked key cannot succeed.
What the live model returned
We sent the request above and five failures from our own harness test to laya-typed-decisions (then called verdict-laya-typed-1) on the live API on 2026-09-29. Every failure_class answer abstained, so the policy above sends all six to a person. safe_to_resend was false or abstained every time, so nothing would have been resent blind.
- Heartbeat timeout, 71 s left on the lease:
transient0.38 overdeterministic0.31, abstained. Resend false (0.25). - Claim heartbeat timeout:
deterministic0.33 overtransient0.31, abstained. Resend false (0.16). - Progress sequence regressed, HTTP 500: all four classes between 0.24 and 0.27, abstained. Resend false (0.26).
- HTTP 409
stale_context:deterministic0.29, abstained. Resend abstained (0.37). - HTTP 504 after dispatch:
outcome_unknown0.46, abstained. Resend abstained (0.44). - HTTP 401 revoked key:
auth_or_config0.41, abstained. Resend false (0.17).
The leading class matched the expected one for the 409, the 504 and the 401, and missed the heartbeat timeout and the sequence error. It never led by enough to act on. Warm calls took about 0.23 s on the worker.
Choosing a model
laya-typed-decisions (aliases laya-typed, verdict-laya-typed-1) is the 421M triage model. It is fast, but on the failures above it abstained every time, so treat its answers as a first pass and keep the abstention branch. Use decider-4b-v2 when the state includes a long trace or several earlier failures.
Limits
A probability is an assessment. It does not grant permission to retry and does not show that a retry is safe. Apply your own permission and idempotency rules. If the Decision call itself fails or times out, fall back to your existing retry rule instead of blocking the caller.