Introduction
Nilsafe is a typed inference primitive. You send it state and a set of named questions; it returns values that fit the shape you declared. Every call. Every time. By construction.
The primitive
Modern software is built on small contracts that refuse to bend. A function returns the type it said it would. A database row has the columns the schema promised. A JSON payload parses, or it does not — there is no in-between where the bytes become suggestions.
Large language models broke that deal. Outputs are free-form prose that happens to look like the thing you wanted, confidence is a vibe, and a hallucination is the price of admission. We are not interested in negotiating that price.
Nilsafe is a stochastic execution primitive that honours the contract. You declare the shape. We return values inside it. The model is the loop, the latency is single-digit millisecond, and the cost is zero. What you wire it into is up to you.
Nilsafe primitives
Every question is one of three types. Each carries its own criteria and produces its own return shape.
| Type | Goal | Returns |
|---|---|---|
choice |
Pick one of a fixed set of labelled options. | choice, probabilities, confidence |
score |
Place input on an ordered rubric of levels. | score, legend, probabilities, confidence |
noul |
Estimate a scalar probability in [0, 1]. |
noul |
Atomic questions, composed in code
Each question is small. The interesting work happens when you bundle many of them into a single call, give each one a key, and treat the answer object as a typed struct your code can destructure.
"questions": {
"department": { "type": "choice", "criteria": { "billing": "…", "technical": "…" } },
"frustration": { "type": "score", "criteria": ["calm", "angry"] },
"is_urgent": { "type": "noul" }
}
Question shape
Every question requires a type. The type determines whether and what shape criteria must take.
type |
criteria |
shape |
|---|---|---|
choice |
required | non-empty object, keyed by option id (≤255 keys) |
score |
required | non-empty array, ordered rubric levels |
noul |
— | no criteria |
instructions is optional on every type. It is not echoed in the response.
Quickstart
The endpoint accepts a single POST, matches the documented nilsafe request shape, and returns values that conform to it. No auth, no SDK, no model warm-up. Curl from the terminal is the entire getting-started story.
Request
{
"model": "nilsafe-0.0.1",
"state": "Hi, I've been trying to connect my Stripe account for 3 days and it keeps failing. I'm losing sales. Please help ASAP.",
"questions": {
"department": {
"type": "choice",
"instructions": "Which team should handle this",
"criteria": {
"billing": "Payment or subscription issues",
"technical": "Bugs or integration problems",
"sales": "Pricing or account questions"
}
},
"frustration": {
"type": "score",
"instructions": "How frustrated the customer appears",
"criteria": [
"Frustrated but civil",
"Calm, just stating facts"
"Very angry, strong language",
]
},
"is_urgent": {
"type": "noul",
"instructions": "The message conveys urgency or time-sensitivity"
}
}
}
Response
{
"model": "nilsafe-0.0.1",
"answers": {
"department": {
"type": "choice",
"choice": "billing",
"probabilities": { "billing": 0.61, "technical": 0.22, "sales": 0.17 },
"confidence": 0.61
},
"frustration": {
"type": "score",
"score": 0.05,
"legend": { "0": "Frustrated but civil", "1": "Very angry", "2": "Calm" },
"probabilities": { "0": 0.95, "1": 0.05, "2": 0.0 },
"confidence": 0.95
},
"is_urgent": {
"type": "noul",
"noul": 0.78
}
},
"usage": { "input_tokens": 0, "output_tokens": 0 }
}
Invariants
The wire format keeps the following guarantees from the nilsafe schema, byte-for-byte:
choiceis always one of the criteria keys.scoreis always in[0, n−1];noulis always in[0, 1].- Every key in
questionshas a matching key inanswers. probabilitiesandlegendare keyed the same way. Forchoice, keys are criterion ids from the request; forscore, keys are stringified indices"0".."n−1", ordered by descending mass so the most likely level sits at index"0"and the least likely at index"n−1". The distribution sums to ~1.
Errors
| Status | Trigger |
|---|---|
400 |
Body is not JSON |
400 |
questions is missing, empty, or not an object |
400 |
state is missing or null |
400 |
A question is not an object |
400 |
A choice question has no criteria or an empty criteria object |
400 |
A score question has no criteria or an empty criteria array |
400 |
A question declares a type other than choice, score, or noul |
405 |
Method is not POST (and not GET / OPTIONS) |
413 |
Body exceeds 1 MB |
500 |
Anything else — we did not plan for it |
Coding agents
Agents need a way to gate before they fire. A tool call is a privilege, not a guess. Nilsafe is the gate: cheap enough to sit on the hot path, fast enough that the agent never notices, typed enough that the policy is a JSON schema, not a prompt.
Tool gating, not tool guessing
The pattern is a thin wrapper around any risky action. Before the agent invokes the tool, it asks nilsafe a single noul question about the planned call. If the answer is above your threshold, the call proceeds. If not, the agent re-plans or escalates. The decision is a float, the policy is a number, and the audit trail is a structured log entry.
// pseudocode
const risk = await nilsafe.noul({
instructions: "This action will delete a production record."
});
if (risk.noul > 0.8) {
return escalate(plan);
}
return execute(plan);
Classification, scoring, and extraction as function calls
Most agent work is not generation — it is bookkeeping. Sort this ticket. Score this lead. Extract these fields from this blob. Nilsafe turns every one of those into an ordinary function call with a typed return value, the way a database query or an HTTP request already is. No string parsing downstream. No retry loop when the model decides to be creative.
Routing and triage
At single-digit millisecond latency, nilsafe can sit in front of every inbound message and decide which queue, which handler, which human. The routing decision is a choice with a probability mass you can threshold, log, and replay. The hot path stays hot.
Auditable decisions
Every response carries a structured value with a confidence you can store. When something goes wrong at 3am, the question you ask is not "what did the model feel like doing?" — it is "what value did the schema return?" The latter is a row in a table. The former is a vibes report.
When confidence is too low
A low confidence on a choice is a signal, not a complaint. It means the rubric you declared is too coarse for the input — the runtime cannot pick a winner because the criteria do not separate the cases. Do not lower the threshold. Widen the rubric.
When a choice comes back close to uniform across options, the answer is not "uncertain" — the criteria are. Replace the broad labels with a richer set that actually distinguishes the input space, and the distribution sharpens. Each added criterion gives the runtime more surface area to discriminate, without changing the shape your code already expects.
// before — coarse rubric, flat distribution
"department": {
"type": "choice",
"criteria": {
"billing": "Payment issues",
"technical": "Everything else"
}
}
// after — finer rubric, peaked distribution
"department": {
"type": "choice",
"criteria": {
"billing": "Payment or subscription issues",
"technical": "Bugs or integration problems",
"sales": "Pricing or account questions"
}
}
The same shape. The same call. A more discriminating answer. Richness is the lever; the schema stays the contract.
Returned values are produced by a stochastic execution primitive operating within the constraints of the schema declared in the originating request. Nilsafe warrants solely that returned values conform to that declared shape. Nilsafe makes no representation, warranty, or covenant, express or implied, as to the correspondence of any returned value with any ground truth, reference standard, downstream expectation, business outcome, or subjective notion of usefulness, and disclaims all such representations to the fullest extent permitted by applicable law. The calling system is solely responsible for the interpretation, evaluation, downstream propagation, and consequences of any returned value, and any reliance upon or integration of any returned value is undertaken at the sole discretion and risk of the calling system. Nilsafe’s total aggregate liability, if any, shall not exceed the amount paid by the calling system for the specific request from which liability is alleged to arise, which amount, for the avoidance of doubt, is zero.