discern

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 14, 2026 License: MIT Imports: 16 Imported by: 0

README ΒΆ

🧭  go-discern

A small, typed Go decision layer for the TypeSafe Jev and Cloudflare Clef System One APIs.


Release Go Version License


CI / CD Β Β  Build Last Commit Β Β Β Β  Quality Β Β  Coverage
Security Β Β  Scorecard Security Β Β Β Β  Community Β Β  Contributors Bitcoin


Project Navigation
πŸš€Β Installation 🏁 QuickΒ Start πŸ’‘Β Usage βš™οΈΒ Behavior πŸ“šΒ Documentation
πŸ§ͺΒ ExamplesΒ &Β Tests ⚑ Benchmarks πŸ› οΈΒ CodeΒ Standards πŸ€–Β AIΒ Usage 🀝 Contributing

go-discern asks typed questions about a piece of text or JSON (pick one of your options, give the probability of yes, or rate on your scale) and gets the answers from TypeSafe's Jev API, Cloudflare Workers AI's Clef models, or both at once. Both follow TypeSafe's "System One" API: the models return probabilities, not text. Use go-discern when a Go service needs a fast classification or triage decision, such as routing a support ticket, backed by real probabilities, your own thresholds, and a record you can store.


πŸ“¦ Installation

go-discern requires a supported release of Go (Go 1.26 or newer).

go get github.com/mrz1836/go-discern

The library has zero third-party runtime dependencies: the core, both adapters and discerntest use only the standard library. testify is used only by the tests and their internal helper package.

Credentials

You need credentials only for the providers you call. go-discern never reads environment variables or configuration files itself: your code passes the values to jev.New and clef.New.

Provider You need Where to get it
Jev A TypeSafe API key Your TypeSafe account; see TypeSafe's documentation
Clef Your Cloudflare account ID (32 hexadecimal characters) and an API token that can use Workers AI The Cloudflare dashboard: Workers AI β†’ Use REST API shows the account ID and can create a token with the right permissions

The examples below read them from TYPESAFE_API_KEY, CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN, the names the example program and the live tests use. Any names work.


🏁 Quick Start

Save this as main.go in a new module (go mod init example.com/triage, then go get github.com/mrz1836/go-discern), set TYPESAFE_API_KEY, and run go run .. It makes one billable call to Jev.

package main

import (
    "context"
    "fmt"
    "log"
    "os"
    "time"

    "github.com/mrz1836/go-discern"
    "github.com/mrz1836/go-discern/jev"
)

func main() {
    // go-discern never reads the environment: your code passes the key in.
    jc, err := jev.New(os.Getenv("TYPESAFE_API_KEY"), jev.WithModel("jev-1.13.0"))
    if err != nil {
        log.Fatal(err) // matches discern.ErrInvalidConfig, for example when the key is empty
    }
    ev, err := discern.New(discern.WithAdapter(jc))
    if err != nil {
        log.Fatal(err)
    }

    req := discern.Request{
        Providers: discern.JevOnly, // required: every request names its provider or providers
        State:     discern.Text("Subject: Charged twice\n\nMy card was charged twice for this month's invoice."),
        Questions: []discern.Question{
            discern.Choice("queue", discern.Text("Which support queue should handle this ticket?"),
                discern.Opt("billing", discern.Text("Invoices, refunds, failed payments")),
                discern.Opt("technical", discern.Text("Errors, outages, integration problems")),
                discern.Opt("account", discern.Text("Login, profile, permissions"))),
        },
    }

    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    res, err := ev.Evaluate(ctx, req)
    cancel()
    if err != nil {
        log.Fatal(err) // for example "discern: jev: rejected (HTTP 401)" when the key is wrong
    }

    obs, _ := res.Observation(discern.Jev)
    queue, _ := obs.Choice("queue")
    fmt.Println("chosen:", queue.Option)
    for _, p := range queue.Probabilities { // every option, in request order
        fmt.Printf("  %-9s %.2f\n", p.Option, p.Probability)
    }
    fmt.Println("model:", obs.Model) // the version that answered
}

It prints the chosen option and the probability of every option. The numbers come from the model, so yours will differ:

chosen: billing
  billing   0.99
  technical 0.01
  account   0.00
model: jev-1.13.0
Both providers and a policy

To ask Jev and Clef the same question and get a recommendation, register both adapters and give the evaluator a policy. Add this function to the program above, add the errors and github.com/mrz1836/go-discern/clef imports, set CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN, and call it at the end of main with if err := routeBoth(context.Background(), jc, req); err != nil { log.Fatal(err) }:

func routeBoth(ctx context.Context, jc *jev.Client, req discern.Request) error {
    cc, err := clef.New(os.Getenv("CLOUDFLARE_ACCOUNT_ID"), os.Getenv("CLOUDFLARE_API_TOKEN"))
    if err != nil {
        return err
    }
    ev, err := discern.New(
        discern.WithAdapter(jc),
        discern.WithAdapter(cc),
        discern.WithDefaultPolicy(discern.Policy{
            Version: "routing-v1", // required; your label, recorded with every result
            Questions: []discern.QuestionPolicy{{
                Question: "queue",
                Checks:   []discern.Check{{Metric: discern.MetricChoiceProbability, Op: discern.AtLeast, Threshold: 0.80}},
            }},
        }),
    )
    if err != nil {
        return err
    }

    req.Providers = discern.JevAndClef // two billable calls, made concurrently
    res, err := ev.Evaluate(ctx, req)
    if err != nil && !errors.Is(err, discern.ErrPartialFailure) {
        return err // no usable answer (with ErrPartialFailure, one provider still answered)
    }

    rec, _ := res.Recommendation("queue")
    if rec.Status == discern.Accepted {
        fmt.Println("route to", rec.Selected.Option)
    } else {
        fmt.Println("send to a person:", rec.Status, rec.Reasons)
    }
    fmt.Println(rec.Summary)
    return nil
}

The question is accepted only when both providers chose the same option and both passed the check. The summary says why, for example:

route to billing
accepted billing: jev choice.probability 0.98 β‰₯ 0.8; clef choice.probability 0.95 β‰₯ 0.8; both chose billing

Otherwise the status says why: below_threshold when they agree but a probability is under 0.8, review_required (reason providers_disagree) when they chose different options, and incomplete (reason provider_failed) when one of them failed. The policy never calls a provider; it only reads the answers.

Next steps
  • Usage: the three question kinds, the models, JSON state, policies, batches, storing results, an HTTP handler, and testing your code with fakes.
  • Behavior: billing and retries, errors, what a stored record holds, and the limits.
  • examples/: in a clone of this repository, go run ./examples routes synthetic tickets against fake providers, offline. DISCERN_LIVE=1 go run ./examples -live makes real, billable calls with the three variables above.
  • pkg.go.dev: the full API reference.

πŸ’‘ Usage

go-discern asks typed questions about a piece of content and returns every provider observation, with an optional, explicit recommendation on top. Three question kinds are supported:

Kind Asks Returns
Choice Pick one of your named options (2–255) The chosen option, the probability of every option (in your order), confidence
Noul A yes/no question The probability of yes, in [0,1]: a number, never a boolean
Score Rate on ordered levels (2–10), lowest first A fractional level position, the probability of every level, confidence

Two providers sit behind one API, and every request chooses its provider:

Provider Package Models
TypeSafe Jev jev jev-latest (default), jev-preview, or a versioned ID such as jev-1.13.0
Cloudflare Workers AI Clef clef clef (default), clef-flash, clef-omni

In October 2026 both Jev aliases resolved to jev-1.13.0, and the shorter names jev and jev-1.13 that appear in TypeSafe's SDK examples were refused as unknown models (HTTP 400). Pin a versioned model in production: the aliases can move.

The three Clef models trade context and price. The figures below are from Cloudflare's model pages (October 2026), and Cloudflare changes them:

Clef model Constant Context window Input price Choose it for
clef (default) clef.ModelClef 65,536 tokens $0.24 per M tokens the safest answer quality and the most state
clef-flash clef.ModelFlash 24,576 tokens $0.038 per M tokens the lowest cost, with small state
clef-omni clef.ModelOmni 64,000 tokens $0.15 per M tokens a mixture of experts (30B, 3B active) priced in between

Tokens cannot be counted locally, so go-discern never checks these windows before a call; see Oversized state under Behavior for what each model did with state that did not fit.

Set up once
jc, err := jev.New(apiKey, jev.WithModel("jev-1.13.0"), jev.WithTimeout(15*time.Second))
if err != nil {
    return err // discern.ErrInvalidConfig: empty or header-unsafe key, bad model, timeout <= 0
}
cc, err := clef.New(accountID, apiToken, clef.WithModel(clef.ModelFlash))
if err != nil {
    return err // discern.ErrInvalidConfig: account ID not 32 hex characters, bad token or model
}
ev, err := discern.New(
    discern.WithAdapter(jc),
    discern.WithAdapter(cc),
    discern.WithDefaultPolicy(routing), // optional; routing is a discern.Policy, see Recommendations below
)

Both adapters take the same options: WithModel, WithTimeout (default 30 seconds), WithMaxResponseBytes (default 4 MiB, at most 64 MiB), WithHTTPClient, and WithTestEndpoint, which accepts only an https server on a loopback address and exists for tests. discern.New takes one WithAdapter per provider, WithDefaultPolicy and WithBatchConcurrency. The Evaluator and both adapters are immutable and safe for concurrent use.

Ask questions
req := discern.Request{
    Providers: discern.ClefOnly, // discern.JevOnly | discern.ClefOnly | discern.JevAndClef (required)
    State:     discern.Text(subject + "\n\n" + body),
    Questions: []discern.Question{
        discern.Choice("queue", discern.Text("Which support queue should handle this ticket?"),
            discern.Opt("billing", discern.Text("Invoices, refunds, failed payments")),
            discern.Opt("technical", discern.Text("Errors, outages, integration problems")),
            discern.Opt("account", discern.Text("Login, profile, permissions"))),
        discern.Noul("outage", discern.Text("Does the customer report a production outage?")),
        discern.Score("severity", discern.Text("How severe is the customer impact?"),
            discern.Level("cosmetic", discern.Text("Cosmetic; work continues")),
            discern.Level("degraded", discern.Text("Degraded; workaround exists")),
            discern.Level("blocked", discern.Text("Blocked; no workaround"))),
    },
    CorrelationID: "ticket-1042",          // optional, stored verbatim in the record
    EvidenceRefs:  []string{"kb:article-17"}, // optional, opaque; never fetched or validated
}

res, err := ev.Evaluate(ctx, req)
if err != nil {
    return err
}
obs, _ := res.Observation(discern.Clef)
q, _ := obs.Choice("queue")       // q.Option "billing"; q.Probabilities in request order; q.Confidence *float64
n, _ := obs.Noul("outage")        // n.Value 0.07 = P(yes)
s, _ := obs.Score("severity")     // s.Value 1.05 (fractional); s.Nearest() == "degraded"

Questions are answered independently. A Noul question can also describe both ends: set Yes and No on the Question that discern.Noul returns. Option order is sent exactly as given (models can lean toward earlier options).

The state is text, a JSON object or a JSON array, and must not be blank. Structured state is sent byte for byte, so large integers and key order survive:

state := discern.JSON([]byte(`{"ticket":{"id":12345678901234567890,"text":"..."}}`)) // raw JSON, sent as is
state, err := discern.Marshal(ticket) // or encode a Go value once, without HTML escaping
Run both providers

JevAndClef makes exactly two potentially billable calls, concurrently. One provider failing never cancels the other, and the result keeps a fixed order (Jev, then Clef) regardless of which finishes first.

req.Providers = discern.JevAndClef
res, err := ev.Evaluate(ctx, req)
if res != nil {
    audit.Save(res) // persist first: res != nil whenever any call was attempted
}
switch {
case err == nil: // res.Status == discern.Complete
case errors.Is(err, discern.ErrPartialFailure): // one observation is usable; check this case first
case res != nil: // both failed; res.Outcomes hold both typed failures
default: // nothing was sent (invalid, unsupported, or already canceled)
}
Recommendations

Without a policy, go-discern returns observations and never manufactures an acceptance decision. A Policy is a small, typed set of threshold checks per question:

routing := discern.Policy{
    Version: "routing-v3", // required; your label, recorded but not interpreted
    Questions: []discern.QuestionPolicy{{
        Question: "queue",
        Checks: []discern.Check{ // all applicable checks must pass
            {ID: "queue-jev", Provider: discern.Jev, Metric: discern.MetricChoiceProbability, Op: discern.AtLeast, Threshold: 0.80},
            {ID: "queue-clef", Provider: discern.Clef, Metric: discern.MetricChoiceProbability, Op: discern.AtLeast, Threshold: 0.75},
        },
    }, {
        Question:  "severity",
        Checks:    []discern.Check{{Metric: discern.MetricScore, Op: discern.AtMost, Threshold: 1.0}}, // lower is better
        Agreement: &discern.Agreement{MaxDifference: 0.25}, // dual Noul/Score tolerance; 0 allows only float noise
    }},
    // PreferOnConflict: discern.Jev β€” opt in to pick a provider when the two disagree
    // AllowPartial: true             β€” opt in to evaluate the survivor when one provider fails
}

rec, _ := res.Recommendation("queue") // Status, Selected (nil unless Accepted), Reasons, Checks, Summary
picked := res.SelectedAnswers("queue") // the typed answers the selection points to
fmt.Println(res.Overall.Status)         // accepted only if every covered question is accepted

res, err = ev.Evaluate(ctx, req, discern.WithPolicy(strict)) // this call only; the evaluator is untouched
res, err = ev.Evaluate(ctx, req, discern.WithoutPolicy())    // observations only
again, err := discern.Recommend(res, experimental)           // replay locally: zero calls, input untouched
Status Meaning
accepted Every applicable check passed; for dual runs the providers agreed, or the preferred provider passed a disagreement
below_threshold At least one applicable check failed
review_required The providers disagreed and no conflict preference resolved it (the default)
not_applicable No check applied to a provider or model, or a dual Noul/Score question has no agreement tolerance
incomplete A provider the policy needs failed (with AllowPartial, a dual run's surviving provider is evaluated alone), or a checked metric was absent
no_rule No policy covered the question

Metrics are choice.probability (the probability of the chosen option), confidence (Choice and Score), noul and score (constants discern.MetricChoiceProbability, MetricConfidence, MetricNoul and MetricScore); ops are gte, gt, lte and lt (discern.AtLeast, Above, AtMost and Below), with exact boundary behavior. Checks can be scoped to a provider and a model. A model-scoped check matches the model the provider reported (or the requested model when none is reported), so an alias that moves to a new version stops matching. If that leaves no check for the provider, the question is not_applicable (model_mismatch); if unscoped checks still apply, they decide and the reason model_check_skipped records the skip. Reason codes are a closed, documented list; the summary text is for people and is not a stable contract. Two-sided gates (escalate at noul β‰₯ 0.9, close at noul ≀ 0.1) are two Recommend calls.

A dual-run policy needs an applicable check for both providers, even with PreferOnConflict. With AllowPartial, a dual run can return an error matching ErrPartialFailure together with accepted recommendations for the surviving provider. Policy has JSON tags; when you load one from configuration, decode it with json.Decoder.DisallowUnknownFields so a misspelled key cannot silently become a zero threshold.

Batches
items, err := ev.EvaluateBatch(ctx, reqs, discern.WithPolicy(routing))
if err != nil {
    return err // only when an item is invalid: zero calls for the whole batch
}
for _, it := range items {
    _ = it.Result // same contract as Evaluate, in input order
    _ = it.Err
}

At most WithBatchConcurrency(n) requests run at once (default 4), so up to 2n calls for dual requests. Every item is billed; nothing is deduplicated. Provider rate limits apply (in October 2026, Jev documented 80 requests per second, and Cloudflare 300 requests per minute for Clef's task type).

Storage

A Result is a versioned JSON record (discern.result/v1):

data, err := json.Marshal(res.Redact(discern.Redaction{CorrelationID: true}))
restored, err := discern.ParseResult(data) // ErrInvalidRecord on another schema, broken references or inconsistent verdicts
In a net/http handler
func (h *Handler) Route(w http.ResponseWriter, r *http.Request) {
    ctx, cancel := context.WithTimeout(r.Context(), 8*time.Second)
    defer cancel()
    res, err := h.ev.Evaluate(ctx, buildRequest(r))
    if res != nil {
        h.audit.Save(r.Context(), res)
    }
    switch {
    case errors.Is(err, discern.ErrInvalidRequest), errors.Is(err, discern.ErrUnsupported),
        errors.Is(err, discern.ErrInvalidPolicy):
        http.Error(w, "invalid request", http.StatusBadRequest) // zero calls made
        return
    case err != nil && !errors.Is(err, discern.ErrPartialFailure) && errors.Is(err, context.DeadlineExceeded):
        http.Error(w, "timeout", http.StatusGatewayTimeout)
        return
    case err != nil && !errors.Is(err, discern.ErrPartialFailure):
        http.Error(w, "upstream failure", http.StatusBadGateway)
        return
    }
    respond(w, res) // Complete or Partial
}
Testing your application

discerntest provides offline fakes that count calls. The evaluator validates fake answers exactly like real ones.

fake := discerntest.Static(discern.Jev, discern.Observation{Model: "jev-test", Answers: answers})
ev, err := discern.New(discern.WithAdapter(fake))
// ... exercise your code ...
calls := fake.Calls()
// discerntest.Func(provider, model, fn) and discerntest.Failing(provider, err) also exist.

βš™οΈ Behavior

Billing, retries and timeouts
  • One attempt per selected provider. No retries, fallback to the other provider, hedging, racing or shadow calls: each of these can bill twice. A dual request is the only way to make two calls. The SDK also switches off the standard library's own transparent replays: Go's HTTP/2 client would otherwise resend a POST whose stream was refused or reset, even after the server read the whole body.
  • Validation first. The request, the effective policy and every selected adapter (including provider limits such as Clef's 64 questions and 13 MiB body, and whether the answer could even fit in WithMaxResponseBytes) are checked before any call. A local error means nothing was sent; an already-canceled context also sends nothing.
  • Timeouts. Each adapter applies WithTimeout (default 30s) to the request and the full body read; the caller's context deadline still applies. An adapter timeout is timeout, and so is the Timeout of an injected http.Client when it fires first; the caller's deadline or cancellation is deadline_exceeded or canceled.
  • Retry hints. retry-after-ms (rounded up to a whole millisecond) and then Retry-After (seconds or an HTTP date) are reported in ProviderError.RetryAfter (set when HasRetryAfter is true) and in the record, capped at 24 hours. They are hints, not proof that repeating a call is safe or free. Retrying is your call.
  • No guarantees beyond the SDK. Cancelling a request does not promise the provider stopped work, and there is no exactly-once promise. An injected http.Client is copied with redirects disabled and no cookie jar; a custom Transport that retries or redirects on its own is outside these guarantees. The default transport honors the standard HTTPS_PROXY and NO_PROXY settings.
  • Bounded responses. Bodies above WithMaxResponseBytes (default 4 MiB) fail as response_too_large, and reading stops at the limit. Every body is closed, and one within the limit is read to the end, so its connection can be reused.
Errors
var pe *discern.ProviderError
errors.Is(err, discern.ErrInvalidRequest)     // also ErrInvalidPolicy, ErrUnsupported: zero calls
errors.Is(err, discern.ErrInvalidConfig)      // constructors: jev.New, clef.New, discern.New
errors.Is(err, discern.ErrPartialFailure)     // dual run: one provider failed, the other's observation is in the Result
errors.Is(err, discern.ErrRejected)           // non-2xx (including a redirect), or Cloudflare success:false or any errors[] entry
errors.Is(err, discern.ErrMalformedResponse)  // includes response_too_large
errors.Is(err, discern.ErrInvalidRecord)      // ParseResult and Recommend
errors.Is(err, context.Canceled)              // preserved through ProviderError
errors.As(err, &pe)                           // pe.Provider, pe.Kind, pe.StatusCode, pe.RequestID, pe.RetryAfter, pe.Codes

pe.Kind, also stored as the record's Failure.Kind, is one of timeout, deadline_exceeded, canceled, transport, rejected, malformed_response, response_too_large or adapter_error (an error from a custom or fake adapter that is not a ProviderError).

Error strings never contain credentials, account IDs, URLs, request state or provider bodies. Provider error messages can echo input, so they are only available through pe.ProviderMessages(): Cloudflare's errors[].message, and for Jev, whose error bodies are undocumented, the observed FastAPI-style detail, one message per error as error_type: message (for example api_usage_error: Unknown model: jev, or just max_tokens_exceeded). Refusal details are read leniently and kept bounded: at most 64 codes and 64 messages, each message cut to 1 KiB. Tools that walk error chains (such as testify failure messages) print the sanitized causes, which never include credentials, URLs, payloads or provider messages.

Confidence, scores and what the models return
  • Confidence is not the probability of being right. Both providers compute it from the probabilities as a concentration statistic. Jev's and Clef's values are not comparable, and the SDK never compares raw confidences across providers or picks a winner by them.

  • Noul is a probability, not a boolean, and has no separate confidence.

  • Score is ordinal and fractional (Ξ£ levelΒ·probability). Nearest() rounds to a level when you need one; magnitudes between levels are not calibrated.

  • No rationale. System One models do not explain themselves, and go-discern never invents a reason. Summaries describe only observed numbers and the SDK's own checks.

  • Rounding. Providers round probabilities without documenting how. Validation infers each distribution's precision and allows one rounding unit per value; nothing is ever renormalized. With many options at two decimals, one unit per value adds up, so the sum check is necessarily loose there.

  • Oversized state. Tokens cannot be counted locally, so the SDK never checks a context window before a call. What the providers did with state that did not fit, observed in October 2026:

    • Jev answered a state of about 31K tokens and refused one of about 70K with HTTP 400; its ProviderMessages() held max_tokens_exceeded.
    • clef (65,536-token window) and clef-flash (24,576) refused the request before the model ran: HTTP 422 with Cloudflare code 3030 ("Request requires N tokens; the maximum is M"), or, far over the window, HTTP 413 with code 5021.
    • clef-omni (64,000) cut the state to 64,000 tokens and answered, so not all of the state is guaranteed to influence its answer.

    Every refusal is rejected (it matches ErrRejected), with Cloudflare's code in the record's Failure.Codes and the provider's text in ProviderMessages(). Cloudflare's model pages say all three Clef models truncate, so treat this as observed behavior that can change.

Audit records and privacy

Records include caller identifiers (question, option and level IDs, the correlation ID, evidence references, the policy version), provider outputs and metadata, every SDK check, and storage-safe failures (kind, HTTP status, request IDs, retry hint, Cloudflare codes). They never include the state, instructions, option or level descriptions, legend text, credentials, headers, bodies, provider error messages or Go error strings. Values a provider reports about itself (the model name, request ID and ray ID) are kept only when they are at most 256 bytes of visible ASCII, so a provider cannot echo request content or line breaks into a record or a summary; a reported model that breaks the rule fails the call as malformed_response, and an invalid request or ray ID is dropped.

Identifiers and outputs can still be sensitive. Redact returns a copy without the fields you select (the correlation ID, evidence references, request and ray IDs, usage) and lists them in redacted; question, option and level IDs are kept because replay needs them, so keep them free of personal data. Nothing is persisted, logged or sent anywhere by the SDK.

A record supports traceability and local policy replay. It is not proof that an answer is true, not a tamper-evident ledger, and not a promise that a repeated call would return the same values. Optional values are pointers, so absent is never confused with zero; started_at is client wall-clock time (UTC) and elapsed_ns is client-observed time, not provider compute time. Within v1 the schema changes additively only; parsers must treat unknown reason codes as non-accepting.

Limits and unverified assumptions
Jev Clef
Questions per request not documented (65 answered in October 2026) 1–64 (checked)
Question IDs not documented [A-Za-z0-9_.-]{1,100} β€” applied to both so requests stay portable
Choice options ≀255 2–255 β€” 2–255 applied to both
Score levels 2–10 2–10
Request body not documented ≀13 MiB (checked on the exact bytes; documented for clef and clef-flash, applied to clef-omni too)

Option and level IDs, the correlation ID and each evidence reference are limited to 256 bytes of UTF-8 without control or invisible formatting characters, and at most 32 evidence references are accepted; these are library limits that keep records bounded. IDs are compared byte for byte, without Unicode normalization. The state and instructions must be text, a JSON object or a JSON array, and not blank: text of only white space, {} and [] are rejected before any call, since a provider would still bill for a blank state. Score level descriptions are required (Jev's docs also allow null), and JSON values with duplicate keys are rejected. Jev documents a context of 64k tokens per request (32k for the state plus the longest question) without saying what happens beyond it; in October 2026 it refused an oversized state with HTTP 400 (see Oversized state above). go-discern sends only text and JSON state: it does not support the image input of the Clef models, or the audio and video input of clef-omni.

The offline test suite cannot verify: actual rounding precision, whether Jev keeps its error-body shape (observed, not documented; read leniently), Cloudflare's cf-ai-req-id, cf-ray and cf-ai-neurons headers (not documented; seen on live calls in October 2026 and checked by the live tests, but captured best-effort), whether Cloudflare ever returns success:false with HTTP 200 (handled as a rejection) or reports truncation in messages[] (ignored), and whether providers keep option IDs byte for byte (for example, a JavaScript layer could reorder integer-like keys such as "2", "1"). Pin a versioned Jev model in production: jev-latest can move.


πŸ“š Documentation

View the generated documentation, the TypeSafe API reference and the Cloudflare Clef model page.

Package Purpose
discern Requests, the Evaluator, policies, recommendations and records
jev TypeSafe Jev adapter
clef Cloudflare Workers AI Clef adapter
discerntest Offline fakes for application tests
Development Setup (Getting Started)

Install MAGE-X build tool for development:

# Install MAGE-X for development and building
go install github.com/mrz1836/mage-x/cmd/magex@latest
magex update:install
Library Deployment

This project uses goreleaser for streamlined binary and library deployment to GitHub. To get started, install it via:

brew install goreleaser

The release process is defined in the .goreleaser.yml configuration file.

Then create and push a new Git tag using:

magex version:bump bump=patch push=true branch=master
Build Commands

View all build commands

magex help
GitHub Workflows

All workflows are driven by modular configuration in .github/env/ β€” no YAML editing required.

View all workflows and the control center β†’

Updating Dependencies

To update all dependencies (Go modules, linters, and related tools), run:

magex deps:update

πŸ§ͺ Examples & Tests

All unit tests, fuzz tests and examples run via GitHub Actions on Go 1.26.x (the supported baseline) and the current Go release. View the configuration file.

Run all tests (fast):

magex test

Run all tests with race detector (slower):

magex test:race

Runnable examples live in the Example* functions of the discern, jev and clef packages and in examples/, a synthetic support-ticket router that runs offline against fake providers by default:

go run ./examples                         # offline, no credentials needed
DISCERN_LIVE=1 go run ./examples -live    # billable: needs TYPESAFE_API_KEY, CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN
Live contract tests

Unit tests never touch the network. A separate, opt-in suite in internal/livetest calls the real Jev and Clef APIs and asserts the behavior the adapters rely on: every model's answers decode, refusals carry the expected HTTP status, Cloudflare codes and provider messages, timeouts are classified, the documented limits hold, and oversized state is refused or truncated as observed. When a provider changes its contract, a test fails with a message that starts with provider behavior changed? and shows what was observed. Run it on demand: before a release, or when a provider announces a change.

Credentials in your environment never start a call by themselves. Every gate must pass:

Variable Default Effect
DISCERN_LIVE unset Must be 1, or every live test skips. Set it only for the run.
TYPESAFE_API_KEY unset Enables the Jev tests.
CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_API_TOKEN unset Enable the Clef tests. Tests that use both providers need both sets.
DISCERN_LIVE_LARGE unset 1 adds the large-state probes, about $0.025 more per run.
DISCERN_LIVE_MAX_USD 0.10 Estimated spend, in US dollars, after which the remaining tests skip.
CI, DISCERN_LIVE_ALLOW_CI unset When CI is set, the suite skips unless DISCERN_LIVE_ALLOW_CI=1 is set too.

The suite also needs the live build tag. The magex test targets and CI compile it with that tag, and it skips there because DISCERN_LIVE is unset.

# Standard tier: about 60 calls, roughly $0.002
DISCERN_LIVE=1 go test -tags live -run TestLive -v ./internal/livetest

# Add the large-state probes
DISCERN_LIVE=1 DISCERN_LIVE_LARGE=1 go test -tags live -run TestLive -v ./internal/livetest

# One provider: unset the other provider's credentials
env -u CLOUDFLARE_API_TOKEN DISCERN_LIVE=1 go test -tags live -run TestLive -v ./internal/livetest

# One area, or one test
DISCERN_LIVE=1 go test -tags live -run 'TestLive/err/clef' -v ./internal/livetest
DISCERN_LIVE=1 go test -tags live -run 'TestLive/clef/kinds/clef-omni' -v ./internal/livetest

Tests run one at a time. A provider that answers HTTP 429 is skipped for the rest of the run, and the calls, tokens, neurons and estimated spend are logged at the end. Logs hold only response metadata (models, request and ray IDs, usage and answers), and every error and record is checked for the credentials.


⚑ Benchmarks

Run the Go benchmarks:

magex bench
πŸ“Š Performance Results

Benchmarks measure local CPU work only (validation, encoding, decoding, policy evaluation and record encoding) with in-memory fakes. They make no claim about network or provider latency, which dominates real calls. For lower latency, run a single provider per request: a dual run waits for the slower one.


πŸ› οΈ Code Standards

Read more about this Go project's code standards.


πŸ€– AI Usage & Assistant Guidelines

Read the AI Usage & Assistant Guidelines for details on how AI is used in this project and how to interact with the AI assistants.


πŸ‘₯ Maintainers

MrZ
MrZ

🀝 Contributing

View the contributing guidelines and please follow the code of conduct.

How can I help?

All kinds of contributions are welcome πŸ™Œ! The most basic way to show your support is to star 🌟 the project, or to raise issues πŸ’¬. You can also support this project by becoming a sponsor on GitHub πŸ‘ or by making a bitcoin donation to ensure this journey continues indefinitely! πŸš€

Stars


πŸ“ License

License

Documentation ΒΆ

Overview ΒΆ

Package discern is a small decision layer over two hosted System One judgment APIs: TypeSafe Jev (package jev) and Cloudflare Workers AI Clef (package clef).

One typed Request asks one or more independent questions about a state. Each question is a Choice (pick one of named options), a Noul (the probability that the answer is yes) or a Score (a probability-weighted position on ordered levels). The caller picks the provider for every request: Jev, Clef, or an explicit run on both.

jc, err := jev.New(apiKey, jev.WithModel("jev-1.13.0"))
cc, err := clef.New(accountID, apiToken, clef.WithModel(clef.ModelFlash))
ev, err := discern.New(discern.WithAdapter(jc), discern.WithAdapter(cc))

res, err := ev.Evaluate(ctx, discern.Request{
	Providers: discern.ClefOnly,
	State:     discern.Text(ticketText),
	Questions: []discern.Question{
		discern.Choice("queue", discern.Text("Which support queue should handle this ticket?"),
			discern.Opt("billing", discern.Text("Invoices, refunds, failed payments")),
			discern.Opt("technical", discern.Text("Errors, outages, integration problems"))),
	},
})

Observations ΒΆ

A Result always carries every provider's full observation: the chosen option and the probability of every option in request order, Noul and Score values, confidence when reported, usage, model and request identifiers. Optional provider fields are pointers, so an absent value is never read as zero.

Confidence is a concentration statistic computed from the probabilities. It is not the probability that an answer is correct, and values from Jev and Clef are not comparable. A Noul value is a probability, not a boolean. Neither provider returns a rationale; the SDK never invents one.

Recommendations ΒΆ

A Policy is an explicit set of threshold checks per question, applied locally to the observations. Without a policy no recommendation is made: every question gets the NoRule status and the observations are returned unchanged. Policies come from WithDefaultPolicy (per evaluator) or WithPolicy and WithoutPolicy (per call; the last option wins and the evaluator is never modified). Recommend re-applies another policy to a stored result without any provider call.

Each question gets one Recommendation with a status (Accepted, BelowThreshold, ReviewRequired, NotApplicable, Incomplete or NoRule), machine-readable reason codes, every evaluated check, and a deterministic summary. For a dual run the providers must agree (same Choice option, or Noul and Score values within the policy's Agreement tolerance); disagreement needs review unless the policy names a PreferOnConflict provider. When one provider of a dual run fails, questions are Incomplete unless the policy sets AllowPartial; with AllowPartial, Evaluate can return an error matching ErrPartialFailure together with Accepted recommendations for the surviving provider. Result.Overall is Accepted only if every question the policy covers is Accepted.

Calls, billing and retries ΒΆ

Every selected provider gets exactly one attempt. The SDK never retries, falls back to another provider, hedges or races requests: each call may be billed, and a dual run makes two potentially billable calls, run concurrently. One attempt includes the standard library's own transparent replays, which the adapters disable: net/http's HTTP/2 client would otherwise resend a request whose stream was refused or reset, even after the server read it. Request, policy and adapter validation all happen before any call, so a local error means nothing was sent. Retry hints from providers are reported in ProviderError and Failure; whether repeating a call is safe is the caller's decision. Injected HTTP transports that retry or follow redirects on their own are outside these guarantees.

The SDK never reads environment variables itself. When no HTTP client is injected, the adapters use http.DefaultTransport, which honors the standard proxy variables (HTTPS_PROXY, HTTP_PROXY and NO_PROXY) as net/http always does; inject a client to control that.

Errors ΒΆ

Evaluate returns a non-nil Result exactly when at least one call was attempted. Persist it before inspecting the error. ErrPartialFailure means one of two providers failed and the other's observation is usable; under a policy with AllowPartial its recommendations may be Accepted. Local failures match ErrInvalidRequest, ErrInvalidPolicy or ErrUnsupported, and the error of a call that was made never matches them. Provider failures are *ProviderError values that match ErrRejected or ErrMalformedResponse, or wrap context.Canceled and context.DeadlineExceeded. Error strings never contain credentials, URLs, request state or provider bodies. Tools that walk error chains, such as testify's failure messages, also print the causes; the jev and clef adapters keep only sanitized causes, which never include credentials, URLs, payloads or provider messages.

Records ΒΆ

A Result is a versioned JSON record (SchemaVersion) that never contains the state, instructions, descriptions, credentials or provider bodies. It does contain caller identifiers (question, option and level IDs, the correlation ID and evidence references) and provider outputs, which may still be sensitive; Redact removes the optional identifiers. Provider-reported identifiers (the model, request ID and ray ID) are kept only when they are visible ASCII of at most 256 bytes, so provider-echoed text cannot reach a record or a summary. ParseResult restores a record and checks its internal references and that its recommendations and overall verdict are consistent with its policy snapshot. A record supports traceability and local policy replay; it is not proof that an answer is true, not tamper-evident, and not a promise that a repeated call would return the same values.

Testing ΒΆ

Package discerntest provides fake adapters with call counting, so application code can be tested without network access.

Index ΒΆ

Examples ΒΆ

Constants ΒΆ

View Source
const (
	// MaxQuestionIDLength is the longest question ID (Clef's documented limit).
	MaxQuestionIDLength = 100

	// MinChoiceOptions and MaxChoiceOptions bound a Choice question's options.
	MinChoiceOptions = 2
	MaxChoiceOptions = 255

	// MinScoreLevels and MaxScoreLevels bound a Score question's levels.
	MinScoreLevels = 2
	MaxScoreLevels = 10

	// MaxIdentifierLength bounds option IDs, level IDs, the correlation ID, each
	// evidence reference, check IDs, check models and the policy version, in
	// bytes. These identifiers must be valid UTF-8 without control or invisible
	// format characters (Unicode categories Cc and Cf, such as U+200B and U+202E).
	// They are compared byte for byte, without Unicode normalization, so two IDs
	// that look the same but are encoded differently are different IDs.
	MaxIdentifierLength = 256

	// MaxEvidenceRefs bounds Request.EvidenceRefs.
	MaxEvidenceRefs = 32
)

Documented library limits. Some are narrower than a provider contract so that a request valid for one provider is valid for both, and records stay bounded.

View Source
const DefaultBatchConcurrency = 4

DefaultBatchConcurrency is the number of requests EvaluateBatch runs at once unless WithBatchConcurrency says otherwise.

View Source
const SchemaVersion = "discern.result/v1"

SchemaVersion identifies the JSON layout of a Result. Changes within v1 are additive only.

Variables ΒΆ

View Source
var (
	// ErrInvalidConfig reports an invalid constructor argument or option.
	ErrInvalidConfig = errors.New("discern: invalid configuration")

	// ErrInvalidRequest reports a request that fails local validation.
	ErrInvalidRequest = errors.New("discern: invalid request")

	// ErrInvalidPolicy reports a recommendation policy that fails validation.
	ErrInvalidPolicy = errors.New("discern: invalid policy")

	// ErrUnsupported reports a request that a selected provider cannot accept,
	// such as more questions than the provider allows.
	ErrUnsupported = errors.New("discern: unsupported by provider")

	// ErrRejected reports a provider that refused the call: a non-2xx HTTP
	// status (including a redirect), or a Cloudflare envelope with success:false
	// or any errors[] entry.
	ErrRejected = errors.New("discern: rejected by provider")

	// ErrMalformedResponse reports a provider response that could not be decoded
	// or failed validation, including one larger than the configured limit and
	// one whose model, request ID or ray ID is not a provider identifier (see
	// Observation).
	ErrMalformedResponse = errors.New("discern: malformed provider response")

	// ErrPartialFailure reports a dual run in which exactly one provider failed.
	// The Result still holds the surviving provider's observation.
	ErrPartialFailure = errors.New("discern: partial failure")

	// ErrInvalidRecord reports a stored result record that ParseResult rejects.
	ErrInvalidRecord = errors.New("discern: invalid result record")
)

Sentinel errors. Match them with errors.Is.

ErrInvalidConfig, ErrInvalidRequest, ErrInvalidPolicy and ErrUnsupported are local: an error matching one of them means no provider call was made. The error of a call that was made never matches them, nor ErrInvalidRecord, and matches ErrPartialFailure only as Evaluate reports it: an adapter call error that matches one of these sentinels is recorded with a static cause instead.

Functions ΒΆ

This section is empty.

Types ΒΆ

type Adapter ΒΆ

type Adapter interface {
	Provider() Provider
	Model() string
	Prepare(req *Request) (Call, error)
}

Adapter is the provider boundary. The jev and clef packages implement it, and discerntest provides fakes.

Prepare validates the request against the provider's documented limits and encodes the exact bytes to send, without any I/O. It returns an error matching ErrInvalidRequest or ErrUnsupported when the request cannot be sent. Such errors are returned to callers unchanged, so they must not contain caller data (a *ValidationError, which names fields rather than values, fits). Any other Prepare error is reported as ErrUnsupported with a fixed message that names only the provider; the adapter's error stays reachable through errors.Is and errors.As. The request is the adapter's own copy, so the returned Call may keep it.

New calls Provider and Model to check the adapter; one for which either panics, such as a typed nil pointer, is rejected with ErrInvalidConfig. Adapters must be safe for concurrent use.

type Agreement ΒΆ

type Agreement struct {
	MaxDifference float64 `json:"max_difference"`
}

Agreement is the largest allowed absolute difference between the two providers' Noul or Score values. The comparison allows 1e-9 of floating-point noise, so zero is valid but accepts values up to 1e-9 apart rather than only strictly equal ones.

type Answer ΒΆ

type Answer struct {
	QuestionID string        `json:"question_id"`
	Kind       Kind          `json:"kind"`
	Choice     *ChoiceAnswer `json:"choice,omitempty"`
	Noul       *NoulAnswer   `json:"noul,omitempty"`
	Score      *ScoreAnswer  `json:"score,omitempty"`
}

Answer is one typed answer. Exactly one of Choice, Noul and Score is set, matching Kind.

type BatchItem ΒΆ

type BatchItem struct {
	Result *Result
	Err    error
}

BatchItem is one EvaluateBatch result, with the same contract as Evaluate: Result is nil exactly when no call was attempted for the item.

type Call ΒΆ

type Call func(ctx context.Context) (*Observation, error)

Call performs one prepared provider call. It must honor ctx, make at most one attempt, and return either an Observation or an error (preferably a *ProviderError). A call error that matches a local sentinel, such as ErrInvalidRequest, is recorded with a static cause, because those sentinels mean that no call was made.

type Check ΒΆ

type Check struct {
	// ID is the rule identifier recorded in results. When empty, it defaults to
	// "<question>/checks/<index>".
	ID        string   `json:"id"`
	Provider  Provider `json:"provider"`
	Model     string   `json:"model"`
	Metric    Metric   `json:"metric"`
	Op        Op       `json:"op"`
	Threshold float64  `json:"threshold"`
}

Check is one threshold rule. An empty Provider applies the check to every provider; an empty Model applies it to every model. A check scoped to a model matches the model the provider reported, falling back to the requested model, so an alias that moves to a new version stops matching.

type CheckResult ΒΆ

type CheckResult struct {
	RuleID        string     `json:"rule_id"`
	Provider      Provider   `json:"provider"`
	ScopeProvider Provider   `json:"scope_provider"`
	ScopeModel    string     `json:"scope_model"`
	ObservedModel string     `json:"observed_model"`
	Metric        Metric     `json:"metric"`
	Op            Op         `json:"op"`
	Threshold     float64    `json:"threshold"`
	Observed      *float64   `json:"observed"`
	Passed        bool       `json:"passed"`
	Skipped       ReasonCode `json:"skipped,omitempty"`
}

CheckResult is one evaluated check for one provider, or the agreement test (RuleID "<question>/agreement", empty Provider). Observed is nil when the metric was absent or the check was skipped.

A Choice agreement test compares options, not numbers: its Op is Equal, its Threshold is 0 and its Observed is nil, and Passed says whether both providers chose the same option.

type ChoiceAnswer ΒΆ

type ChoiceAnswer struct {
	Option        string              `json:"option"`
	Confidence    *float64            `json:"confidence,omitempty"`
	Probabilities []OptionProbability `json:"probabilities"`
}

ChoiceAnswer is a Choice result: the chosen option, the probability of every option in request order, and the provider's confidence when reported.

Confidence is a concentration statistic computed from the probabilities, not the probability that the answer is correct.

type ChoiceOption ΒΆ

type ChoiceOption struct {
	ID          string
	Description Value
}

ChoiceOption is one Choice alternative. ID is sent to the provider as the option name; Description may be the zero Value (sent as null).

func Opt ΒΆ

func Opt(id string, description Value) ChoiceOption

Opt returns a Choice option.

type EvalOption ΒΆ

type EvalOption func(*evalConfig)

EvalOption configures one Evaluate or EvaluateBatch call. It never changes the Evaluator. The last policy option wins.

func WithPolicy ΒΆ

func WithPolicy(p Policy) EvalOption

WithPolicy replaces the effective policy for this call only. The policy is copied now; the evaluator's default is untouched.

func WithoutPolicy ΒΆ

func WithoutPolicy() EvalOption

WithoutPolicy disables the default policy for this call: the result holds observations and NoRule recommendations only.

type Evaluator ΒΆ

type Evaluator struct {
	// contains filtered or unexported fields
}

Evaluator runs requests on registered adapters and applies recommendation policies. It is immutable after New and safe for concurrent use.

func New ΒΆ

func New(opts ...Option) (*Evaluator, error)

New returns an Evaluator. It returns an error matching ErrInvalidConfig when no adapter is registered, an adapter is nil (including a typed nil pointer, such as a nil *jev.Client, whose Provider or Model panics) or duplicates a provider, or the batch concurrency is below 1, and one matching ErrInvalidPolicy when the default policy is invalid.

func (*Evaluator) Evaluate ΒΆ

func (e *Evaluator) Evaluate(ctx context.Context, req Request, opts ...EvalOption) (*Result, error)

Evaluate validates the request and the effective policy, prepares every selected adapter, and then makes one call per selected provider; a dual request runs both calls concurrently and waits for both. One provider's failure never cancels the other.

The returned Result is non-nil exactly when at least one call was attempted; persist it before inspecting err. err is nil when every provider succeeded (Result.Status Complete); it matches ErrPartialFailure when one of two failed (Partial); otherwise it joins the provider errors (Failed). When no call was made, err matches ErrInvalidRequest, ErrUnsupported or ErrInvalidPolicy, or is the context's error.

Example ΒΆ
package main

import (
	"context"
	"fmt"

	"github.com/mrz1836/go-discern"
	"github.com/mrz1836/go-discern/discerntest"
)

// exampleRequest is a synthetic support ticket with one routing question.
func exampleRequest(providers discern.ProviderSet) discern.Request {
	return discern.Request{
		Providers: providers,
		State:     discern.Text("Subject: Charged twice\n\nMy invoice was paid twice this month."),
		Questions: []discern.Question{
			discern.Choice("queue", discern.Text("Which support queue should handle this ticket?"),
				discern.Opt("billing", discern.Text("Invoices, refunds, failed payments")),
				discern.Opt("technical", discern.Text("Errors, outages, integration problems")),
				discern.Opt("account", discern.Text("Login, profile, permissions"))),
		},
		CorrelationID: "ticket-1042",
	}
}

// exampleObservation is what a fake provider answers for exampleRequest.
func exampleObservation(model, option string, billing, technical, account float64) discern.Observation {
	return discern.Observation{Model: model, Answers: []discern.Answer{{
		QuestionID: "queue", Kind: discern.KindChoice,
		Choice: &discern.ChoiceAnswer{Option: option, Probabilities: []discern.OptionProbability{
			{Option: "billing", Probability: billing},
			{Option: "technical", Probability: technical},
			{Option: "account", Probability: account},
		}},
	}}}
}

func main() {
	fake := discerntest.Static(discern.Clef, exampleObservation("clef-flash", "billing", 0.87, 0.1, 0.03))
	ev, err := discern.New(discern.WithAdapter(fake))
	if err != nil {
		fmt.Println(err)
		return
	}

	res, err := ev.Evaluate(context.Background(), exampleRequest(discern.ClefOnly))
	if err != nil {
		fmt.Println(err)
		return
	}
	obs, _ := res.Observation(discern.Clef)
	q, _ := obs.Choice("queue")
	fmt.Println(res.Status, q.Option)
	for _, p := range q.Probabilities {
		fmt.Println(p.Option, p.Probability)
	}
	fmt.Println(fake.Calls(), "call")
}
Output:
complete billing
billing 0.87
technical 0.1
account 0.03
1 call
Example (DualWithPolicy) ΒΆ
package main

import (
	"context"
	"errors"
	"fmt"

	"github.com/mrz1836/go-discern"
	"github.com/mrz1836/go-discern/discerntest"
)

// exampleRequest is a synthetic support ticket with one routing question.
func exampleRequest(providers discern.ProviderSet) discern.Request {
	return discern.Request{
		Providers: providers,
		State:     discern.Text("Subject: Charged twice\n\nMy invoice was paid twice this month."),
		Questions: []discern.Question{
			discern.Choice("queue", discern.Text("Which support queue should handle this ticket?"),
				discern.Opt("billing", discern.Text("Invoices, refunds, failed payments")),
				discern.Opt("technical", discern.Text("Errors, outages, integration problems")),
				discern.Opt("account", discern.Text("Login, profile, permissions"))),
		},
		CorrelationID: "ticket-1042",
	}
}

// exampleObservation is what a fake provider answers for exampleRequest.
func exampleObservation(model, option string, billing, technical, account float64) discern.Observation {
	return discern.Observation{Model: model, Answers: []discern.Answer{{
		QuestionID: "queue", Kind: discern.KindChoice,
		Choice: &discern.ChoiceAnswer{Option: option, Probabilities: []discern.OptionProbability{
			{Option: "billing", Probability: billing},
			{Option: "technical", Probability: technical},
			{Option: "account", Probability: account},
		}},
	}}}
}

func main() {
	jev := discerntest.Static(discern.Jev, exampleObservation("jev-1.13.0", "billing", 0.91, 0.06, 0.03))
	clef := discerntest.Static(discern.Clef, exampleObservation("clef", "billing", 0.87, 0.1, 0.03))
	routing := discern.Policy{
		Version: "routing-v3",
		Questions: []discern.QuestionPolicy{{
			Question: "queue",
			Checks: []discern.Check{
				{ID: "queue-jev", Provider: discern.Jev, Metric: discern.MetricChoiceProbability, Op: discern.AtLeast, Threshold: 0.80},
				{ID: "queue-clef", Provider: discern.Clef, Metric: discern.MetricChoiceProbability, Op: discern.AtLeast, Threshold: 0.75},
			},
		}},
	}
	ev, err := discern.New(discern.WithAdapter(jev), discern.WithAdapter(clef), discern.WithDefaultPolicy(routing))
	if err != nil {
		fmt.Println(err)
		return
	}

	res, err := ev.Evaluate(context.Background(), exampleRequest(discern.JevAndClef))
	switch {
	case err == nil:
	case errors.Is(err, discern.ErrPartialFailure):
		fmt.Println("one provider failed")
	default:
		fmt.Println(err)
		return
	}
	rec, _ := res.Recommendation("queue")
	fmt.Println(rec.Status, rec.Selected.Option, rec.Selected.Providers)
	fmt.Println(rec.Reasons)
	fmt.Println(rec.Summary)
	fmt.Println(res.Overall.Status)
}
Output:
accepted billing [jev clef]
[providers_agree]
accepted billing: jev choice.probability 0.91 β‰₯ 0.8; clef choice.probability 0.87 β‰₯ 0.75; both chose billing
accepted

func (*Evaluator) EvaluateBatch ΒΆ

func (e *Evaluator) EvaluateBatch(ctx context.Context, reqs []Request, opts ...EvalOption) ([]BatchItem, error)

EvaluateBatch evaluates independent requests with at most WithBatchConcurrency requests in flight. Every item is validated and prepared first: if any item is invalid, it returns an error naming the index and makes no call at all. Otherwise each item follows Evaluate's contract and the items are returned in input order. Items are billed separately and never deduplicated. When ctx is done, items not yet started get Result nil and the context's error.

If an adapter panics, no further item is started; the panic is re-raised once the items in flight have finished.

Prepared requests (including each adapter's encoded body) are held until their item runs, so peak memory is roughly the size of the batch's input.

type Failure ΒΆ

type Failure struct {
	Kind       FailureKind `json:"kind"`
	StatusCode int         `json:"status_code,omitempty"`
	RequestID  string      `json:"request_id,omitempty"`
	RayID      string      `json:"ray_id,omitempty"`

	// RetryAfter is the provider's retry hint, when supplied. It is a hint, not
	// proof that repeating the call is safe.
	RetryAfter *time.Duration `json:"retry_after_ns,omitempty"`

	// Codes are provider error codes, such as Cloudflare errors[].code.
	Codes []int `json:"codes,omitempty"`
}

Failure is the storage-safe form of a provider error. RequestID and RayID are provider identifiers, like an Observation's: a value that is not empty or 1 to 256 bytes of visible ASCII is dropped before it is recorded.

type FailureKind ΒΆ

type FailureKind string

FailureKind classifies a provider failure. The list is closed; parsers of stored records should treat an unknown kind as a failure of unknown cause.

const (
	// FailureTimeout means the call's own timeout expired, such as the adapter's
	// timeout or its HTTP client's, rather than the caller's deadline.
	FailureTimeout FailureKind = "timeout"

	// FailureDeadlineExceeded means the caller's context deadline expired.
	FailureDeadlineExceeded FailureKind = "deadline_exceeded"

	// FailureCanceled means the caller's context was canceled.
	FailureCanceled FailureKind = "canceled"

	// FailureTransport means the request failed in transit (DNS, TLS, connection
	// reset, or an incomplete body). The provider may or may not have run it.
	FailureTransport FailureKind = "transport"

	// FailureRejected means the provider refused the call: a non-2xx HTTP status
	// (including a redirect), or a Cloudflare envelope with success:false or any
	// errors[] entry. It matches ErrRejected.
	FailureRejected FailureKind = "rejected"

	// FailureMalformed means the response failed decoding or validation. It matches
	// ErrMalformedResponse.
	FailureMalformed FailureKind = "malformed_response"

	// FailureTooLarge means the response exceeded the configured size limit. It
	// matches ErrMalformedResponse.
	FailureTooLarge FailureKind = "response_too_large"

	// FailureAdapter means an adapter returned an error that is neither a
	// *ProviderError nor a ProviderError value, such as a test fake's error, or a
	// ProviderError without a kind.
	FailureAdapter FailureKind = "adapter_error"
)

Failure kinds.

type Kind ΒΆ

type Kind string

Kind is a question type.

const (
	// KindChoice picks one option from a caller-defined list.
	KindChoice Kind = "choice"

	// KindNoul returns the probability that the answer is yes.
	KindNoul Kind = "noul"

	// KindScore rates the state on an ordered list of levels.
	KindScore Kind = "score"
)

Question kinds.

type LevelProbability ΒΆ

type LevelProbability struct {
	Level       string  `json:"level"`
	Probability float64 `json:"probability"`
}

LevelProbability is the probability of one Score level.

type Library ΒΆ

type Library struct {
	Module  string `json:"module"`
	Version string `json:"version"`
}

Library identifies the module that produced a Result.

type Metric ΒΆ

type Metric string

Metric is an observed quantity a Check compares with its threshold.

const (
	// MetricChoiceProbability is the probability of the option the provider chose,
	// in [0,1]. Choice only.
	MetricChoiceProbability Metric = "choice.probability"

	// MetricConfidence is the provider's concentration statistic, in [0,1]. It is
	// not the probability that the answer is correct, and values from different
	// providers are not comparable. Choice and Score only.
	MetricConfidence Metric = "confidence"

	// MetricNoul is the Noul probability of yes, in [0,1]. Noul only.
	MetricNoul Metric = "noul"

	// MetricScore is the fractional Score value, in [0, levels-1]. Score only.
	MetricScore Metric = "score"

	// MetricAgreement appears only in CheckResult, for the dual-provider agreement
	// test. It is not valid in a Check.
	MetricAgreement Metric = "agreement"
)

Metrics.

type NoulAnswer ΒΆ

type NoulAnswer struct {
	Value float64 `json:"value"`
}

NoulAnswer is a Noul result. Value is the probability that the answer is yes, in [0,1]; it is not a boolean and carries no separate confidence.

type Observation ΒΆ

type Observation struct {
	// Model is the model the provider reported, or empty if it reported none.
	Model string `json:"model,omitempty"`

	// RequestID is the provider's request identifier, when supplied.
	RequestID string `json:"request_id,omitempty"`

	// RayID is Cloudflare's cf-ray identifier, when supplied.
	RayID string `json:"ray_id,omitempty"`

	Usage   *Usage   `json:"usage,omitempty"`
	Answers []Answer `json:"answers"`
}

Observation is a validated provider response. Answers are in request order. Optional provider fields are pointers or empty strings when absent.

Model, RequestID and RayID are provider identifiers: each is empty or 1 to 256 bytes of visible ASCII (0x21 to 0x7E). An observation that breaks the rule fails validation as FailureMalformed, so provider-echoed text never reaches a record or a recommendation summary.

func (*Observation) Answer ΒΆ

func (o *Observation) Answer(questionID string) (Answer, bool)

Answer returns a copy of the answer to the question.

func (*Observation) Choice ΒΆ

func (o *Observation) Choice(questionID string) (ChoiceAnswer, bool)

Choice returns a copy of the Choice answer to the question.

func (*Observation) Clone ΒΆ

func (o *Observation) Clone() Observation

Clone returns a deep copy of the observation.

func (*Observation) Noul ΒΆ

func (o *Observation) Noul(questionID string) (NoulAnswer, bool)

Noul returns the Noul answer to the question.

func (*Observation) Score ΒΆ

func (o *Observation) Score(questionID string) (ScoreAnswer, bool)

Score returns a copy of the Score answer to the question.

type Op ΒΆ

type Op string

Op is a threshold comparison: observed <op> threshold.

const (
	// AtLeast passes when observed >= threshold.
	AtLeast Op = "gte"

	// Above passes when observed > threshold.
	Above Op = "gt"

	// AtMost passes when observed <= threshold.
	AtMost Op = "lte"

	// Below passes when observed < threshold.
	Below Op = "lt"

	// Equal appears only in CheckResult, for a Choice agreement test (both
	// providers chose the same option). It is not valid in a Check.
	Equal Op = "eq"
)

Comparison operators.

type Option ΒΆ

type Option func(*config)

Option configures an Evaluator in New. Nil options are ignored, and the last option of each kind wins, except that registering a second adapter for the same provider is an error.

func WithAdapter ΒΆ

func WithAdapter(a Adapter) Option

WithAdapter registers a provider adapter, such as one from jev.New or clef.New.

func WithBatchConcurrency ΒΆ

func WithBatchConcurrency(n int) Option

WithBatchConcurrency sets how many requests EvaluateBatch runs at once. A dual request makes two concurrent calls, so up to 2n calls can be in flight. n must be at least 1.

func WithDefaultPolicy ΒΆ

func WithDefaultPolicy(p Policy) Option

WithDefaultPolicy sets the policy applied to every evaluation that does not override it. The policy is copied now and validated in New.

type OptionProbability ΒΆ

type OptionProbability struct {
	Option      string  `json:"option"`
	Probability float64 `json:"probability"`
}

OptionProbability is the probability of one Choice option.

type Outcome ΒΆ

type Outcome struct {
	Provider       Provider      `json:"provider"`
	RequestedModel string        `json:"requested_model"`
	Status         OutcomeStatus `json:"status"`
	Elapsed        time.Duration `json:"elapsed_ns"`
	Observation    *Observation  `json:"observation,omitempty"`
	Failure        *Failure      `json:"failure,omitempty"`
}

Outcome is one provider call. Exactly one of Observation and Failure is set.

type OutcomeStatus ΒΆ

type OutcomeStatus string

OutcomeStatus is the status of one provider call.

const (
	// OutcomeSucceeded means the call returned a valid observation.
	OutcomeSucceeded OutcomeStatus = "succeeded"

	// OutcomeFailed means the call failed; Outcome.Failure says how.
	OutcomeFailed OutcomeStatus = "failed"
)

Outcome statuses.

type Overall ΒΆ

type Overall struct {
	// Status is NoRule when no question is covered; otherwise the most severe
	// covered status, so it is Accepted only if every covered question is.
	Status RecommendationStatus `json:"status"`

	// Uncovered lists the questions with status NoRule, in request order.
	Uncovered []string `json:"uncovered"`
}

Overall is the per-request verdict over the questions the policy covers.

type Policy ΒΆ

type Policy struct {
	// Version is a caller label recorded with every result. The SDK does not
	// interpret it.
	Version string `json:"version"`

	// Questions holds at most one entry per question ID.
	Questions []QuestionPolicy `json:"questions"`

	// PreferOnConflict, when set, accepts the named provider's answer if the two
	// providers disagree and that provider passes its checks. The default ("")
	// makes disagreement ReviewRequired.
	PreferOnConflict Provider `json:"prefer_on_conflict"`

	// AllowPartial, when true, evaluates the surviving provider of a dual run alone
	// when the other failed. The default (false) makes such questions Incomplete.
	//
	// With AllowPartial, Evaluate can return an error matching ErrPartialFailure
	// together with Accepted recommendations that select the surviving provider:
	// read the recommendations, not only the error, before acting.
	AllowPartial bool `json:"allow_partial"`
}

Policy is an explicit, typed recommendation policy. It is applied locally to observations and never causes a provider call. The zero Policy is invalid: Version is required.

Policy and its parts carry JSON tags, so a policy can be loaded from configuration. Decode it with a json.Decoder that has DisallowUnknownFields set: otherwise a misspelled key, such as "treshold", is silently ignored and leaves a zero threshold that every value meets.

A dual run evaluates each provider's answer against the checks that apply to it, so a policy for dual runs needs at least one applicable check for each provider: with checks scoped only to the preferred provider, the other provider has none, and the question is NotApplicable on dual runs.

type PolicySnapshot ΒΆ

type PolicySnapshot struct {
	Source    PolicySource `json:"source"`
	Policy    *Policy      `json:"policy"`
	Unmatched []string     `json:"unmatched"`
}

PolicySnapshot records the effective policy of a result, with defaults made explicit. Unmatched lists policy questions that matched no request question.

type PolicySource ΒΆ

type PolicySource string

PolicySource says where a result's effective policy came from.

const (
	// PolicyNone means no policy was configured.
	PolicyNone PolicySource = "none"

	// PolicyClientDefault means the evaluator's WithDefaultPolicy applied.
	PolicyClientDefault PolicySource = "client_default"

	// PolicyCallOverride means a per-call WithPolicy applied.
	PolicyCallOverride PolicySource = "call_override"

	// PolicyDisabled means a per-call WithoutPolicy disabled the default.
	PolicyDisabled PolicySource = "disabled"

	// PolicyRecommend means Recommend applied the policy to a stored result.
	PolicyRecommend PolicySource = "recommend"
)

Policy sources.

type Provider ΒΆ

type Provider string

Provider identifies a System One provider.

const (
	// Jev is TypeSafe's hosted Jev API.
	Jev Provider = "jev"

	// Clef is Cloudflare Workers AI's Clef model family.
	Clef Provider = "clef"
)

Providers.

type ProviderError ΒΆ

type ProviderError struct {
	Provider      Provider
	Kind          FailureKind
	StatusCode    int
	RequestID     string
	RayID         string
	RetryAfter    time.Duration
	HasRetryAfter bool
	Codes         []int
	// contains filtered or unexported fields
}

ProviderError describes a failed provider call. Its Error string is built only from the provider, kind and HTTP status: it never contains credentials, URLs, headers, request state or response bodies. Tools that walk error chains, such as testify's failure messages, also print the cause returned by Unwrap. The jev and clef adapters store only sanitized causes, which never include credentials, URLs, payloads or provider messages; a custom adapter's cause is its own.

Adapters may return a ProviderError as a pointer or as a value. Request and ray IDs that are not provider identifiers (see Observation) are dropped when the error is recorded.

RetryAfter is a provider hint, not proof that repeating the call is safe or free. HasRetryAfter distinguishes an explicit zero from an absent hint.

func NewProviderError ΒΆ

func NewProviderError(provider Provider, kind FailureKind, cause error, messages ...string) *ProviderError

NewProviderError returns a ProviderError for adapters. The cause is reachable through errors.Is and errors.As but never appears in Error; tools that walk error chains print it, so it must not carry secrets, URLs or payloads (store a *url.Error's inner error, not the *url.Error). The messages are provider-supplied texts, such as Cloudflare errors[].message, and are available only through ProviderMessages.

func (ProviderError) Error ΒΆ

func (e ProviderError) Error() string

Error returns "discern: <provider>: <kind>", with the HTTP status when known.

func (ProviderError) GoString ΒΆ

func (e ProviderError) GoString() string

GoString prints the metadata fields only; the cause and provider messages are omitted so that %#v cannot leak them.

func (ProviderError) Is ΒΆ

func (e ProviderError) Is(target error) bool

Is maps the failure kind onto the package sentinels.

func (ProviderError) ProviderMessages ΒΆ

func (e ProviderError) ProviderMessages() []string

ProviderMessages returns provider-supplied error texts, such as Cloudflare errors[].message. They may echo request content, so they are excluded from Error, GoString and result records; logging them is the caller's decision.

func (ProviderError) Unwrap ΒΆ

func (e ProviderError) Unwrap() error

Unwrap returns the sanitized cause, such as context.Canceled.

type ProviderSet ΒΆ

type ProviderSet string

ProviderSet selects the providers a request runs on. The zero value is invalid, so every request must choose explicitly.

const (
	// JevOnly runs the request on Jev.
	JevOnly ProviderSet = "jev"

	// ClefOnly runs the request on Clef.
	ClefOnly ProviderSet = "clef"

	// JevAndClef runs the request on both providers concurrently: two potentially
	// billable calls.
	JevAndClef ProviderSet = "jev+clef"
)

Provider sets.

func (ProviderSet) Has ΒΆ

func (s ProviderSet) Has(p Provider) bool

Has reports whether the set selects p.

func (ProviderSet) Providers ΒΆ

func (s ProviderSet) Providers() []Provider

Providers returns the selected providers in fixed order (Jev, then Clef), or nil for an invalid set.

type Question ΒΆ

type Question struct {
	// ID identifies the question; it must match [A-Za-z0-9_.-]{1,100}.
	ID string

	// Kind is the question type.
	Kind Kind

	// Instructions say what to decide: non-empty text, or a JSON object or array.
	Instructions Value

	// Options are the Choice alternatives, in the order sent to the provider.
	// Order matters: models can lean toward earlier options.
	Options []ChoiceOption

	// Levels are the Score levels, lowest first.
	Levels []ScoreLevel

	// Yes and No optionally describe what a yes and a no mean for a Noul question.
	Yes, No Value
}

Question is one typed question. Build it with Choice, Noul or Score.

func Choice ΒΆ

func Choice(id string, instructions Value, options ...ChoiceOption) Question

Choice returns a Choice question with the options in the given order.

func Noul ΒΆ

func Noul(id string, instructions Value) Question

Noul returns a Noul (yes/no probability) question. Set Yes and No on the result to describe the two ends.

func Score ΒΆ

func Score(id string, instructions Value, levels ...ScoreLevel) Question

Score returns a Score question with the levels lowest first.

type QuestionInfo ΒΆ

type QuestionInfo struct {
	ID      string   `json:"id"`
	Kind    Kind     `json:"kind"`
	Options []string `json:"options,omitempty"`
	Levels  []string `json:"levels,omitempty"`
}

QuestionInfo records a question's identity and its option or level IDs, in request order. It never holds instructions or descriptions.

type QuestionPolicy ΒΆ

type QuestionPolicy struct {
	Question string  `json:"question"`
	Checks   []Check `json:"checks"`

	// Agreement is the tolerance for dual Noul and Score questions; without it a
	// dual numeric question is NotApplicable. It is invalid on Choice questions,
	// which agree when both providers choose the same option.
	Agreement *Agreement `json:"agreement"`
}

QuestionPolicy holds the checks for one question. All applicable checks must pass (AND); each check is evaluated separately for each provider it applies to.

type ReasonCode ΒΆ

type ReasonCode string

ReasonCode is a machine-readable reason for a recommendation. The list is closed and documented; parsers of stored records must treat an unknown code as non-accepting.

const (
	// ReasonNoPolicy means no policy was in effect (NoRule).
	ReasonNoPolicy ReasonCode = "no_policy"

	// ReasonQuestionNotInPolicy means the policy has no entry for the question (NoRule).
	ReasonQuestionNotInPolicy ReasonCode = "question_not_in_policy"

	// ReasonProviderFailed means a provider the policy needs failed (Incomplete).
	ReasonProviderFailed ReasonCode = "provider_failed"

	// ReasonMetricAbsent means a checked metric was not reported (Incomplete).
	ReasonMetricAbsent ReasonCode = "metric_absent"

	// ReasonNoApplicableCheck means no check applies to a provider (NotApplicable).
	ReasonNoApplicableCheck ReasonCode = "no_applicable_check"

	// ReasonModelMismatch means a provider's checks are all scoped to other models
	// (NotApplicable).
	ReasonModelMismatch ReasonCode = "model_mismatch"

	// ReasonAgreementMissing means a dual Noul or Score question has no Agreement
	// tolerance (NotApplicable).
	ReasonAgreementMissing ReasonCode = "agreement_missing"

	// ReasonProvidersDisagree means the two providers disagreed.
	ReasonProvidersDisagree ReasonCode = "providers_disagree"

	// ReasonBelowThreshold means at least one applicable check failed. It comes
	// with BelowThreshold, and with ReviewRequired when the providers of a dual
	// run disagreed and a check also failed.
	ReasonBelowThreshold ReasonCode = "below_threshold"

	// ReasonThresholdsMet means a single provider passed every check (Accepted).
	ReasonThresholdsMet ReasonCode = "thresholds_met"

	// ReasonProvidersAgree means both providers agreed and passed (Accepted).
	ReasonProvidersAgree ReasonCode = "providers_agree"

	// ReasonPreferredOnConflict means the preferred provider passed and was selected
	// despite a disagreement (Accepted).
	ReasonPreferredOnConflict ReasonCode = "preferred_on_conflict"

	// ReasonPartialSingleProvider means AllowPartial evaluated the surviving
	// provider of a dual run alone.
	ReasonPartialSingleProvider ReasonCode = "partial_single_provider"

	// ReasonModelCheckSkipped means a check scoped to another model was skipped
	// for a provider while other checks still applied to it, which can signal
	// that the provider's model changed. It is informational and never changes
	// the status.
	ReasonModelCheckSkipped ReasonCode = "model_check_skipped"
)

Reason codes.

type Recommendation ΒΆ

type Recommendation struct {
	QuestionID string               `json:"question_id"`
	Status     RecommendationStatus `json:"status"`
	Selected   *Selected            `json:"selected"`
	Reasons    []ReasonCode         `json:"reasons"`
	Checks     []CheckResult        `json:"checks"`
	Summary    string               `json:"summary"`
}

Recommendation is the policy outcome for one question. Selected is nil unless Status is Accepted. Summary is deterministic text built from observed numbers and checks; it is for people and is not a stable contract (the reason codes are).

type RecommendationStatus ΒΆ

type RecommendationStatus string

RecommendationStatus is the outcome of applying a policy to one question.

const (
	// Accepted means every applicable check passed (and, for dual runs, the
	// providers agreed or the preferred provider won the conflict).
	Accepted RecommendationStatus = "accepted"

	// BelowThreshold means at least one applicable check failed.
	BelowThreshold RecommendationStatus = "below_threshold"

	// ReviewRequired means the two providers disagreed and no conflict preference
	// resolved it.
	ReviewRequired RecommendationStatus = "review_required"

	// NotApplicable means the policy could not be applied, for example because no
	// check matched a provider or model.
	NotApplicable RecommendationStatus = "not_applicable"

	// Incomplete means a required provider failed or a required metric was absent.
	Incomplete RecommendationStatus = "incomplete"

	// NoRule means no policy covered the question.
	NoRule RecommendationStatus = "no_rule"
)

Recommendation statuses. Accepted through Incomplete are listed from least to most severe, the order in which Overall picks the most severe covered status. NoRule, for a question no policy covers, is outside that order.

type Redaction ΒΆ

type Redaction struct {
	CorrelationID bool
	EvidenceRefs  bool
	RequestIDs    bool
	Usage         bool
}

Redaction selects the fields Redact removes.

type Request ΒΆ

type Request struct {
	// Providers selects the providers. It is required.
	Providers ProviderSet

	// State is the content to evaluate: a string, object or array. It must not
	// be blank: text of only white space, and an empty object or array, are
	// rejected before any call.
	State Value

	// Questions are evaluated independently against State.
	Questions []Question

	// CorrelationID is an optional caller label stored verbatim in the result.
	CorrelationID string

	// EvidenceRefs are optional opaque references stored verbatim in the result.
	// They are never fetched or validated beyond their size.
	EvidenceRefs []string
}

Request is one evaluation: a state and the questions asked about it.

Evaluate copies the request on entry; callers must still not mutate it from another goroutine while a call is in progress.

type Result ΒΆ

type Result struct {
	SchemaVersion string   `json:"schema_version"`
	Library       Library  `json:"library"`
	CorrelationID string   `json:"correlation_id"`
	EvidenceRefs  []string `json:"evidence_refs"`

	// Redacted lists the fields that Redact removed.
	Redacted []string `json:"redacted"`

	Providers ProviderSet  `json:"providers"`
	Status    ResultStatus `json:"status"`

	// StartedAt is the client wall-clock time, in UTC, when the calls started.
	StartedAt time.Time `json:"started_at"`

	// Elapsed is the client-observed time for all calls, not provider compute time.
	Elapsed time.Duration `json:"elapsed_ns"`

	Questions       []QuestionInfo   `json:"questions"`
	Outcomes        []Outcome        `json:"outcomes"`
	Policy          PolicySnapshot   `json:"policy"`
	Recommendations []Recommendation `json:"recommendations"`
	Overall         Overall          `json:"overall"`
}

Result is the typed, storable record of one evaluation: every provider outcome, the effective policy, and the recommendations derived from them. Marshal it with encoding/json and restore it with ParseResult.

A Result holds caller identifiers (question, option and level IDs, the correlation ID and evidence references) and provider outputs, but never the state, instructions, descriptions, credentials or provider bodies. It is not proof that an answer is true, not tamper-evident, and not a guarantee that a repeated evaluation would return the same values.

func ParseResult ΒΆ

func ParseResult(data []byte) (*Result, error)

ParseResult restores a Result encoded with encoding/json. It returns an error matching ErrInvalidRecord for another schema version or a record whose parts are not consistent with each other or with the rules a live evaluation follows:

  • outcomes that do not match the providers or the status, or answers that do not match the questions (every observation must pass the checks a live one does, with answers and probabilities in request order, as Evaluate stores them);
  • a correlation ID or evidence references that a request could not carry, a reported model, request ID or ray ID that is not a provider identifier (empty, or 1 to 256 bytes of visible ASCII), or a negative elapsed time;
  • recommendations that do not match the policy snapshot: without a policy (source none or disabled) every question is NoRule with exactly the no_policy reason; with one, a question is NoRule with exactly the question_not_in_policy reason when the policy has no entry for it, and is not NoRule and carries neither reason when the policy has one;
  • an Overall that is not the aggregation of the recommendations (the most severe covered status, and the NoRule questions in request order), or a selection that points at a provider or option the record does not support.

The checks are consistency checks, not a replay: recommendations are not recomputed, so records written by other v1 versions of this library still parse. Statuses and reason codes it does not know are kept, except where a rule above needs a specific value. Unknown fields are ignored.

Example ΒΆ
package main

import (
	"context"
	"encoding/json"
	"fmt"

	"github.com/mrz1836/go-discern"
	"github.com/mrz1836/go-discern/discerntest"
)

// exampleRequest is a synthetic support ticket with one routing question.
func exampleRequest(providers discern.ProviderSet) discern.Request {
	return discern.Request{
		Providers: providers,
		State:     discern.Text("Subject: Charged twice\n\nMy invoice was paid twice this month."),
		Questions: []discern.Question{
			discern.Choice("queue", discern.Text("Which support queue should handle this ticket?"),
				discern.Opt("billing", discern.Text("Invoices, refunds, failed payments")),
				discern.Opt("technical", discern.Text("Errors, outages, integration problems")),
				discern.Opt("account", discern.Text("Login, profile, permissions"))),
		},
		CorrelationID: "ticket-1042",
	}
}

// exampleObservation is what a fake provider answers for exampleRequest.
func exampleObservation(model, option string, billing, technical, account float64) discern.Observation {
	return discern.Observation{Model: model, Answers: []discern.Answer{{
		QuestionID: "queue", Kind: discern.KindChoice,
		Choice: &discern.ChoiceAnswer{Option: option, Probabilities: []discern.OptionProbability{
			{Option: "billing", Probability: billing},
			{Option: "technical", Probability: technical},
			{Option: "account", Probability: account},
		}},
	}}}
}

func main() {
	fake := discerntest.Static(discern.Jev, exampleObservation("jev-1.13.0", "billing", 0.91, 0.06, 0.03))
	ev, err := discern.New(discern.WithAdapter(fake))
	if err != nil {
		fmt.Println(err)
		return
	}
	res, err := ev.Evaluate(context.Background(), exampleRequest(discern.JevOnly))
	if err != nil {
		fmt.Println(err)
		return
	}

	data, err := json.Marshal(res.Redact(discern.Redaction{CorrelationID: true}))
	if err != nil {
		fmt.Println(err)
		return
	}
	restored, err := discern.ParseResult(data)
	if err != nil {
		fmt.Println(err)
		return
	}
	obs, _ := restored.Observation(discern.Jev)
	q, _ := obs.Choice("queue")
	fmt.Println(restored.SchemaVersion, restored.Redacted, q.Option)
}
Output:
discern.result/v1 [correlation_id] billing

func Recommend ΒΆ

func Recommend(res *Result, policy Policy) (*Result, error)

Recommend applies a policy to an existing result without any provider call. It returns a new Result whose policy source is PolicyRecommend; res is not modified. It returns ErrInvalidPolicy for an invalid policy and ErrInvalidRecord when the parts of res it keeps are inconsistent. The parts it replaces (the policy snapshot, recommendations and overall verdict) are not checked, so a result built by hand may leave them zero.

Two-sided gates, such as escalating at noul >= 0.9 and closing at noul <= 0.1, are two Recommend calls with two policies.

Example ΒΆ
package main

import (
	"context"
	"fmt"

	"github.com/mrz1836/go-discern"
	"github.com/mrz1836/go-discern/discerntest"
)

// exampleRequest is a synthetic support ticket with one routing question.
func exampleRequest(providers discern.ProviderSet) discern.Request {
	return discern.Request{
		Providers: providers,
		State:     discern.Text("Subject: Charged twice\n\nMy invoice was paid twice this month."),
		Questions: []discern.Question{
			discern.Choice("queue", discern.Text("Which support queue should handle this ticket?"),
				discern.Opt("billing", discern.Text("Invoices, refunds, failed payments")),
				discern.Opt("technical", discern.Text("Errors, outages, integration problems")),
				discern.Opt("account", discern.Text("Login, profile, permissions"))),
		},
		CorrelationID: "ticket-1042",
	}
}

// exampleObservation is what a fake provider answers for exampleRequest.
func exampleObservation(model, option string, billing, technical, account float64) discern.Observation {
	return discern.Observation{Model: model, Answers: []discern.Answer{{
		QuestionID: "queue", Kind: discern.KindChoice,
		Choice: &discern.ChoiceAnswer{Option: option, Probabilities: []discern.OptionProbability{
			{Option: "billing", Probability: billing},
			{Option: "technical", Probability: technical},
			{Option: "account", Probability: account},
		}},
	}}}
}

func main() {
	fake := discerntest.Static(discern.Jev, exampleObservation("jev-1.13.0", "technical", 0.3, 0.62, 0.08))
	ev, err := discern.New(discern.WithAdapter(fake))
	if err != nil {
		fmt.Println(err)
		return
	}
	res, err := ev.Evaluate(context.Background(), exampleRequest(discern.JevOnly))
	if err != nil {
		fmt.Println(err)
		return
	}

	// Replay two candidate thresholds locally: no further provider calls.
	for _, threshold := range []float64{0.6, 0.7} {
		pol := discern.Policy{Version: fmt.Sprintf("trial-%.1f", threshold), Questions: []discern.QuestionPolicy{{
			Question: "queue",
			Checks:   []discern.Check{{Metric: discern.MetricChoiceProbability, Op: discern.AtLeast, Threshold: threshold}},
		}}}
		again, err := discern.Recommend(res, pol)
		if err != nil {
			fmt.Println(err)
			return
		}
		rec, _ := again.Recommendation("queue")
		fmt.Println(pol.Version, rec.Status)
	}
	fmt.Println(fake.Calls(), "call")
}
Output:
trial-0.6 accepted
trial-0.7 below_threshold
1 call

func (*Result) Clone ΒΆ

func (r *Result) Clone() *Result

Clone returns a deep copy of the result.

func (*Result) Observation ΒΆ

func (r *Result) Observation(p Provider) (Observation, bool)

Observation returns a copy of the provider's observation, or false if the provider was not selected or failed.

func (*Result) Outcome ΒΆ

func (r *Result) Outcome(p Provider) (Outcome, bool)

Outcome returns a copy of the provider's outcome, or false if the provider was not selected.

func (*Result) Recommendation ΒΆ

func (r *Result) Recommendation(questionID string) (Recommendation, bool)

Recommendation returns a copy of the question's recommendation.

func (*Result) Redact ΒΆ

func (r *Result) Redact(opts Redaction) *Result

Redact returns a deep copy with the selected fields removed and listed in Redacted. Question, option and level IDs cannot be redacted; keep them free of sensitive data.

func (*Result) SelectedAnswers ΒΆ

func (r *Result) SelectedAnswers(questionID string) []Answer

SelectedAnswers returns copies of the answers an accepted recommendation selected, in fixed provider order. It returns nil unless the question's status is Accepted.

type ResultStatus ΒΆ

type ResultStatus string

ResultStatus summarizes the provider outcomes of one evaluation.

const (
	// Complete means every selected provider succeeded.
	Complete ResultStatus = "complete"

	// Partial means one of two selected providers failed.
	Partial ResultStatus = "partial"

	// Failed means every selected provider failed.
	Failed ResultStatus = "failed"
)

Result statuses.

type ScoreAnswer ΒΆ

type ScoreAnswer struct {
	Value         float64            `json:"value"`
	Confidence    *float64           `json:"confidence,omitempty"`
	Probabilities []LevelProbability `json:"probabilities"`
}

ScoreAnswer is a Score result. Value is the probability-weighted level position in [0, levels-1] and may be fractional. Probabilities are in level order.

func (ScoreAnswer) Nearest ΒΆ

func (s ScoreAnswer) Nearest() string

Nearest returns the ID of the level nearest to Value; halves round up. It returns "" if the answer has no levels.

Example ΒΆ
package main

import (
	"fmt"

	"github.com/mrz1836/go-discern"
)

func main() {
	s := discern.ScoreAnswer{Value: 1.05, Probabilities: []discern.LevelProbability{
		{Level: "cosmetic", Probability: 0.0},
		{Level: "degraded", Probability: 0.95},
		{Level: "blocked", Probability: 0.05},
	}}
	fmt.Println(s.Nearest())
}
Output:
degraded

type ScoreLevel ΒΆ

type ScoreLevel struct {
	ID          string
	Description Value
}

ScoreLevel is one Score level. ID is local: the provider sees only the level's position. Description is required.

func Level ΒΆ

func Level(id string, description Value) ScoreLevel

Level returns a Score level.

type Selected ΒΆ

type Selected struct {
	Providers []Provider `json:"providers"`
	Option    string     `json:"option,omitempty"`
}

Selected points at the accepted answers: the providers whose answers were selected, in fixed order, and for Choice the selected option. Numeric questions carry no blended value; use Result.SelectedAnswers for the typed answers.

type Usage ΒΆ

type Usage struct {
	InputTokens  *int64 `json:"input_tokens,omitempty"`
	OutputTokens *int64 `json:"output_tokens,omitempty"`

	// Neurons is Cloudflare's cf-ai-neurons billing figure, when supplied.
	Neurons *float64 `json:"neurons,omitempty"`
}

Usage is provider-reported metering. Each field is nil when not reported; token counts from different providers are not one bill.

type ValidationError ΒΆ

type ValidationError struct {
	Err    error
	Field  string
	Reason string
}

ValidationError describes a local validation failure. Err is the matching sentinel (ErrInvalidConfig, ErrInvalidRequest, ErrInvalidPolicy, ErrUnsupported or ErrInvalidRecord). Field is a path such as "questions[1].options[0].id"; it uses indexes, not caller values. Reason never contains caller data.

func (ValidationError) Error ΒΆ

func (e ValidationError) Error() string

Error returns the sentinel message followed by the field and reason.

func (ValidationError) Unwrap ΒΆ

func (e ValidationError) Unwrap() error

Unwrap returns the sentinel.

type Value ΒΆ

type Value struct {
	// contains filtered or unexported fields
}

Value is an immutable JSON value used for state, instructions and descriptions. Build one with Text, JSON or Marshal. The zero Value encodes as null, which is accepted only as a Choice option description.

A Value is checked once, when it is built: its validity, top-level type and whether it is blank are cached, so validating and encoding a large state never rescans or copies it more than needed.

func JSON ΒΆ

func JSON(b []byte) Value

JSON returns a Value holding raw JSON bytes, which are copied and later sent verbatim: they are never re-encoded, so large integers and key order survive. The bytes must be valid JSON and valid UTF-8, and no object may repeat a key; that is checked when the request is validated, before any call.

func Marshal ΒΆ

func Marshal(v any) (Value, error)

Marshal encodes v once with encoding/json, without HTML escaping, and returns the result as a Value. The result follows the same rules as JSON; for example, a json.RawMessage field with duplicate keys makes the Value invalid.

func Text ΒΆ

func Text(s string) Value

Text returns a Value holding a JSON string. The string must be valid UTF-8; that is checked when the request is validated, before any call.

func (Value) AppendJSON ΒΆ

func (v Value) AppendJSON(dst []byte) ([]byte, error)

AppendJSON appends the encoded value to dst, like MarshalJSON, without an intermediate copy. It returns an error matching ErrInvalidRequest, and dst unchanged, for invalid JSON or UTF-8, or JSON with duplicate object keys.

func (Value) IsZero ΒΆ

func (v Value) IsZero() bool

IsZero reports whether v is the zero Value.

func (Value) MarshalJSON ΒΆ

func (v Value) MarshalJSON() ([]byte, error)

MarshalJSON returns the encoded value: a JSON string for Text, the raw bytes for JSON and Marshal, and null for the zero Value. HTML characters are not escaped. It returns an error matching ErrInvalidRequest for invalid JSON or UTF-8, or JSON with duplicate object keys.

Directories ΒΆ

Path Synopsis
Package clef is the discern adapter for Cloudflare Workers AI Clef, a hosted System One judgment model.
Package clef is the discern adapter for Cloudflare Workers AI Clef, a hosted System One judgment model.
Package discerntest provides fake adapters for testing code that uses discern, without network access or provider credentials.
Package discerntest provides fake adapters for testing code that uses discern, without network access or provider credentials.
Package main routes synthetic support tickets with go-discern.
Package main routes synthetic support tickets with go-discern.
internal
adapter
Package adapter is the plumbing the Jev and Clef adapters share: their common options and the validation of them, the function that holds each Client's connection settings where no formatting can reach them, the one attempt each call makes, and the errors built along the way.
Package adapter is the plumbing the Jev and Clef adapters share: their common options and the validation of them, the function that holds each Client's connection settings where no formatting can reach them, the one attempt each call makes, and the errors built along the way.
httpx
Package httpx is the single HTTP transport contract shared by the Jev and Clef adapters: one bounded POST attempt per call, no redirects, no retries, and errors that never carry URLs, headers, credentials or bodies.
Package httpx is the single HTTP transport contract shared by the Jev and Clef adapters: one bounded POST attempt per call, no redirects, no retries, and errors that never carry URLs, headers, credentials or bodies.
ident
Package ident holds the byte-level identifier rules that discern and its adapters share, so that each rule is written once: visible ASCII for what a provider reports or a header carries, and a short name alphabet for question IDs and model names.
Package ident holds the byte-level identifier rules that discern and its adapters share, so that each rule is written once: visible ASCII for what a provider reports or a header carries, and a short name alphabet for question IDs and model names.
jsonx
Package jsonx holds JSON primitives that encoding/json does not offer: a duplicate-key check and a string unquoter, both decoding escapes exactly as encoding/json does and allocating as little as the input allows, and a scan for the end of a string literal.
Package jsonx holds JSON primitives that encoding/json does not offer: a duplicate-key check and a string unquoter, both decoding escapes exactly as encoding/json does and allocating as little as the input allows, and a scan for the end of a string literal.
livetest
Package livetest holds go-discern's opt-in live contract suite: tests that call the real Jev and Clef APIs and assert the provider behavior the adapters rely on, so a provider change shows up as a failing test whose message starts with "provider behavior changed?".
Package livetest holds go-discern's opt-in live contract suite: tests that call the real Jev and Clef APIs and assert the provider behavior the adapters rely on, so a provider change shows up as a failing test whose message starts with "provider behavior changed?".
systemone
Package systemone encodes System One requests and strictly decodes System One responses for the Jev and Clef adapters.
Package systemone encodes System One requests and strictly decodes System One responses for the Jev and Clef adapters.
testutil
Package testutil holds the test plumbing that go-discern's packages share: race-aware allocation guards, in-memory and recording HTTP fakes, fmt leak assertions, error extraction and JSON helpers.
Package testutil holds the test plumbing that go-discern's packages share: race-aware allocation guards, in-memory and recording HTTP fakes, fmt leak assertions, error extraction and JSON helpers.
Package jev is the discern adapter for TypeSafe's hosted Jev System One API (POST https://api.typesafe.ai/v1/systemone, authenticated with a bearer API key).
Package jev is the discern adapter for TypeSafe's hosted Jev System One API (POST https://api.typesafe.ai/v1/systemone, authenticated with a bearer API key).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL