guideme
Judgments from TypeSafe Jev that read like Rust control flow.
A yes/no question is an if. A choice is an exhaustive match over your own enum. A score is
a comparison against your own ordered levels. Thresholds, unsure bands and fallbacks are
explicit and composable. Every request is one tracing span. The decision logic is pure and
its contract is published under spec/, so every guideme SDK, in any language, answers the
same way.
use ;
let guide = from_env?; // reads TYPESAFE_API_KEY
if guide.ask.await?
match guide.ask.await?
if guide.ask.await?
>= Frustrated
The doc comment on each variant is the rubric the model reads. The variant name in
snake_case is the wire key. The compiler enforces that every option is handled.
Examples in a rubric
A description alone leaves confusable options to a coin flip. Name the inputs that belong to an option, and the ones that do not:
Billing reaches the wire as one string:
Payments, invoicing, refunds
Examples: My card was charged twice; Where is my refund?
Not this option: The dashboard is down
Both keys are repeatable and compose with rubric, key and fallback. A variant with
neither renders to its rubric unchanged, byte for byte, so nothing you wrote before moves.
example works on #[derive(Levels)] too, where an example is the statement that such an
input scores at that level — its position on the scale carries the number, so nothing is added
to the text. counterexample is a choice key only: a level is a position on a scale, not an
option to rule out, and asking for one is a compile error. So are an empty or duplicated
example, an example on a variant with no rubric to attach it to, and an example that claims an
input belongs to two options at once. The same string as an example of one option and a
counterexample of another is exactly the point, and stays legal.
A noul has no enum to hang attributes off, so it takes Rubric, which composes the same way:
guide.ask.await?
This is where examples earn the most: on that ticket, plain criteria answer 0.75 and these
answer 0.25 — and 0.25 is right. criteria accepts a description or a Rubric, and a
description is anything String converts from, so a plain pair of strings keeps working
unchanged.
Install
or in Cargo.toml:
[]
= "0.1"
= { = "1", = ["rt-multi-thread", "macros"] }
Rust 1.98 or newer. #[derive(Choice)] and #[derive(Levels)] come with the crate; you
never depend on guideme-derive yourself.
Set TYPESAFE_API_KEY in the environment, or pass a key to Guide::builder().api_key(..).
The three questions
| Constructor | Sends | Plain output | .detail() output |
|---|---|---|---|
noul("…") |
a yes/no question | bool |
Verdict::{Yes, No, Unsure}(p) |
choose::<C>("…") where C: Choice |
a choice over C's variants |
C |
Ranked<C> with confidence and the full distribution |
score::<L>("…") where L: Levels |
a score over L's levels, low to high |
L, the most probable level |
Scored<L> with the expected value, the level, confidence and distribution |
choose_among("…", options) |
a choice over runtime (key, rubric) pairs |
Key |
Ranked<Key> |
score_levels("…", levels) |
a score over runtime level descriptions | Rank |
Scored<Rank> |
A noul can carry .criteria("what yes means", "what no means"). Instructions accept a string
or a serde_json::Value, so a question can reference structured data by field name the way
the TypeSafe docs describe.
The state is anything serialisable: a text literal, a String, a serde_json::Value, or a
reference to your own struct.
Policy
Thresholds decide how a probability or a confidence becomes an answer. They form a patch that merges from the question, over the guide, over the crate defaults.
| Layer | How to set | Wins over |
|---|---|---|
| question | .yes_above(p), .no_below(p) on nouls; .min_confidence(c) on choice and score; .with(Policy) on any |
guide |
| guide | Guide::builder().policy(..), or guide.with_policy(..)? for a scoped copy |
defaults |
| defaults | yes_above 0.5, no_below 0.5, min_confidence 0.0 |
nothing |
The rules:
- Noul:
p >= yes_aboveis yes,p <= no_belowis no, strictly between is unsure. With the defaults there is no unsure band. - Choice and score:
confidence < min_confidenceis unsure. With the default there is never an unsure answer.
When an answer is unsure, resolution goes down a ladder: .or(value) on the question, then
the enum's #[guide(fallback)] variant, then Error::Unsure naming the question and the
boundary it missed. .detail() skips the ladder and hands you the reading to decide yourself.
House policies are constants, because the setters are const fn:
const CAUTIOUS: Policy = new.yes_above.no_below;
let guide = builder.api_key.policy.build?;
let strict = guide.with_policy?;
match guide.ask.await?
Several judgments, one request
A tuple of questions is a question. So is a Vec or a BTreeMap, and they nest. The answer
has the same shape, from one request and one span. Each question keeps its own policy.
let labels = from;
let = guide.ask.await?;
if urgent || mood.value > 1.5 || flags
Question ids are q0..qN in encounter order; they appear on the wire, in errors and in
events. A batch is atomic: one answer that cannot be resolved fails the whole call, so put
.or(..) or .detail() on the questions that may come back unsure.
Observability
guideme emits tracing spans and events and installs nothing: no subscriber, no file, no
exporter. Add a subscriber and it appears. The smallest one:
fmt.with_env_filter.init;
One tracing span named guideme.ask per request, shaped by the OpenTelemetry GenAI
conventions: gen_ai.request.model, gen_ai.response.model, gen_ai.usage.*, and on
failure error.type with an error status. Under it, one HTTP client span per attempt with
http.response.status_code, so a retry is visible as sibling spans, plus a WARN event when
an attempt is throttled. One guideme.answer event per question with the outcome, the
probability or confidence, the unsure verdict and the settled thresholds that produced it.
The state is never recorded unless you opt in with record_state(true). The API key never
appears anywhere.
Because the shapes are standard, any OTLP backend reads them as is, and events exported as
OTLP log records carry the trace and span id of the ask they belong to. docs/observability.md
has the field tables, the RUST_LOG matrix, the environment variables that point the
exporter anywhere, and console and OTLP setups. examples/otlp runs all of it against the
live API with a collector that prints what arrives.
Errors
One enum, guideme::Error, for everything:
| Variant | When |
|---|---|
Auth |
401 |
Invalid { detail } |
422, body included |
RateLimited { retry_after } |
429 after retries, or a retry-after too long to wait for |
Overloaded |
529 after retries |
Transport(..) |
connection, TLS, timeout |
UnexpectedStatus { status, body } |
anything the contract does not define |
Protocol { detail } |
the response violates the contract: undecodable body, wrong answer kind, option or level not in the rubric, probability outside 0..1 |
Unsure { question, value, threshold } |
the policy said unsure and nothing caught it |
Config { detail } |
bad thresholds, missing key, empty batch, unserialisable state, empty or duplicate rubric |
Retries on 429 and 529 use exponential backoff with jitter, capped at 30 s, and honour
retry-after.
Lower layers
guideme::apiis the exact wire mirror ofPOST /v1/systemoneandGET /v1/models, plusClientfor callers who want to build requests themselves.guideme::policy::resolve(&Answer, Thresholds) -> Outcomeis the pure decision function.spec/holds its JSON Schemas, 42 golden policy vectors and the rubric rendering cases;docs/contract.mdstates what every guideme SDK must satisfy.docs/design.mdrecords the design and its sharp edges.
Other SDKs
Every guideme SDK is written from scratch in its own language and answers the same way,
because they all satisfy the contract this repository publishes under spec/ and states in
docs/contract.md: the wire schemas, the 42 golden policy vectors, the
rubric rendering, and the interface shape.
| Language | Package | Repository |
|---|---|---|
| Rust | guideme |
this repository |
| Python | guideme |
guideme-python |
The Python SDK mirrors the verbs in Python's idiom: Choice and Levels are enum.Enum
bases whose members carry the rubric, .or(value) is .otherwise(value) because or is a
keyword, and Guide and AsyncGuide share one surface. It emits the same span, event and
attribute names, so one dashboard reads both.
Environment
| Variable | Meaning |
|---|---|
TYPESAFE_API_KEY |
required by Guide::from_env |
TYPESAFE_BASE_URL |
optional API origin override |
GUIDEME_MODEL |
optional model or alias; default jev-latest |
Development
Tooling is managed by mise; mise install fetches gitleaks,
cargo-nextest and cargo-deny. The toolchain is pinned in rust-toolchain.toml.
mise run check # fmt-check, clippy -D warnings, nextest, doctests, rustdoc, cargo-deny
mise run test # nextest + doctests
mise run spec # regenerate spec/ after changing api, policy, or the vector grid
mise run hooks # point core.hooksPath at the tracked hooks in .githooks
The hooks are tracked, not generated: mise run hooks sets this repository's
core.hooksPath to .githooks and verifies it took effect. AGENTS.md says what each stage
runs.
Library code is held to a strict lint set: pedantic clippy, with unwrap, expect, panic,
dbg and todo denied. Tests are few and high-grade: property tests for the policy laws, a
local mock server for the wire and retry contract, structural tracing assertions, a
compile-fail suite for the derives, and a drift guard that re-resolves every golden vector.
Two opt-in tests hit the real API and are skipped by default:
TYPESAFE_API_KEY=… cargo nextest run -p guideme --test live --run-ignored ignored-only --no-capture
Contributor rules live in AGENTS.md. Report a vulnerability privately, as SECURITY.md
describes, never in a public issue.
License
MIT or Apache-2.0, at your option.