index

Judge: What If TypeScript Could Make Fuzzy Decisions?

· 6min

Most control flow is easy when the rule is already known.

if (invoice.total > 10_000) {
  requestApproval()
}

But what about questions like:

  • Is this support ticket actually urgent?
  • Which team should own this issue?
  • Does this transaction look suspicious?
  • Which tool should an agent use next?

These are still decisions with a small set of possible answers. The rule is just based on meaning instead of something I can cleanly put inside an if statement.

So I built Judge: semantic control flow for TypeScript.

Illusion47586judge

--

-- -- --

The idea is simple: models judge, TypeScript constrains, and my application executes.

The model behind it

Judge uses Jev, a System One model from TypeSafe AI. Unlike a normal language model that generates some text and hopefully returns the JSON I asked for, Jev answers bounded questions.

It supports Boolean-like decisions, choices from a closed set and scores on an ordered scale. The response includes probabilities and confidence.

It can still be wrong, obviously. The useful part is that it cannot invent an answer outside the options my application provided, and uncertainty comes back as data that I can handle.

The syntax I wanted

I basically wanted to write this:

if (dhruv is a villain) {
  hideTheLaunchCodes()
} else {
  offerChai()
}

That is sadly not valid TypeScript, so Judge looks like this instead:

const verdict = await judge.if({
  condition: 'Is Dhruv a villain?',
  context: {
    recentActions: ['Built an AI decision library', 'Named it Judge'],
    hasDramaticCape: true
  },
  then: () => hideTheLaunchCodes(),
  else: () => offerChai(),
  confidence: {
    minimum: 0.8,
    uncertain: () => keepAnEyeOnHim()
  }
})

The condition is semantic, but every effect is still a normal function owned by my application. The model never sees those functions and it cannot generate a new one.

For decisions with more options, there is judge.switch():

const routing = await judge.switch({
  question: 'Which support queue should own this ticket?',
  context: {
    customerTier: 'business',
    subject: 'Team cannot sign in',
    message: 'Our SSO users are redirected back to the login screen.'
  },
  cases: {
    account: () => ({ queue: 'account' as const }),
    billing: () => ({ queue: 'billing' as const }),
    technical: () => ({ queue: 'technical' as const })
  },
  confidence: {
    minimum: 0.75,
    uncertain: () => ({ queue: 'human-review' as const })
  }
})

Judge uses the keys of cases as the only legal choices. It validates the provider response, checks confidence and runs exactly one callback.

TypeScript infers the result as:

type Queue = 'account' | 'billing' | 'technical' | 'human-review'

If I remove billing, it disappears from both the choices sent to the model and the return type. If the provider sends back something invalid, no callback runs.

There is no compiler transform here. judge.if() and judge.switch() are just async functions that look like familiar control flow.

The important boundary

I do not want a model to own execution.

typed facts -> constrained decision -> runtime validation -> one callback

Judge only sends structured context and a bounded question to the provider. The provider chooses an outcome, Judge validates it, and only then does application code run.

Low confidence also gets a proper branch. It can go to a human, a deterministic policy or another model instead of quietly continuing as if the answer was certain.

Gateways and testing

Judge currently works with:

The provider-neutral core is also available at @brkn-labs/judge/core, so another decision provider can use the same control-flow contract.

There is a deterministic mock provider too. I can test every branch without an API key, network request or random model output, which is probably my favourite part ngl.

What it is not

Judge is not an agent framework. It does not generate code, run arbitrary tools or plan an open-ended sequence of actions. It also does not make a risky decision safe just because TypeScript likes the return type.

It is useful when the decision is semantic, the possible outcomes are known and ambiguity has an explicit fallback.

If a normal TypeScript expression can solve the problem, use that. Judge is for the fuzzy gaps in between.

Where it is now

Judge is open source and still early. The first release includes typed Boolean, Choice and Score decisions, if and switch control flow, confidence routing, layered context, cancellation, multiple Jev transports and a mock provider for tests.

I built the API around one sentence:

Models judge. TypeScript constrains. Application code executes.

If that sounds useful, everything is in the Judge repository.

References