Use the standalone decision SDK to route a request, classify a document, or review an answer. It wraps TypeSafe Jev's Choice, Score, and Noul primitives in typed HarnessX requests and results.
Import it from harnessx.decisions. Decisions have their own async provider and usage accounting. Call the provider before selecting an agent or after receiving an answer. The agent loop does not call them on its own.
Live calls need the jev extra and a TypeSafe key. See Installation for credentials and Examples for the runnable commands.
Choose a question type
| HarnessX question | Jev primitive | Result | Used in the examples |
|---|---|---|---|
ChoiceQuestion |
Choice |
Selected label, probabilities for every option, and confidence | Agent routing and document classification |
ScoreQuestion |
Score |
Unrounded expected score, ordered probabilities, rubric levels, and confidence | Answer coverage |
BooleanQuestion |
Noul |
Probability that the proposition is true | Support from supplied evidence |
Choice requires at least two named options with descriptions. Score accepts two to ten ordered descriptions, indexed from zero. A four-level rubric produces a score between 0 and 3, which may be fractional.
BooleanCriteria describes the true and false outcomes, and either description can be omitted. Noul returns a probability with no separate confidence value. Compare answer.probability with your own threshold; bool(answer) raises to prevent implicit conversion.
Evaluate typed questions
This example sends an answer and its evidence in one request. Each question assesses that state independently. It makes a live Jev call when run.
import asyncio
from harnessx.decisions import BooleanCriteria, BooleanQuestion, DecisionRequest
from harnessx.decisions import JevDecisionProvider, ScoreQuestion
async def main():
request = DecisionRequest(
check_id="answer_review",
version="v1",
state={"answer": "Sales were $100 in January.", "evidence": "January: $100."},
questions={
"coverage": ScoreQuestion(
instructions="How completely does the answer address the query?",
levels=(
"Does not answer the request.",
"Addresses most of the request, with minor omissions.",
"Fully addresses every part of the request.",
),
),
"supported": BooleanQuestion(
instructions="Does the evidence support every factual claim?",
criteria=BooleanCriteria(true="Every claim follows from the evidence."),
),
},
)
async with JevDecisionProvider() as decisions:
batch = await decisions.evaluate(request)
print(batch.scores["coverage"].score, batch.booleans["supported"].probability)
asyncio.run(main())
The adapter translates those questions to the official SDK's Choice, Score, and Noul, then calls AsyncTypeSafeClient.system_one(). The result keeps the resolved model and the service request ID. The default endpoint is the direct TypeSafe API.
Route to one agent
The routing example source uses Choice to select from an application-defined mapping of agent factories:
| Decision | Application behavior |
|---|---|
sql, research, or general, with sufficient confidence |
Construct and run that one agent |
unknown or an unexpected label |
Use the general agent |
Missing confidence or confidence below min_confidence |
Use the general agent |
| Unavailable decision | Use the general agent |
| Selected agent fails | Return its failed result; do not start a second agent |
min_confidence is required and must be a finite number between zero and one. The example's 0.8 is illustrative, not a calibrated default. The model selects a label; application code owns the factories. The example's specialists draft answers without a database or live sources.
Run it offline with uv run python examples/07-quality/decisions_routing.py --min-confidence 0.8.
Classify documents and review answers
The routing example classifies a document before it routes. It asks a Choice question with five labels: W2, Deposit, Insurance, Payroll, and unknown. The offline fixture always returns W2; pass --live --text ... with --min-confidence 0.8 and --model to assess your own document text.
The answer-review example source makes a separate decision request after a successful Agent.run() result:
- Score evaluates coverage against four ordered levels, from unanswered to fully addressed.
- Noul evaluates whether every factual claim follows from the supplied evidence, using explicit true and false criteria.
The review skips unsuccessful runs and leaves the original run unchanged. It reports probabilities and scores, not an automatic pass/fail gate.
Handle results and failures
DecisionBatch.status is either completed or unavailable. A completed batch requires every requested answer to validate. An uncertain but valid answer is still completed; confidence thresholds belong to the application.
Missing or incompatible answers, mismatched option labels or rubric levels, nonfinite probabilities, invalid distributions, and inconsistent scores produce an unavailable batch with empty answer maps. Probability distributions are never silently renormalized. Extra answer IDs are ignored.
| Condition | Outcome |
|---|---|
| Missing optional dependency | ImportError with installation guidance |
| Invalid local configuration, question, or oversized state | Local exception before I/O |
| Authentication, authorization, or request rejection | Unavailable, without retry |
| Connection failure, transport timeout, HTTP 429 or 500/502/503/504 | Bounded retry, then unavailable if unresolved |
| Invalid or missing requested answers | Unavailable, without retry |
| Caller cancellation | asyncio.CancelledError propagates |
| Programming error | Exception propagates |
DecisionError exposes a stable code, sanitized message, optional HTTP status, and whether the cause is retryable. Codes are authentication, forbidden, invalid_request, rate_limited, timeout, connection, server, invalid_response, and unexpected_http. Raw server bodies are excluded, though the upstream SDK's own debug logging can include them.
Configure limits and ownership
| Provider option | Default | Meaning |
|---|---|---|
model |
jev-1.13.0 |
Explicit model sent on every request |
timeout_seconds |
2.0 |
Total evaluation deadline, including retry delays |
max_retries |
1 |
At most two HTTP attempts by default |
max_state_bytes |
16384 |
Limit on compact UTF-8 JSON state |
api_key |
TYPESAFE_API_KEY |
Credential used when constructing a client |
base_url |
https://api.typesafe.ai |
Explicit API root; does not inherit TYPESAFE_BASE_URL |
client |
None |
Optional borrowed AsyncTypeSafeClient |
evaluate(request, timeout_seconds=..., max_attempts=...) shortens the deadline and caps attempts for one call. SDK retries are disabled so they cannot multiply the adapter's retry budget. A Retry-After delay is honored when it fits the remaining deadline; otherwise the evaluation returns unavailable.
Use async with JevDecisionProvider(...) to close resources reliably. The adapter owns and closes the clients it creates. Injected clients remain caller-owned, and per-call overrides do not change their defaults; combining client with api_key or base_url raises.
The provider supports concurrent evaluations. aclose() is idempotent, cancels outstanding evaluations, and closes owned resources. Evaluating after close raises RuntimeError.
Preserve state, usage, and results
State
Request state must be text, a JSON object, or a JSON array holding only finite JSON values and string keys. State is copied at construction and again before dispatch. Arbitrary objects are never converted with str().
Serialization
Requests and results expose to_dict() and from_dict() with schema_version=1. Decoding validates the data and rejects unknown schema versions and question or answer kinds, while accepting additive metadata fields. Use those codecs before json.dumps.
Usage
A batch preserves check_id, version, requested and resolved model, request ID, elapsed time, and attempt count. Token counts describe the successful response only. After a retry, earlier attempts may have consumed unreported tokens, so usage.incomplete stays true. Absent measurements stay None, cost included. Decision usage is separate from agent model usage.