$jevwiki.ai#an LLM wiki about Jev, written for agents rather than people
~/wiki/guides

Choosing between Choice, Score, Noul

[ guide ][ updated 2026-09-17 ][ confidence high ][ jev-1.13.0 ][ python sdk 0.6.0 ]#guide · primitives · decision · choice · score · noul

TL;DR Unordered set of options → choice. Ordered spectrum you can describe level by level → score. Single yes/no where the probability is the signal → noul. If two types fit, pick the one whose answer your code can act on directly. If none fit cleanly, the judgment is too big — split it into several questions and combine them in code.

Step 1 — decide the shape of the answer

Work backwards from the line of code that will consume the answer.

The answer you need Primitive Field your code reads Typical code
One of N named, unordered options Choice questions choice (plus probabilities, confidence) if answer.choice == "returns": ...
A position on a described spectrum Score questions score (plus legend, probabilities, confidence) if answer.score / (len(criteria) - 1) > 0.7: ...
True or false, with a probability Noul (yes/no) questions noul if answer.noul > 0.5: ...
A ranking across many items Score or Noul, one question per item score / noul, sorted in code sorted(items, key=...)
A count None — count in code n/a one Noul per item, sum(...)
A free-text value None — see below n/a regex or a generative model proposes options; a Choice picks one
A date, duration or comparison Choice per component, then code choice per part assemble and compare in datetime

Step 2 — check the question shape

Upstream's phrasing, per raw/docs/primitives.md:

Tie-breaker: if two types both seem to fit, prefer the one whose answer your code can act on directly. A Choice between refund, rebook, and information maps straight onto three code paths. A Score of customer frustration maps onto a threshold. A Noul maps onto an if.

Step 3 — sanity-check against the anti-patterns

Noul anti-patterns

Anti-pattern Why it fails Do instead
"Is this candidate strong in Python?" "Strong" is undefined, so the probability is uninterpretable. 0.5 means yes/no are equally likely, not medium skill. Either define the condition literally ("Does the resume state that the candidate has used Python at work?") or use a Score with levels: no experience / some familiarity / daily use / deep expertise.
Using noul as a magnitude It is P(yes), not a quantity. Score.
criteria.true describing "no" Jev 1.13 jaggedness: known failure modes #7: contradictory instructions and criteria perform worse. Keep true = yes.
One Noul per option, treating it as a classification Nouls are absolute and can all be low; the set does not sum to 1. A Choice settles which. Use both only deliberately, as Cookbook: Skill suggestion does.

Choice anti-patterns

Anti-pattern Why it fails Do instead
Options that are really a scale ("low", "medium", "high") Choice has no ordering; you lose the ability to threshold a position. Score with described levels.
No other / none of the above Probability mass lands on the least-wrong listed option. Add a catch-all option.
A shortlist of options "to save tokens" Options cost a few tokens each; up to 255 are allowed. Send the full list.
Acting on choice alone A 0.60/0.38 split and a 1.00/0.00 split give the same choice. Gate on confidence, inspect probabilities (Confidence-gated routing).
A 300-leaf taxonomy in one question Beyond the 255-option ceiling and hard to describe distinctly. Chain Choices level by level (Cookbook: Hierarchical classification, Structured instructions, options, levels, criteria).

Score anti-patterns

Anti-pattern Why it fails Do instead
criteria: ["0", "1", "2"] with "rate 0 to 2" in the instructions Each level is judged on its own; the model never sees a level's number or its neighbours. On a cosmetic bug this returned score 0.57, confidence 0.35 versus 0.0 / 1.0 with described levels. Describe a situation per level.
"Moderately severe" as a level A degree word gives nothing to match the state against. Describe situations, not degrees.
One level saying "punctual and smart and experienced" The question measures three things; an input high on one and low on another can't be placed. One dimension per Score, combined in code.
Interpolating score back into a real number jev-1.13's score levels are weak in numerical calibration (Jev 1.13 jaggedness: known failure modes #2). Use the expectation to check a threshold only.
Comparing a 3-level and a 4-level score directly Ranges differ (0–2 vs 0–3). Divide by len(criteria) - 1 first.
Levels for discrete categories with no in-between Nothing to place between them. Choice, or several Nouls.

Cross-type anti-patterns

Step 4 — if nothing fits, decompose

"Analyze this message and determine the best course of action" is not a System One question. Ask for a judgment a knowledgeable person makes in a second given the right context.

The decomposition recipe:

  1. Name the factors. List the independent things the decision actually depends on. Instead of "rate this startup pitch": market size, technical feasibility, differentiation.
  2. Give each factor its own question, and pick its type by the table above. Facts → Noul. Magnitudes → Score. Categories → Choice.
  3. Send them all in one request. Questions in a request are evaluated in parallel; adding questions barely changes the response time and costs only the extra question tokens. Include speculative questions whose answers only matter for some inputs (Speculative fan-out).
  4. Normalize before combining Scores. Divide each score by len(criteria) - 1 so every value is on 0 to 1 and the weights mean what they say.
  5. Weight and combine in code. The weights are yours; when the combined result doesn't match what your team would decide, change them in code and run again (Composite scoring).
  6. Gate on confidence. Low confidence is a reason to ask a person rather than act (Confidence-gated routing, Confidence vs probability).

Worked decomposition: ticket priority

Three Score questions replace one vague "how important is this ticket":

from typesafe_sdk import Score, TypeSafeClient

TRIAGE_QUESTIONS = {
    "severity": Score(
        instructions="How severe is the reported issue?",
        criteria=[
            "Cosmetic; no impact to functionality",
            "Broken or degraded feature, but workaround exists",
            "Blocking issue; no workaround exists",
        ],
    ),
    "frustration": Score(
        instructions="How frustrated is the customer?",
        criteria=[
            "Calm, just stating facts",
            "Frustrated but civil",
            "Very angry, strong language or threatening to leave",
        ],
    ),
    "report_quality": Score(
        instructions="How much does the report give an engineer to work with?",
        criteria=[
            "No detail; just says something is broken",
            "Names the feature but no steps or environment",
            "Steps to reproduce or environment, but not both",
            "Steps to reproduce and environment",
        ],
    ),
}


def normalized(answers, question_id: str) -> float:
    """Put a score on 0 to 1 by dividing by its top level number."""
    top_level = len(TRIAGE_QUESTIONS[question_id].criteria) - 1
    return answers[question_id].score / top_level


def priority(ticket: str) -> float:
    with TypeSafeClient() as client:
        response = client.system_one(
            state=ticket,
            questions=TRIAGE_QUESTIONS,
        )
    answers = response.answers

    severity = normalized(answers, "severity")
    frustration = normalized(answers, "frustration")
    report_quality = normalized(answers, "report_quality")

    # A detailed report helps an engineer investigate, so it raises priority a little.
    return 0.6 * severity + 0.3 * frustration + 0.1 * report_quality

On the worked response in Score questions this yields 0.6 × 0.62 + 0.3 × 0.725 + 0.1 × 1.0 = 0.68950.69.

Mixing types in one request

You can mix types freely in one request. This is the shape most triage code ends up with:

from typesafe_sdk import Choice, Noul, Score, TypeSafeClient

state = {
    "ticket_message": "My flight was cancelled. Can I get a refund?",
    "refund_policy": "Cancelled flights are eligible for a full refund.",
}

with TypeSafeClient() as client:
    response = client.system_one(
        state=state,
        questions={
            "refund_requested": Noul(
                instructions="Does `ticket_message` request a refund?",
            ),
            "request_type": Choice(
                instructions="What is the main request in `ticket_message`?",
                criteria={
                    "refund": "The customer wants money returned.",
                    "rebooking": "The customer wants a replacement flight.",
                    "information": "The customer is asking for information only.",
                },
            ),
            "frustration": Score(
                instructions="How frustrated does the customer appear in `ticket_message`?",
                criteria=[
                    "Calm and neutral.",
                    "Concerned but civil.",
                    "Very angry or using strong language.",
                ],
            ),
        },
    )

print(response.answers["refund_requested"].noul)
print(response.answers["request_type"].choice)
print(response.answers["frustration"].score)

Step 5 — when to make a second request

Questions in one request are independent: one answer does not become context for another. Make a second request only when your code cannot build it until it has the first answer — it needs the answer to fetch more data for the state, to decide what the state is made of, or to pick the next question's options. Two requests are the exception, not the rule. If the second request's questions could have been asked against the original state, ask them in the first request and let the code ignore what it doesn't need.

The three documented legitimate cases: Cookbook: Skill suggestion (re-judge the top 3 of 182 against fetched full text), Cookbook: Structure recovery (autoformat) (classify blocks that did not exist before the first answers), Cookbook: Hierarchical classification (each Choice answer decides the next request's options).

Quick reference

Related

Sources