Skip to content
Everruns Cloud is open in early access. Run agents without operating the platform.

Direct Classification

Some questions have typed answers. Is this claim supported by the source? How severe is this complaint? Which queue does this ticket belong in? A chat model answers those in prose, so the call site ends up with a prompt asking for JSON, a parser, and a fallback for when the parse fails.

A classifier answers them as numbers instead, and the decision stays in your code:

use everruns::Classifier;
use everruns_integrations_typesafe::TypeSafeClassifier;
let classifier = Classifier::new(TypeSafeClassifier::from_env()?);
let spam = classifier
.probability("Is this message spam?", "Claim your prize now!")
.await?;
if spam > 0.9 {
println!("quarantined");
}

This is the counterpart to direct model calls: the same shape, a different contract.

ModelClassifier
you sendmessagesstate plus typed questions
you get backtextcalibrated numbers
decides the outcomethe model’s wordsyour threshold, in your code
streamsyesno — one round trip

A judgment asks one or more questions about the same state. Each is one of three shapes:

use everruns::Classifier;
let answers = classifier
.about("I've been on hold for two hours and my card was charged twice.")
.noul("urgent", "Does this convey urgency?")
.score(
"severity",
"How severe is the problem the writer describes?",
[
"A minor annoyance",
"A real problem with their account",
"Serious harm requiring immediate action",
],
)
.choice(
"queue",
"Which team should handle this message?",
["billing", "technical", "sales"],
)
.send()
.await?;
let urgent: f64 = answers.probability("urgent")?;
let queue: &str = answers.selected("queue")?;
  • noul — whether something holds, as the probability of yes. A value near 0.5 means yes and no are near-equally likely, not “medium”.
  • choice — exactly one option from your set, with the distribution behind it. Needs at least two options.
  • score — a position along levels you define, lowest first. Needs at least two levels.

Questions in one call are answered in parallel inside a single request, so asking five costs one round trip, not five.

Ids are yours; instructions are the model’s

Section titled “Ids are yours; instructions are the model’s”

The id labels the answer for your code and is never sent to the model. A question whose meaning lives in its id asks nothing:

// Wrong: the model never sees "is_the_joke_funny".
.noul("is_the_joke_funny", "?")
// Right: the instructions carry the question.
.noul("funny", "Would a general audience laugh at this joke?")

The same applies to score levels. Describe concrete situations — “A minor annoyance” reads on its own where “2 out of 5” does not.

For “is there any serious hit here” rules, read the probability mass at or above a level rather than the weighted score. Something probably fine but possibly awful must not average into fine:

// Not: answers.score("severity")? > 1.5
let serious = answers.tail("severity", 2)?;
if serious > 0.3 {
println!("escalated");
}

Classifier::simulated needs no credentials and no network, so judgments are testable the same way agents and completions are:

use everruns::Classifier;
let p = Classifier::simulated(0.93)
.probability("Does this convey urgency?", "Two hours on hold.")
.await?;
assert!(p > 0.9);

TypeSafeClassifier::from_env() reads your application’s own TYPESAFE_API_KEY. The classifier itself comes from the integration crate, so add it alongside everruns:

everruns = "0.22"
everruns-integrations-typesafe = { version = "0.1", default-features = false }

default-features = false leaves out the hosted connector catalog, which only the platform needs. everruns itself stays vendor-free: Classifier::new takes any ClassifierService, exactly as Model::new takes any provider.

A deployment running the Everruns platform configures a separate UTILITY_TYPESAFE_API_KEY for its guardrails — a different account from the one an embedding application holds.

Everything above is agentless: your code asks, your code decides. The other half is letting an agent classify as part of its own work — checking a claim against a source before citing it, rating a draft before sending it.

Jev is the same classifier as a capability, so an agent gets it as a tool:

use everruns::{Agent, Engine, Model, OpenAI};
use everruns_integrations_typesafe::Jev;
let agent = Agent::builder()
.name("reviewer")
.instructions(
"You review copy. When asked how something reads, measure it with \
jev_evaluate and report the numbers rather than judging by eye.",
)
.model(Model::new("gpt-5.6-terra", OpenAI::from_env()?))
.capability(Jev::new(std::env::var("TYPESAFE_API_KEY")?))
.build()?;
let session = Engine::new().create(agent);
let turn = session
.run("Rate this subject line for pushiness: 'Act now before it is too late'")
.await?;

The agent calls jev_evaluate, writing its own questions about whatever it is looking at, and gets the same calibrated numbers back. It is the identical tool the hosted TypeSafe integration gives platform agents — same name, same schema — so behavior matches whether you embed the Framework or run on Everruns.

Which one to reach for:

you decidethe agent decides
who asksyour code writes the questionsthe model writes the questions
useClassifierthe Jev capability
good fora policy check, a routing rule, a gateverification inside a longer task

Add the dependency alongside everruns:

everruns-integrations-typesafe = { version = "0.1", default-features = false }

default-features = false leaves out the hosted connector catalog, which only the platform needs.

A judgment owns no session, no history, and no workspace, and runs no tools. Reach for an agent as soon as the work needs any of those. Typed output guarantees the interface, not the truth: validate thresholds against your own data and consequences.

Runnable: direct_classification.rs and agent_classification.rs.