decide

package module
v0.4.0 Latest Latest
Warning

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

Go to latest
Published: Oct 8, 2026 License: Apache-2.0 Imports: 26 Imported by: 0

README

Decide

Reference Tests Release Last commit

A Go library and CLI for decision models: ask typed questions, get answers with probabilities.

  • Go library: go get github.com/deepnoodle-ai/decide, then ask typed questions from your program. Jump to the Go guide.
  • CLI: brew install deepnoodle-ai/tap/decide, then ask questions about files, folders, JSON records, diffs, and text from your shell or CI. Jump to the CLI.

Docs: decide.deepnoodle.ai, with a quickstart, tutorials, and the reference.

brew install deepnoodle-ai/tap/decide

Or go install github.com/deepnoodle-ai/decide/cmd/decide@latest, or download a binary from the releases.

A decision model answers typed questions instead of generating text. Decide asks yes-or-no (noul), multiple-choice (choice), and scale (score) questions, and each answer comes back typed, with a probability, so a script or a program can act on it directly. There is no prose to parse.

Decide works with Jev on the TypeSafe API, Clef on Cloudflare Workers AI, GPT-6 Luna on OpenAI's Decisions API, and any other service that speaks the Jev API, with the same commands and the same Go code. Switch between them without changing anything else:

Jev by TypeSafe Clef by Cloudflare GPT-6 Luna by OpenAI
Models jev-latest clef, and clef-flash for lower latency gpt-6-luna
Runs on the TypeSafe API Cloudflare Workers AI the Decisions API, in beta

Clef's weights are open on Hugging Face under Apache 2.0.

Try it

With Jev, the default, set your TypeSafe API key:

export TYPESAFE_API_KEY=...
echo "The new release fixed everything I cared about" | decide run sentiment

With Clef, set a Workers AI API token and your account ID, and choose the Cloudflare provider:

export CLOUDFLARE_AUTH_TOKEN=...
export CLOUDFLARE_ACCOUNT_ID=...
export DECIDE_PROVIDER=cloudflare   # or pass --provider cloudflare
echo "The new release fixed everything I cared about" | decide run sentiment

With GPT-6 Luna, set your OpenAI API key and choose the OpenAI provider:

export OPENAI_API_KEY=...
export DECIDE_PROVIDER=openai   # or pass --provider openai
echo "The new release fixed everything I cared about" | decide run sentiment

With another Jev-compatible service, such as one you host yourself, set its address and a model name:

export TYPESAFE_API_KEY=...
export TYPESAFE_BASE_URL=https://decisions.example.com
echo "The new release fixed everything I cared about" | decide run sentiment --model my-model

You get a typed answer with its probability. With Jev, the output looks like this; the first line names whichever provider and model you chose:

Running sentiment on 1 line · typesafe jev-latest

stdin:1  The new release fixed everything I cared about
  sentiment  positive  94%

✓ 1 answered  nothing flagged  1.2s
See these results again with: decide runs view 20261002-153012-a1b2

Building from source requires Go 1.27 or later. Run decide on its own for a tour.

What you can ask

Decide comes with eleven templates. A template is a named set of questions.

Template Asks about each item
sentiment Is it positive, negative, or neutral?
triage Is this support request urgent, and how severe is its impact?
ticket-routing Does this support ticket belong to billing, engineering, or other?
relevance Is it relevant to a question you choose?
code-risk Could it cause security or data problems, and how maintainable is it?
security Can untrusted input reach a SQL query, a command, a fetched URL, or a page, and is any cryptography weak?
prompt-injection Does it try to take over an AI agent that reads it?
task-readiness Is this issue ready to hand to a coding agent, and how large is it?
pr-description Do this pull request's title and description meet your guidelines?
command-risk Could this shell command destroy work, expose secrets, deploy or publish, or cause severe harm?
receipt-quality Does this image show a readable receipt? Runs on Clef.
decide run code-risk src --include '*.go'
decide run security src                      # injection, SSRF, XSS, weak crypto
decide run relevance docs --each section -p question="pricing"
decide runs view --format csv > results.csv
decide runs view --top 20                    # the 20 items nearest their flags, flagged first
decide run code-risk src --fail-on flagged   # exit code 2 if anything is flagged
git diff main | decide run code-risk --each function   # judge each changed function
git diff main | decide run prompt-injection            # hidden instructions for AI agents
gh issue list --json number,title,body | decide run task-readiness
gh pr view 42 --json number,title,body | decide run pr-description --fail-on flagged
echo 'git reset --hard HEAD~3' | decide run command-risk    # before an agent runs it

Decide reads JSONL, JSON, CSV, text, Markdown, source code, diffs, and images, flags answers that need attention, and resumes stopped runs. It keeps every answer, by the model name you ask for, so a second run asks only about what changed, and asks again when a live answer shows the model behind the name changed. It prints text, JSON, CSV, a Markdown report, or GitHub Actions annotations on a pull request. To try the templates on planted problems, see demo. You can write your own template in a few lines of JSON with decide templates new. The reference covers it all. The tutorials and recipes show it in GitHub Actions, other CI systems, and git hooks.

Use it from Go

go get github.com/deepnoodle-ai/decide

Create a client for Jev:

client, err := decide.NewClient() // reads TYPESAFE_API_KEY and TYPESAFE_BASE_URL

For another Jev-compatible service, pass decide.WithBaseURL and decide.WithModel. Or create a client for Clef:

client, err := backend.NewClient(backend.Config{
	Provider:  backend.Cloudflare,
	APIKey:    os.Getenv("CLOUDFLARE_AUTH_TOKEN"),
	AccountID: os.Getenv("CLOUDFLARE_ACCOUNT_ID"),
	Model:     "clef", // or "clef-flash"
})

Or for GPT-6 Luna, with Provider: backend.OpenAI and APIKey: os.Getenv("OPENAI_API_KEY"). Then ask a question the same way with any of them:

e, err := decide.Eval(ctx, client, ticket, decide.Noul("Is this about billing?"))
if err != nil {
	return err
}
fmt.Println(e.Answer.Noul) // the probability of yes, such as 0.97

To ask a built-in template's questions, use the templates package. You get its tuned wording and flags, and they change when you upgrade decide:

req := decide.NewRequest(command)
severe := decide.Ask(req, "severe", templates.CommandRisk.Noul("severe"))
resp, err := client.SystemOne(ctx, req)
if err != nil {
	return err
}
a, err := severe.From(resp)
if err != nil || templates.CommandRisk.Flagged("severe", a) { // yes >= 80%, as in decide run
	// stop the command
}

Every answer is validated against its question, and transient failures are retried. The package documentation covers asking several questions at once, Pick, and test fakes in decidetest. Packages under patterns/, such as gate, rank, and funnel, build common decisions from answers. The examples show each one in a short program.

Use it from Claude Code

The decide plugin gives Claude a second opinion. It checks content from outside before Claude acts on it and, in bypass mode, each shell command before it runs, and it gives Claude a judge tool for its own typed questions. In Claude Code:

/plugin marketplace add deepnoodle-ai/decide
/plugin install decide@decide

The Claude Code guide shows how to set it up and use /decide:audit and /decide:hunt. See plugin/README.md for what each check does and what it sends.

Status

Decide is young. Before v1, the library API and the CLI may change in any release.

Contributing

Issues and pull requests are welcome. Please read CONTRIBUTING.md first, and report security issues as described in SECURITY.md.

License

Apache 2.0

Documentation

Overview

Package decide is a Go client for decision models: TypeSafe's Jev, Cloudflare's Clef, and OpenAI's GPT-6 Luna. Client.SystemOne takes its name from TypeSafe's term for this kind of model, System One.

A decision model evaluates a piece of state (a ticket, a diff, a JSON record) against a set of typed questions and returns typed answers with probabilities that code can act on directly. It does not generate text. NewClient connects to TypeSafe; the backend package connects to any provider through the same API. The templates package holds the decide command's built-in questions.

Eval asks one typed question and returns an Evaluation. Pick selects an original candidate or abstains, returning a Decision. Both retain response evidence and apply no application thresholds.

Three primitives cover most questions:

  • Noul: does a condition hold? The answer is P(yes) in [0,1].
  • Choice: which one of these options? The answer is the chosen option, the full distribution, and a confidence.
  • Score: where on this ordered scale? The answer is the probability-weighted level, the distribution, and a confidence.

Build a Request, add questions with Ask to get typed handles, send it with Client.SystemOne, and read answers back through the handles:

client, err := decide.NewClient() // reads TYPESAFE_API_KEY
if err != nil {
	return err
}
req := decide.NewRequest(ticketText)
billing := decide.Ask(req, "billing", decide.Noul("Is this ticket about billing?"))
tone := decide.Ask(req, "tone", decide.Choice("What is the customer's tone?",
	decide.Option("calm"), decide.Option("frustrated"), decide.Option("angry")))

resp, err := client.SystemOne(ctx, req)
if err != nil {
	return err
}
b, _ := billing.From(resp) // *decide.NoulAnswer
t, _ := tone.From(resp)    // *decide.ChoiceAnswer
fmt.Println(resp.Model, b.Noul, t.Choice, t.Confidence)

Every answer is checked against its question before it is returned: the keys match, required fields are present, a choice is one of the offered options, probabilities are finite and in range. On a failure SystemOne returns the response together with an *InvalidAnswersError, and Response.Invalid names the keys that failed; the other keys stay usable. Failures where the numbers merely disagree beyond tolerance (the choice is not the argmax, probabilities do not sum to about 1) also match ErrInconsistentAnswer, so a caller can choose to accept them.

Transient failures (408, 429, 5xx, and connection errors) are retried with backoff that honors Retry-After. Errors from the API are *APIError values that match sentinels such as ErrRateLimited and ErrAuth with errors.Is and carry the request ID.

Question and answer types are open. A question type from another package decodes into its own answer type when it implements AnswerMaker; answer types can also be registered with RegisterAnswerType; and an answer type nobody knows decodes to a *RawAnswer instead of failing the response. RawQuestion sends a question type this package does not model.

The decidetest package provides a fake server for tests and examples.

Example

Ask a typed question and read its validated answer.

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/deepnoodle-ai/decide"
	"github.com/deepnoodle-ai/decide/decidetest"
)

// startFake starts a fake API with canned answers, standing in for
// https://api.typesafe.ai so the examples run without a key.
func startFake() *decidetest.Server {
	srv, err := decidetest.Start()
	if err != nil {
		log.Fatal(err)
	}
	srv.Answer("billing", decidetest.NoulAnswer(0.93))
	return srv
}

func main() {
	srv := startFake()
	defer srv.Close()
	client, err := srv.NewClient() // in production: decide.NewClient()
	if err != nil {
		log.Fatal(err)
	}

	req := decide.NewRequest("I was charged twice this month and nobody answers my emails.")
	billing := decide.Ask(req, "billing", decide.Noul("Is this ticket about billing?"))

	resp, err := client.SystemOne(context.Background(), req)
	if err != nil {
		log.Fatal(err)
	}
	answer, err := billing.From(resp)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("billing: %.2f\n", answer.Noul)
}
Output:
billing: 0.93

Index

Examples

Constants

View Source
const (
	ReasonMissingField     = "missing_field"
	ReasonMissingAnswer    = "missing_answer"
	ReasonUnexpectedAnswer = "unexpected_answer"
	ReasonDecodeFailed     = "decode_failed"
	ReasonTypeMismatch     = "type_mismatch"
	ReasonNotFinite        = "not_finite"
	ReasonOutOfRange       = "out_of_range"
	ReasonProbabilityKeys  = "probability_keys"
	ReasonChoiceNotOption  = "choice_not_option"
	ReasonChoiceNotArgmax  = "choice_not_argmax"
	ReasonLegendKeys       = "legend_keys"
	ReasonProbabilitySum   = "probability_sum"
	ReasonCustom           = "custom"
)

Answer validation reasons, used in AnswerError.Reason.

View Source
const Version = "0.0.0"

Version is the version of this SDK. It is sent in the User-Agent and X-TypeSafe-SDK headers.

Variables

View Source
var (
	ErrAuth           = errors.New("decide: authentication failed")     // 401, 403
	ErrValidation     = errors.New("decide: request validation failed") // 422
	ErrRateLimited    = errors.New("decide: rate limited")              // 429
	ErrOverloaded     = errors.New("decide: overloaded")                // 529
	ErrServer         = errors.New("decide: server error")              // any 5xx, including 529
	ErrNoAPIKey       = errors.New("decide: no API key: set TYPESAFE_API_KEY or use WithAPIKey")
	ErrInvalidRequest = errors.New("decide: invalid request")
	ErrInvalidAnswer  = errors.New("decide: invalid answer")
	// ErrInconsistentAnswer marks an answer that is well formed but whose
	// numbers disagree with each other beyond tolerance: the choice is not
	// the argmax, the score is outside its range, or the probabilities do not
	// sum to about 1. It is always accompanied by ErrInvalidAnswer.
	ErrInconsistentAnswer = errors.New("decide: inconsistent answer")
	ErrDecode             = errors.New("decide: cannot decode response")
)

Sentinel errors. Match them with errors.Is.

Functions

func AnswerAs

func AnswerAs[A Answer](resp *Response, key string) (A, error)

AnswerAs is the non-handle form of Handle.From, with the same rules: a zero A only for missing_answer and type_mismatch; for a key listed in resp.Invalid it returns the answer and that error together, so a caller who accepts ErrInconsistentAnswer can still use the answer.

func Attempt

func Attempt(ctx context.Context) int

Attempt returns the zero-based attempt number the client is on, for transports that want to send a retry-count header. 0 outside the client.

func RegisterAnswerType

func RegisterAnswerType(typ string, newAnswer func() Answer) error

RegisterAnswerType registers a constructor for an answer type, used when a response is decoded without its request (or the request's question does not implement AnswerMaker). It returns an error if typ is empty, newAnswer is nil, or typ is already registered, including the built-in types.

The constructor must return a pointer that encoding/json can unmarshal into.

Types

type APIError

type APIError struct {
	StatusCode int
	Type       string             // detail.error_type, or Details[0].Type, or ""
	Message    string             // message extracted from the body
	RequestID  string             // x-typesafe-request-id
	RetryAfter time.Duration      // parsed server delay; 0 if absent
	Body       []byte             // raw body, key redacted
	Details    []ValidationDetail // 422 only
}

APIError is a non-2xx response from the API.

Example

Branch on the kind of failure with errors.Is; every API error carries the request ID.

package main

import (
	"context"
	"errors"
	"fmt"
	"log"

	"github.com/deepnoodle-ai/decide"
	"github.com/deepnoodle-ai/decide/decidetest"
)

// startFake starts a fake API with canned answers, standing in for
// https://api.typesafe.ai so the examples run without a key.
func startFake() *decidetest.Server {
	srv, err := decidetest.Start()
	if err != nil {
		log.Fatal(err)
	}
	srv.Answer("billing", decidetest.NoulAnswer(0.93))
	return srv
}

func main() {
	srv := startFake()
	defer srv.Close()
	srv.Overloaded(3) // more than the default two retries
	client, err := srv.NewClient()
	if err != nil {
		log.Fatal(err)
	}
	req := decide.NewRequest("state")
	decide.Ask(req, "billing", decide.Noul("About billing?"))

	_, err = client.SystemOne(context.Background(), req)
	if ae, ok := errors.AsType[*decide.APIError](err); ok {
		fmt.Println(ae.StatusCode, errors.Is(err, decide.ErrOverloaded), errors.Is(err, decide.ErrServer), ae.RequestID)
	}
}
Output:
529 true true req_3

func (*APIError) Error

func (e *APIError) Error() string

Error returns "decide: <status> <message> (request_id=<id>)".

func (*APIError) Unwrap

func (e *APIError) Unwrap() []error

Unwrap returns the sentinels for StatusCode: ErrAuth for 401 and 403, ErrValidation for 422, ErrRateLimited for 429, ErrOverloaded and ErrServer for 529, ErrServer for other 5xx, and none for 408 and other 4xx.

Discrepancy: /api says 401 for a missing or invalid key, but a live request without a key returned 403. Both map to ErrAuth. The OpenAPI spec lists only 422 as an error response; the docs list 401, 422, 429, and 529. We handle all of them.

type Answer

type Answer interface {
	AnswerType() string
}

Answer is one entry in Response.Answers. Implementations outside this package are allowed.

func DecodeAnswer

func DecodeAnswer(data []byte) (Answer, error)

DecodeAnswer reads "type", constructs the registered answer, and unmarshals into it. An unregistered type returns a *RawAnswer and nil error. A registered type that fails to unmarshal returns a *RawAnswer with Err set, and the same error. Input that is not an object with a string "type" returns a *RawAnswer with Type "" and Err set.

type AnswerError

type AnswerError struct {
	Key    string // question key
	Type   string // question type; for unexpected_answer, the answer's AnswerType()
	Reason string // one of the Reason constants
	Detail string // human-readable specifics, e.g. `choice "x" not in options`
	Err    error  // underlying error for decode_failed and custom
	// contains filtered or unexported fields
}

AnswerError reports one answer that failed validation or could not be read through a handle.

func (*AnswerError) Error

func (e *AnswerError) Error() string

Error returns `decide: answer "k" (choice): choice_not_option: ...`.

func (*AnswerError) Unwrap

func (e *AnswerError) Unwrap() []error

Unwrap returns ErrInvalidAnswer, ErrInconsistentAnswer for consistency failures (choice_not_argmax, probability_sum, and a score out of range), and Err if non-nil.

type AnswerMaker

type AnswerMaker interface {
	MakeAnswer() Answer
}

AnswerMaker is optionally implemented by a Question so responses decode into the question's own answer type without registering it. MakeAnswer returns a new zero answer; it is the non-generic twin of QuestionFor.NewAnswer. When a response is decoded with its request, an answer whose wire type equals the question's type is made by MakeAnswer, before the registry is consulted.

type AnswerValidator

type AnswerValidator interface {
	ValidateAnswer(a Answer) error
}

AnswerValidator is optionally implemented by a Question. The client calls it during validation after the generic checks (answer present, no unexpected answers, decodable, matching type).

type CallOption

type CallOption func(*callConfig)

CallOption overrides a client setting for one SystemOne call. Invalid values make SystemOne return an error wrapping ErrInvalidRequest.

func WithCallAttemptTimeout

func WithCallAttemptTimeout(d time.Duration) CallOption

WithCallAttemptTimeout sets the per-attempt timeout for one call. d >= 0; 0 disables it.

func WithCallMaxRetries

func WithCallMaxRetries(n int) CallOption

WithCallMaxRetries sets the retry count for one call. n >= 0.

func WithCallValidation

func WithCallValidation(on bool) CallOption

WithCallValidation turns answer validation on or off for one call.

type Candidate

type Candidate[T any] struct {
	Item        T
	Description any
}

Candidate pairs an original item with the JSON description sent to the model. Item is returned unchanged; only Description enters the question.

type ChoiceAnswer

type ChoiceAnswer struct {
	Choice        string
	Probabilities map[string]float64
	Confidence    float64
	Extra         map[string]json.RawMessage
	// contains filtered or unexported fields
}

ChoiceAnswer is the answer to a ChoiceQuestion.

func (*ChoiceAnswer) AnswerType

func (a *ChoiceAnswer) AnswerType() string

AnswerType returns "choice".

func (*ChoiceAnswer) Margin

func (a *ChoiceAnswer) Margin() float64

Margin returns top P minus runner-up P; with one option it returns top P. With no probabilities it returns 0.

func (*ChoiceAnswer) MarshalJSON

func (a *ChoiceAnswer) MarshalJSON() ([]byte, error)

MarshalJSON writes "type" first, then the fields, then Extra sorted by key. Probabilities are written with keys sorted, not in wire order (Go maps are unordered), so a round trip is equal semantically, not byte for byte.

func (*ChoiceAnswer) Ranked

func (a *ChoiceAnswer) Ranked() []Prob

Ranked returns probabilities sorted by P descending, ties by Key ascending.

Example

Rank a Choice answer's full distribution rather than only its top option.

package main

import (
	"fmt"

	"github.com/deepnoodle-ai/decide"
)

func main() {
	a := &decide.ChoiceAnswer{
		Choice:        "refund",
		Probabilities: map[string]float64{"refund": 0.55, "exchange": 0.35, "other": 0.10},
	}
	for _, p := range a.Ranked() {
		fmt.Printf("%-8s %.2f\n", p.Key, p.P)
	}
	fmt.Printf("margin   %.2f\n", a.Margin())
}
Output:
refund   0.55
exchange 0.35
other    0.10
margin   0.20

func (*ChoiceAnswer) UnmarshalJSON

func (a *ChoiceAnswer) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes a choice answer. All three fields are required.

type ChoiceOption

type ChoiceOption struct {
	Key         string
	Description any // nil encodes as null: "interpreted by its name alone"
}

ChoiceOption is one option of a ChoiceQuestion. The model sees both the key and the description.

func Option

func Option(key string, description ...any) ChoiceOption

Option builds a ChoiceOption. Zero descriptions means nil (null); more than one panics, because the variadic exists only to make the description optional.

func OptionsFromMap

func OptionsFromMap[V any](m map[string]V) []ChoiceOption

OptionsFromMap converts a map to options sorted by key, for callers who hold criteria as a map and want deterministic order.

type ChoiceQuestion

type ChoiceQuestion struct {
	Instructions any
	Criteria     []ChoiceOption // encoded as a JSON object in slice order
	Extra        map[string]any
}

ChoiceQuestion asks which one of a set of options applies.

func Choice

func Choice(instructions any, options ...ChoiceOption) *ChoiceQuestion

Choice builds a Choice question. Options are sent in the order given.

func (*ChoiceQuestion) MakeAnswer

func (q *ChoiceQuestion) MakeAnswer() Answer

MakeAnswer returns q.NewAnswer().

func (*ChoiceQuestion) MarshalJSON

func (q *ChoiceQuestion) MarshalJSON() ([]byte, error)

MarshalJSON encodes the question with options in slice order. It fails if an option key is empty or repeated.

func (*ChoiceQuestion) NewAnswer

func (q *ChoiceQuestion) NewAnswer() *ChoiceAnswer

NewAnswer returns a new *ChoiceAnswer.

func (*ChoiceQuestion) QuestionType

func (q *ChoiceQuestion) QuestionType() string

QuestionType returns "choice".

func (*ChoiceQuestion) UnmarshalJSON

func (q *ChoiceQuestion) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes a choice question, preserving option order.

func (*ChoiceQuestion) ValidateAnswer

func (q *ChoiceQuestion) ValidateAnswer(a Answer) error

ValidateAnswer checks a choice answer against the options. Structural: fields present, values finite and in [0,1], probability keys equal the option keys, the choice is an option. Consistency: the choice is the argmax (ties allowed) and probabilities sum to about 1.

type Client

type Client struct {
	Models *ModelsService // set by NewClient, never reassigned
	// contains filtered or unexported fields
}

Client calls the TypeSafe API. It is safe for concurrent use: every field is set by NewClient and only read afterwards, and retry jitter uses the math/rand/v2 top-level functions, which are safe for concurrent use.

func NewClient

func NewClient(opts ...ClientOption) (*Client, error)

NewClient returns a client. Each setting resolves from its option, then its environment variable (unless WithoutEnvironment), then the default:

API key        TYPESAFE_API_KEY        none: ErrNoAPIKey
Base URL       TYPESAFE_BASE_URL       https://api.typesafe.ai
Default model  TYPESAFE_DEFAULT_MODEL  jev-latest
Log level      TYPESAFE_LOG_LEVEL      silent

Environment values are trimmed; blank means unset. TYPESAFE_LOG_LEVEL accepts debug, info, warn, error, or off, and is ignored when WithLogger is given.

func (*Client) DefaultModel

func (c *Client) DefaultModel() string

DefaultModel returns the model used when a request's Model is empty.

func (*Client) GoString

func (c *Client) GoString() string

GoString describes the client for %#v. It never includes the API key.

func (*Client) LogValue

func (c *Client) LogValue() slog.Value

LogValue implements slog.LogValuer. It never includes the API key.

func (*Client) String

func (c *Client) String() string

String describes the client. It never includes the API key.

func (*Client) SystemOne

func (c *Client) SystemOne(ctx context.Context, req *Request, opts ...CallOption) (*Response, error)

SystemOne sends req to POST /v1/systemone and returns the response.

A nil or invalid request, an invalid CallOption, or a request that cannot be encoded returns an error wrapping ErrInvalidRequest without a network call. If req.Model is empty the client's default model is sent; req itself is never modified. Transient failures are retried.

When validation is on (the default) and an answer does not match its question, SystemOne returns a non-nil response AND an *InvalidAnswersError, and resp.Invalid names the bad keys. Callers who treat any error as fatal stay safe; callers who want partial results use resp.Invalid or the handles, which fail only for the affected keys.

If ctx ends, the error satisfies errors.Is(err, ctx.Err()) and, when an attempt had already failed, errors.As still reaches that attempt's *APIError.

Example (PartialResults)

A validation failure returns the response and an error. Keys that passed stay usable; resp.Invalid names the ones that failed.

package main

import (
	"context"
	"errors"
	"fmt"
	"log"

	"github.com/deepnoodle-ai/decide"
	"github.com/deepnoodle-ai/decide/decidetest"
)

// startFake starts a fake API with canned answers, standing in for
// https://api.typesafe.ai so the examples run without a key.
func startFake() *decidetest.Server {
	srv, err := decidetest.Start()
	if err != nil {
		log.Fatal(err)
	}
	srv.Answer("billing", decidetest.NoulAnswer(0.93))
	return srv
}

func main() {
	srv := startFake()
	defer srv.Close()
	// A deliberately inconsistent answer: "calm" is not the most likely option.
	srv.Answer("tone", &decide.ChoiceAnswer{
		Choice:        "calm",
		Probabilities: map[string]float64{"calm": 0.2, "angry": 0.8},
		Confidence:    0.6,
	})
	client, err := srv.NewClient()
	if err != nil {
		log.Fatal(err)
	}

	req := decide.NewRequest("You people are useless.")
	billing := decide.Ask(req, "billing", decide.Noul("Is this ticket about billing?"))
	tone := decide.Ask(req, "tone", decide.Choice("Tone?", decide.Option("calm"), decide.Option("angry")))

	resp, err := client.SystemOne(context.Background(), req)
	if resp == nil {
		log.Fatal(err)
	}
	fmt.Println("error:", err)
	for key, ae := range resp.Invalid {
		fmt.Println("invalid:", key, ae.Reason, errors.Is(ae, decide.ErrInconsistentAnswer))
	}
	if b, err := billing.From(resp); err == nil {
		fmt.Printf("billing still usable: %.2f\n", b.Noul)
	}
	t, err := tone.From(resp)
	fmt.Println("tone:", t.Choice, "err:", err != nil)
}
Output:
error: decide: 1 invalid answer: answer "tone" (choice): choice_not_argmax: choice "calm" has P=0.2, max is 0.8
invalid: tone choice_not_argmax true
billing still usable: 0.93
tone: calm err: true

type ClientOption

type ClientOption func(*clientConfig) error

ClientOption configures a Client. Options that receive invalid values make NewClient return an error.

func WithAPIKey

func WithAPIKey(key string) ClientOption

WithAPIKey sets the API key. It overrides TYPESAFE_API_KEY. An empty key makes NewClient return ErrNoAPIKey.

func WithAttemptTimeout

func WithAttemptTimeout(d time.Duration) ClientOption

WithAttemptTimeout sets the timeout for each attempt, which covers reading the body. Default 10s; 0 disables it. It bounds each attempt only; the overall bound for a call, including retries and sleeps, comes from ctx.

func WithBaseURL

func WithBaseURL(url string) ClientOption

WithBaseURL sets the API base URL. It overrides TYPESAFE_BASE_URL. It must be an absolute http or https URL; a path prefix is kept.

func WithHTTPClient

func WithHTTPClient(hc *http.Client) ClientOption

WithHTTPClient sets the *http.Client used by the native transport. Its Timeout, if any, applies in addition to WithAttemptTimeout.

func WithLogBodies

func WithLogBodies(on bool) ClientOption

WithLogBodies adds request and response bodies to Debug records. Default false. Headers are never logged.

func WithLogger

func WithLogger(l *slog.Logger) ClientOption

WithLogger sets the logger. It overrides TYPESAFE_LOG_LEVEL. The default discards everything.

func WithMaxRetries

func WithMaxRetries(n int) ClientOption

WithMaxRetries sets how many times a failed attempt is retried. n >= 0; default 2.

func WithModel

func WithModel(name string) ClientOption

WithModel sets the default model, used when Request.Model is empty. It overrides TYPESAFE_DEFAULT_MODEL.

func WithRetryBackoff

func WithRetryBackoff(initial, max time.Duration) ClientOption

WithRetryBackoff sets the initial and maximum backoff. Defaults 500ms, 5s.

func WithTransport

func WithTransport(t Transport) ClientOption

WithTransport replaces the native HTTP transport, for gateways and compatible servers. It cannot be combined with WithAPIKey, WithBaseURL, WithHTTPClient, or WithUserAgent, which configure the native transport. With it, TYPESAFE_API_KEY and TYPESAFE_BASE_URL are not read.

func WithUserAgent

func WithUserAgent(product string) ClientOption

WithUserAgent prepends a product token such as "myapp/1.2" to the User-Agent header.

func WithValidation

func WithValidation(on bool) ClientOption

WithValidation turns answer validation on or off. Default true.

func WithoutEnvironment

func WithoutEnvironment() ClientOption

WithoutEnvironment makes NewClient read no TYPESAFE_* environment variables, so only options and defaults apply.

type Decision

type Decision[T any] struct {
	Item     T
	Index    int
	Picked   bool
	Answer   *ChoiceAnswer
	Response *Response
}

Decision contains an original candidate or an explicit abstention. Index is -1 and Item is zero when Picked is false. Answer preserves the original Choice evidence; Response preserves the complete client response. Inspect the error before using a selection: consistency-only errors retain a selected item, while structural failures do not.

func Pick

func Pick[T any](ctx context.Context, client *Client, state any, instructions any, candidates []Candidate[T], opts ...RequestOption) (Decision[T], error)

Pick asks the model to choose one described candidate or abstain. It sends positional keys c1, c2, ... and the abstain key "none" under question "pick". At most 254 candidates are accepted. Empty candidates abstain without a network call, Answer, or Response. A canceled context still returns an error.

Pick always validates the complete response. Errors never become successful abstentions. No thresholds or application actions are applied.

Example
package main

import (
	"context"
	"fmt"

	"github.com/deepnoodle-ai/decide"
	"github.com/deepnoodle-ai/decide/decidetest"
)

func main() {
	srv, err := decidetest.Start()
	if err != nil {
		panic(err)
	}
	defer srv.Close()
	client, err := srv.NewClient()
	if err != nil {
		panic(err)
	}
	srv.Answer("pick", decidetest.ChoiceAnswer(map[string]float64{"c1": .9, "c2": .05, "none": .05}))
	d, err := decide.Pick(context.Background(), client, "Charged twice.", "Choose a queue.", []decide.Candidate[string]{
		{Item: "billing", Description: "Payments and invoices"},
		{Item: "engineering", Description: "Product defects"},
	})
	if err != nil {
		panic(err)
	}
	fmt.Println(d.Item, d.Index, d.Picked)
}
Output:
billing 0 true

type Evaluation

type Evaluation[A Answer] struct {
	Answer   A
	Response *Response
}

Evaluation contains a typed judgment and the response that supplied it. Response retains model, usage, request ID, headers, and validation failures.

func Eval

func Eval[A Answer](ctx context.Context, client *Client, state any, question QuestionFor[A], opts ...RequestOption) (Evaluation[A], error)

Eval asks one typed question under the key "eval". It always validates the answer, even when client-wide validation is disabled. External QuestionFor implementations need no answer registration.

Structural failures return no typed Answer. Consistency-only failures return the full Answer and an error matching ErrInconsistentAnswer. Every response returned by Client.SystemOne remains available in Response. Use NewRequest, Ask, and Client.SystemOne for multiple questions.

Example
package main

import (
	"context"
	"fmt"

	"github.com/deepnoodle-ai/decide"
	"github.com/deepnoodle-ai/decide/decidetest"
)

func main() {
	srv, err := decidetest.Start()
	if err != nil {
		panic(err)
	}
	defer srv.Close()
	client, err := srv.NewClient()
	if err != nil {
		panic(err)
	}
	srv.Answer("eval", decidetest.NoulAnswer(.9))
	e, err := decide.Eval(context.Background(), client, "Charged twice.", decide.Noul("Billing issue?"))
	if err != nil {
		panic(err)
	}
	fmt.Println(e.Answer.Noul, e.Response.Model)
}
Output:
0.9 jev-1.13.0

type HTTPTransport

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

HTTPTransport speaks TypeSafe's native HTTP API. It is safe for concurrent use.

func NewHTTPTransport

func NewHTTPTransport(cfg HTTPTransportConfig) (*HTTPTransport, error)

NewHTTPTransport returns a transport for the native API. A missing API key is an error wrapping ErrNoAPIKey.

func (HTTPTransport) GoString

func (t HTTPTransport) GoString() string

GoString describes the transport with the API key redacted, for %#v.

func (*HTTPTransport) ListModels

func (t *HTTPTransport) ListModels(ctx context.Context) (*ModelList, error)

ListModels sends one GET /v1/models attempt.

func (HTTPTransport) LogValue

func (t HTTPTransport) LogValue() slog.Value

LogValue implements slog.LogValuer with the API key redacted.

func (HTTPTransport) String

func (t HTTPTransport) String() string

String describes the transport with the API key redacted. String, GoString, and LogValue have value receivers so that both HTTPTransport and *HTTPTransport print safely, including with %+v.

func (*HTTPTransport) SystemOne

func (t *HTTPTransport) SystemOne(ctx context.Context, req *Request) (*Response, error)

SystemOne sends one POST /v1/systemone attempt. The body is decoded with DecodeResponse, so answers take the types of req's questions.

type HTTPTransportConfig

type HTTPTransportConfig struct {
	APIKey     string
	BaseURL    string       // default https://api.typesafe.ai
	HTTPClient *http.Client // default: new client with http.DefaultTransport, no Timeout
	UserAgent  string       // product token prepended to the SDK token
}

HTTPTransportConfig configures an HTTPTransport.

func (HTTPTransportConfig) GoString

func (c HTTPTransportConfig) GoString() string

GoString describes the config with the API key redacted, for %#v.

func (HTTPTransportConfig) LogValue

func (c HTTPTransportConfig) LogValue() slog.Value

LogValue implements slog.LogValuer with the API key redacted.

func (HTTPTransportConfig) String

func (c HTTPTransportConfig) String() string

String describes the config with the API key redacted.

type Handle

type Handle[A Answer] struct {
	// contains filtered or unexported fields
}

Handle is a typed reference to one question in a request. It reads the matching answer from a response as the answer's concrete type.

func Ask

func Ask[A Answer](req *Request, key string, q QuestionFor[A]) Handle[A]

Ask adds q under key and returns a typed handle. It panics if req is nil, key is empty, or key is already present: these are programming errors, like registering a duplicate route in net/http.

func (Handle[A]) From

func (h Handle[A]) From(resp *Response) (A, error)

From returns the answer for h's key. In order: if resp is nil or the key is absent, it returns an *AnswerError with reason missing_answer; if resp.Invalid has an error for the key, it returns that error together with the answer when the stored answer is an A (so a caller who accepts ErrInconsistentAnswer can proceed); if the stored answer is not an A, it returns type_mismatch. Other keys of the same response stay usable.

func (Handle[A]) Key

func (h Handle[A]) Key() string

Key returns the question key.

type InvalidAnswersError

type InvalidAnswersError struct{ Answers []*AnswerError }

InvalidAnswersError collects every answer that failed validation, sorted by key then reason. SystemOne returns it together with the response.

func (*InvalidAnswersError) Error

func (e *InvalidAnswersError) Error() string

Error returns the count plus the first two failures.

func (*InvalidAnswersError) Unwrap

func (e *InvalidAnswersError) Unwrap() []error

Unwrap returns each *AnswerError.

type Model

type Model struct {
	Name        string                     // "name"
	Description string                     // "description"
	ReleaseDate string                     // "release_date"; YYYY-MM-DD, kept as a string
	Extra       map[string]json.RawMessage // unmodeled members, written back on marshal
}

Model is one entry from GET /v1/models. The list contains aliases such as "jev-latest"; versioned IDs are accepted by the API whether or not they are listed.

func (Model) MarshalJSON

func (m Model) MarshalJSON() ([]byte, error)

MarshalJSON writes name, description, release_date, then Extra. It has a value receiver so a Model marshals the same whether or not it is addressable.

func (*Model) UnmarshalJSON

func (m *Model) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes a model leniently; unknown members go to Extra.

type ModelList

type ModelList struct {
	Models    []Model
	RequestID string                     // x-typesafe-request-id
	Header    http.Header                // response headers; nil if the transport has none
	Raw       []byte                     // response body; nil if the transport has none
	Extra     map[string]json.RawMessage // unmodeled top-level members
}

ModelList is the result of GET /v1/models.

func (*ModelList) MarshalJSON

func (l *ModelList) MarshalJSON() ([]byte, error)

MarshalJSON writes {"models": [...]} then Extra.

func (*ModelList) UnmarshalJSON

func (l *ModelList) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes a model list. It fails if data is not a JSON object; a missing "models" becomes an empty list.

type ModelsService

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

ModelsService lists models. Use it through Client.Models.

func (*ModelsService) List

func (s *ModelsService) List(ctx context.Context) (*ModelList, error)

List returns the models the API advertises. It uses the client's retry loop and logging. Results are not cached.

Example

List the models the API advertises. Aliases such as jev-latest move; the resolved version is on every Response.

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/deepnoodle-ai/decide/decidetest"
)

// startFake starts a fake API with canned answers, standing in for
// https://api.typesafe.ai so the examples run without a key.
func startFake() *decidetest.Server {
	srv, err := decidetest.Start()
	if err != nil {
		log.Fatal(err)
	}
	srv.Answer("billing", decidetest.NoulAnswer(0.93))
	return srv
}

func main() {
	srv := startFake()
	defer srv.Close()
	client, err := srv.NewClient()
	if err != nil {
		log.Fatal(err)
	}
	list, err := client.Models.List(context.Background())
	if err != nil {
		log.Fatal(err)
	}
	for _, m := range list.Models {
		fmt.Println(m.Name)
	}
}
Output:
jev-latest
jev-preview

type NoulAnswer

type NoulAnswer struct {
	Noul  float64
	Extra map[string]json.RawMessage // unmodeled members, as received
	// contains filtered or unexported fields
}

NoulAnswer is the answer to a NoulQuestion. Noul is P(yes) in [0,1]. There is no confidence field; the API does not return one.

func (*NoulAnswer) AnswerType

func (a *NoulAnswer) AnswerType() string

AnswerType returns "noul".

func (*NoulAnswer) MarshalJSON

func (a *NoulAnswer) MarshalJSON() ([]byte, error)

MarshalJSON writes "type" first, then the fields, then Extra sorted by key.

func (*NoulAnswer) UnmarshalJSON

func (a *NoulAnswer) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes a noul answer. "noul" is required.

type NoulCriteria

type NoulCriteria struct {
	True  any // encoded as "true"; nil encodes as null (the spec allows it)
	False any // encoded as "false"
}

NoulCriteria optionally describes what "true" and "false" mean.

type NoulOption

type NoulOption func(*NoulQuestion)

NoulOption configures a NoulQuestion built by Noul.

func NoulFalse

func NoulFalse(description any) NoulOption

NoulFalse sets Criteria.False, allocating Criteria.

func NoulTrue

func NoulTrue(description any) NoulOption

NoulTrue sets Criteria.True, allocating Criteria.

type NoulQuestion

type NoulQuestion struct {
	Instructions any
	Criteria     *NoulCriteria // nil omits "criteria"
	Extra        map[string]any
}

NoulQuestion asks whether a condition holds. Its answer is P(yes).

Instructions and the criteria values may be a string, an object, an array, or nil (JSON null); they are encoded with encoding/json as given.

func Noul

func Noul(instructions any, opts ...NoulOption) *NoulQuestion

Noul builds a Noul question.

func (*NoulQuestion) MakeAnswer

func (q *NoulQuestion) MakeAnswer() Answer

MakeAnswer returns q.NewAnswer().

func (*NoulQuestion) MarshalJSON

func (q *NoulQuestion) MarshalJSON() ([]byte, error)

MarshalJSON encodes the question with "type" first.

func (*NoulQuestion) NewAnswer

func (q *NoulQuestion) NewAnswer() *NoulAnswer

NewAnswer returns a new *NoulAnswer.

func (*NoulQuestion) QuestionType

func (q *NoulQuestion) QuestionType() string

QuestionType returns "noul".

func (*NoulQuestion) UnmarshalJSON

func (q *NoulQuestion) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes a noul question. Unknown members go to Extra.

func (*NoulQuestion) ValidateAnswer

func (q *NoulQuestion) ValidateAnswer(a Answer) error

ValidateAnswer checks a noul answer: it is a *NoulAnswer with "noul" present, finite, and in [0,1].

type Prob

type Prob struct {
	Key string
	P   float64
}

Prob is one entry of a probability distribution.

type Question

type Question interface {
	QuestionType() string
	json.Marshaler
}

Question is one entry in Request.Questions. Implementations outside this package are allowed. MarshalJSON must emit a JSON object whose "type" member equals QuestionType().

func DecodeQuestion

func DecodeQuestion(data []byte) (Question, error)

DecodeQuestion decodes one question object. Unknown types become *RawQuestion.

type QuestionFor

type QuestionFor[A Answer] interface {
	Question
	NewAnswer() A
}

QuestionFor binds a question to the Go type of its answer so Ask can infer the handle type. NewAnswer returns a new zero answer.

type RawAnswer

type RawAnswer struct {
	Type string
	JSON json.RawMessage
	Err  error
}

RawAnswer holds an answer whose type is not registered, or a registered type that failed to decode (Err is then non-nil). JSON is the full answer object as received. AnswerType returns Type.

func (*RawAnswer) AnswerType

func (a *RawAnswer) AnswerType() string

AnswerType returns a.Type.

func (*RawAnswer) MarshalJSON

func (a *RawAnswer) MarshalJSON() ([]byte, error)

MarshalJSON returns JSON as received, or {"type": Type} if JSON is empty.

func (*RawAnswer) UnmarshalJSON

func (a *RawAnswer) UnmarshalJSON(data []byte) error

UnmarshalJSON keeps a copy of data and reads Type from it.

type RawQuestion

type RawQuestion struct {
	Type string
	JSON json.RawMessage // the full question object, including "type"
}

RawQuestion sends a question type this package does not model.

func (*RawQuestion) MakeAnswer

func (q *RawQuestion) MakeAnswer() Answer

MakeAnswer returns q.NewAnswer().

func (*RawQuestion) MarshalJSON

func (q *RawQuestion) MarshalJSON() ([]byte, error)

MarshalJSON returns q.JSON unchanged after checking that it is an object whose "type" equals q.Type.

func (*RawQuestion) NewAnswer

func (q *RawQuestion) NewAnswer() *RawAnswer

NewAnswer returns a new *RawAnswer with Type set.

func (*RawQuestion) QuestionType

func (q *RawQuestion) QuestionType() string

QuestionType returns q.Type.

func (*RawQuestion) UnmarshalJSON

func (q *RawQuestion) UnmarshalJSON(data []byte) error

UnmarshalJSON keeps a copy of data and reads Type from it.

type Request

type Request struct {
	State     any    // string, object, array, or json.RawMessage
	Model     string // empty: client default
	Questions map[string]Question
	Extra     map[string]any // unmodeled top-level fields
}

Request is one call to POST /v1/systemone.

A Request is not safe for concurrent mutation. A fully built request may be sent concurrently by many goroutines; the client never mutates it.

func NewRequest

func NewRequest(state any, opts ...RequestOption) *Request

NewRequest returns a request for state with a non-nil Questions map.

func (*Request) MarshalJSON

func (r *Request) MarshalJSON() ([]byte, error)

MarshalJSON writes state, model (omitted when empty), questions sorted by key, then Extra sorted by key.

func (*Request) UnmarshalJSON

func (r *Request) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes a request. Questions are decoded with DecodeQuestion; unknown fields go to Extra with their raw bytes. A string state decodes to a Go string; any other state is kept as json.RawMessage so its bytes, including object member order, survive.

func (*Request) Validate

func (r *Request) Validate() error

Validate reports whether the request can be sent. Errors wrap ErrInvalidRequest. It does not enforce per-model limits such as the 255-option or 10-level caps; the server reports those as 422.

type RequestOption

type RequestOption func(*Request)

RequestOption configures a Request built by NewRequest.

func WithRequestExtra

func WithRequestExtra(key string, value any) RequestOption

WithRequestExtra sets an unmodeled top-level request field.

func WithRequestModel

func WithRequestModel(name string) RequestOption

WithRequestModel sets the model for this request, overriding the client default. Aliases such as "jev-latest" and versioned IDs are both accepted.

type Response

type Response struct {
	Model     string // resolved version, e.g. "jev-1.13.0"
	Answers   map[string]Answer
	Usage     Usage
	RequestID string                     // x-typesafe-request-id
	Header    http.Header                // response headers; nil if the transport has none
	Raw       []byte                     // response body; nil if the transport has none
	Extra     map[string]json.RawMessage // unmodeled top-level members

	// Invalid holds the validation failure for each key that failed, set by
	// the client after validation; nil when every answer is valid.
	Invalid map[string]*AnswerError
}

Response is the result of POST /v1/systemone.

func DecodeResponse

func DecodeResponse(data []byte, req *Request) (*Response, error)

DecodeResponse decodes a response body using req to choose answer types, then the registry, then *RawAnswer. req may be nil. It fails only if data is not a JSON object or "answers" is not an object; a bad answer never fails the whole response.

Decoding is lenient: unknown top-level members go to Extra, a missing "answers" becomes an empty map, and a missing "usage" leaves zeros. If "model" or "usage" has the wrong JSON type, the field is left zero and its raw bytes are kept in Extra under the same key; MarshalJSON does not write them back, because the modeled field wins.

Discrepancy: usage is required by the docs and the OpenAPI spec but optional in the Python SDK. We decode a missing usage as zeros.

func (*Response) MarshalJSON

func (r *Response) MarshalJSON() ([]byte, error)

MarshalJSON writes the wire form: model, answers sorted by key, usage, then Extra sorted by key. RequestID, Header, Raw, and Invalid are not part of the body and are not written.

func (*Response) UnmarshalJSON

func (r *Response) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes a response using the answer registry only; see DecodeResponse to decode with the request's question types.

type ScoreAnswer

type ScoreAnswer struct {
	Score         float64
	Legend        map[string]any     // keys "0".."n-1"
	Probabilities map[string]float64 // keys "0".."n-1"
	Confidence    float64
	Extra         map[string]json.RawMessage
	// contains filtered or unexported fields
}

ScoreAnswer is the answer to a ScoreQuestion. Score is the probability-weighted level, in [0, levels-1].

Discrepancy: the docs say legend values are strings; the OpenAPI spec allows string, object, or array. Legend therefore holds any.

func (*ScoreAnswer) AnswerType

func (a *ScoreAnswer) AnswerType() string

AnswerType returns "score".

func (*ScoreAnswer) Level

func (a *ScoreAnswer) Level() int

Level returns the index of the highest probability, lowest index on ties; -1 if there are no probabilities. Keys that are not integers are ignored.

Example

Score is the expected zero-based level; Level selects the most likely level.

package main

import (
	"fmt"

	"github.com/deepnoodle-ai/decide/decidetest"
)

func main() {
	a := decidetest.ScoreAnswer([]any{"can wait", "this week", "today"}, 0.1, 0.3, 0.6)
	fmt.Printf("expected level: %.2f\n", a.Score)
	fmt.Println("most likely:", a.LevelLabel(a.Level()))
}
Output:
expected level: 1.50
most likely: today

func (*ScoreAnswer) LevelLabel

func (a *ScoreAnswer) LevelLabel(i int) any

LevelLabel returns Legend[strconv.Itoa(i)], or nil.

func (*ScoreAnswer) Levels

func (a *ScoreAnswer) Levels() int

Levels returns max(len(Legend), len(Probabilities)). Validation allows a legend with missing keys, so the legend alone can undercount.

func (*ScoreAnswer) MarshalJSON

func (a *ScoreAnswer) MarshalJSON() ([]byte, error)

MarshalJSON writes "type" first, then the fields, then Extra sorted by key. Legend and probabilities are written with keys sorted, not in wire order.

func (*ScoreAnswer) Normalized

func (a *ScoreAnswer) Normalized() float64

Normalized returns Score / (Levels()-1), or 0 when Levels() < 2.

func (*ScoreAnswer) ProbabilityAt

func (a *ScoreAnswer) ProbabilityAt(i int) (float64, bool)

ProbabilityAt returns Probabilities[strconv.Itoa(i)].

func (*ScoreAnswer) UnmarshalJSON

func (a *ScoreAnswer) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes a score answer. All four fields are required.

type ScoreQuestion

type ScoreQuestion struct {
	Instructions any
	Criteria     []any // ordered levels; index is the level number
	Extra        map[string]any
}

ScoreQuestion asks where on an ordered scale the state falls.

func Score

func Score(instructions any, levels ...any) *ScoreQuestion

Score builds a Score question. levels are ordered; the index of each level is its number.

func ScoreOf

func ScoreOf[T any](instructions any, levels []T) *ScoreQuestion

ScoreOf is Score for a typed slice, e.g. ScoreOf("How urgent?", []string{...}).

func (*ScoreQuestion) MakeAnswer

func (q *ScoreQuestion) MakeAnswer() Answer

MakeAnswer returns q.NewAnswer().

func (*ScoreQuestion) MarshalJSON

func (q *ScoreQuestion) MarshalJSON() ([]byte, error)

MarshalJSON encodes the question with levels in order.

Discrepancy: the OpenAPI spec forbids null level entries (Choice allows null descriptions), while the JS builder docs say entries may be null. We send what the caller gave and let the server decide.

func (*ScoreQuestion) NewAnswer

func (q *ScoreQuestion) NewAnswer() *ScoreAnswer

NewAnswer returns a new *ScoreAnswer.

func (*ScoreQuestion) QuestionType

func (q *ScoreQuestion) QuestionType() string

QuestionType returns "score".

func (*ScoreQuestion) UnmarshalJSON

func (q *ScoreQuestion) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes a score question. Any number of levels is accepted.

func (*ScoreQuestion) ValidateAnswer

func (q *ScoreQuestion) ValidateAnswer(a Answer) error

ValidateAnswer checks a score answer against the levels. Structural: fields present, values finite and in [0,1], probability keys are exactly "0".."n-1", every legend key is one of them (missing legend keys are allowed), score finite. Consistency: score within tolerance of [0, n-1] and probabilities sum to about 1.

type Transport

type Transport interface {
	SystemOne(ctx context.Context, req *Request) (*Response, error)
	ListModels(ctx context.Context) (*ModelList, error)
}

Transport performs exactly one attempt of each API call. It must not retry, must be safe for concurrent use, and must not mutate req. req.Model is always set. Non-2xx outcomes return *APIError. Answers must be normalized to this package's answer types; dialect quirks stay inside.

The interface is frozen: it will never gain a method, because that would break every implementation outside this package. A new endpoint arrives as an optional interface that the client type-asserts, returning an error wrapping errors.ErrUnsupported when the transport lacks it.

type Usage

type Usage struct {
	InputTokens  int                        // "input_tokens"
	OutputTokens int                        // "output_tokens"
	Extra        map[string]json.RawMessage // unmodeled members, written back on marshal
}

Usage reports token counts for a request.

func (Usage) MarshalJSON

func (u Usage) MarshalJSON() ([]byte, error)

MarshalJSON writes input_tokens, output_tokens, then Extra sorted by key. It has a value receiver so a Usage field marshals without being addressable.

func (*Usage) UnmarshalJSON

func (u *Usage) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes usage leniently. Token counts are read as numbers and kept when integral (so 12.0 is 12); a count that is not an integral number is left at zero and its raw bytes go to Extra, as do unknown members. It fails only if data is not a JSON object.

type ValidationDetail

type ValidationDetail struct {
	Loc   []any           `json:"loc"` // strings and numbers (float64)
	Msg   string          `json:"msg"`
	Type  string          `json:"type"`
	Input json.RawMessage `json:"input,omitempty"`
	Ctx   map[string]any  `json:"ctx,omitempty"`
}

ValidationDetail is one entry of a 422 response's "detail" array.

Directories

Path Synopsis
Package backend selects a decision provider during decide.Client construction.
Package backend selects a decision provider during decide.Client construction.
Package cloudflare connects decide to the Clef and Clef-flash models on Cloudflare Workers AI.
Package cloudflare connects decide to the Clef and Clef-flash models on Cloudflare Workers AI.
cmd
decide command
Command decide asks typed questions about your data and saves every answer.
Command decide asks typed questions about your data and saves every answer.
Package decidetest provides a fake TypeSafe API server and answer fixtures, so code built on package decide can be tested without an API key or a network.
Package decidetest provides a fake TypeSafe API server and answer fixtures, so code built on package decide can be tested without an API key or a network.
examples
calibrate command
Command calibrate fits a cutoff on saved, labeled answers and checks it on separate held-out cases.
Command calibrate fits a cutoff on saved, labeled answers and checks it on separate held-out cases.
compact command
Command compact trims an agent transcript before the next turn: it asks whether each tool call is still needed for the task, then keeps the most needed ones verbatim under a budget, using a short form where the full output is not needed.
Command compact trims an agent transcript before the next turn: it asks whether each tool call is still needed for the task, then keeps the most needed ones verbatim under a budget, using a short form where the full output is not needed.
fanout command
Command fanout asks whether each retrieved passage is relevant to a query, one request per passage, with at most four requests in flight.
Command fanout asks whether each retrieved passage is relevant to a query, one request per passage, with at most four requests in flight.
funnel command
Command funnel picks tools for a request in two stages: skim every tool's one-line summary in one request, then re-check up to two survivors with their full descriptions, one request each.
Command funnel picks tools for a request in two stages: skim every tool's one-line summary in one request, then re-check up to two survivors with their full descriptions, one request each.
gate command
Command gate asks what file operation a user requested and gates the agent's proposed delete with patterns/gate: allow, review, or escalate.
Command gate asks what file operation a user requested and gates the agent's proposed delete with patterns/gate: allow, review, or escalate.
heads command
Command heads triages a support ticket in one request: it asks which team owns the ticket and, speculatively, each team's follow-up question, then reads only the chosen team's answer.
Command heads triages a support ticket in one request: it asks which team owns the ticket and, speculatively, each team's follow-up question, then reads only the chosen team's answer.
pick command
Command pick finds candidate email addresses with a regex and asks the model which one the sender wants their receipt sent to.
Command pick finds candidate email addresses with a regex and asks the model which one the sender wants their receipt sent to.
rank command
Command rank reranks a retrieved shortlist by asking, for each passage, whether it answers the query, then sorting by the answers.
Command rank reranks a retrieved shortlist by asking, for each passage, whether it answers the query, then sorting by the answers.
internal
cache
Package cache keeps the answers decide has received, so a run asks again only about what changed.
Package cache keeps the answers decide has received, so a run asks again only about what changed.
cli
Package cli implements the decide command.
Package cli implements the decide command.
runs
Package runs saves each evaluation of a dataset so it can be viewed again or resumed after an interruption.
Package runs saves each evaluation of a dataset so it can be viewed again or resumed after an interruption.
source
Package source reads files, directories, and stdin as a stream of items for a template to evaluate.
Package source reads files, directories, and stdin as a stream of items for a template to evaluate.
template
Package template loads templates: named sets of typed questions that the decide command asks about every item in a dataset.
Package template loads templates: named sets of typed questions that the decide command asks about every item in a dataset.
Package openai connects decide to OpenAI's Decisions API, which runs on gpt-6-luna.
Package openai connects decide to OpenAI's Decisions API, which runs on gpt-6-luna.
patterns
calibrate
Package calibrate fits one gate threshold and optional probability temperature from saved, labeled decision-model answers.
Package calibrate fits one gate threshold and optional probability temperature from saved, labeled decision-model answers.
compact
Package compact shortens a long context by asking, for each segment, whether it is still needed, then keeping the highest-scoring segments verbatim under a budget.
Package compact shortens a long context by asking, for each segment, whether it is still needed, then keeping the highest-scoring segments verbatim under a budget.
fanout
Package fanout runs independent decision requests over a collection of items with a bound on concurrent work.
Package fanout runs independent decision requests over a collection of items with a bound on concurrent work.
funnel
Package funnel runs staged screening: cheap questions about many items, then narrower questions about the survivors, carrying each item's answers from stage to stage.
Package funnel runs staged screening: cheap questions about many items, then narrower questions about the survivors, carrying each item's answers from stage to stage.
gate
Package gate turns decision-model answers into one of three outcomes: Allow, Review, or Escalate.
Package gate turns decision-model answers into one of three outcomes: Allow, Review, or Escalate.
heads
Package heads builds one request with a Choice selector and questions for every possible branch, then reads only the selected branch's answers.
Package heads builds one request with a Choice selector and questions for every possible branch, then reads only the selected branch's answers.
pick
Package pick asks a decision model to select an item from a caller's candidate list and returns the original item.
Package pick asks a decision model to select an item from a caller's candidate list and returns the original item.
rank
Package rank orders caller-owned candidates from Noul observations and takes an ordered prefix under a caller-defined allowance.
Package rank orders caller-owned candidates from Noul observations and takes an ordered prefix under a caller-defined allowance.
Package templates gives Go programs the decide command's built-in templates, the same ones decide templates lists.
Package templates gives Go programs the decide command's built-in templates, the same ones decide templates lists.

Jump to

Keyboard shortcuts

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