Documentation
¶
Overview ¶
Package decision defines the DecisionProvider contract and the Chain that runs providers in order until one decides.
A decision answers, in a single inference, the conditional questions a product needs before doing any work: which module, which intent, what kind of interaction, what the user refers to, the minimum context scopes, the dynamic data to fetch, whether deterministic handling is possible, whether the main LLM is needed, and the suggested presentation.
Decisions never carry entity IDs: providers identify a reference expression ("my dentist appointment tomorrow"); the product resolves it against real data.
The schema is intentionally evolvable: new fields are additive, and consumers treat unknown module/intent/presentation values as "not decided".
What is sent to an engine ¶
Everything placed in Request (Text, Recent, State titles, Context, Taxonomy descriptions) or in a ScoreRequest (Text, Context, question instructions, candidate descriptions) is sent VERBATIM to the engine's operator: a hosted decision model or LLM, or a cloud decision endpoint. Send metadata (names, schemas, public descriptions), never row data, credentials or user identifiers; the product is responsible for what it puts there.
Acting on a chain's answer ¶
One rule decides whether an answer may be acted on (Decision.Actionable and Selection.Actionable): a positive verdict was stamped on it by this package's judges. A decision never becomes actionable by omission or by claim: an engine or provider used directly returns it unjudged, which is NOT actionable until a Chain, or an engine built with a policy (compose.WithPolicy), judged it; the zero Decision is not actionable; and the exported Outcome field, which is kept on the wire for traces and telemetry, is informational only: what a remote engine, a stored decision or a provider writes there is never believed.
The positive verdicts are: a SelectionPolicy selected a calibrated answer (selected, several); the policy explicitly accepted an uncalibrated LLM self-report at a stated bar (accepted; SelectionPolicy.AcceptUncalibratedAt, off for DurablePolicy); the decision is deterministic (deterministic: it came from exact logic such as a rule table, declared with Deterministic, and no policy threshold applies to it); or a Chain without a Policy accepted it at its MinConfidence floor (floor). An LLM emulator's self-reported confidence is otherwise a proposal, never a selection.
A side-effectful interaction (SideEffectful: confirmation, rejection, correction, cancellation, undo) is held to one more bar, with or without a policy: its own InteractionConfidence must be above 0 and at least DurableMinConfidence, and a CALIBRATED claim about it must be backed by Decision.InteractionScores (a remote engine's calibrated flag is only a claim); an unbacked one counts as a self-report, which a policy accepts only with SelectionPolicy.AcceptUncalibratedSideEffects. Deterministic decisions are exact and exempt.
A Chain with a Policy only returns ok=true for an answer that rule accepts; an uncertain, "none", unscored or invalid answer falls through to the next provider. A chain built with KeepNonSelected can return ok=true for such an answer (but never an invalid one), and so can an engine called directly (compose.WithPolicy stamps its verdict): such a caller MUST check Decision.Actionable before acting on the decision.
Stored and replayed decisions ¶
A Decision that was marshalled and read back (a database row, a trace, a cloud response) is NOT actionable and has lost its provenance class: the verdict and the deterministic class live in unexported fields no data can set, so a rule decision replayed from storage reads self_reported. A product that replays must re-judge it through a chain or policy (Chain.Rejudge, SelectionPolicy.JudgeDecision), which treats it as the ordinary decision it now is, or re-run its rules. Rejudge keeps a refusal recorded in Outcome (a policy-less chain knows only its floor), so rejudge with the chain that carries the policy that applied. The verdict is also bound to the decision's module, intent, interaction, confidences and Calibrated: editing any of them after judging makes the decision not actionable until it is judged again, and decoding JSON into a judged value discards its verdict.
Stopped chains ¶
A Chain stops at an exhausted allowance (ErrQuota), a spent budget (ErrBudget, see compose.NewBudget) or a misconfigured engine (ErrMisconfigured) instead of handing the call to the next, possibly paid, provider (Chain.StopOnQuota, Chain.StopOnMisconfigured). A stopped chain returns ok=false, exactly like a chain nobody decided in, so a product MUST check Trace.StoppedBy (and use Trace.Err) before treating ok=false as "use the paid main-LLM path": the stop exists to keep that path from being billed. Place deterministic providers (rules) BEFORE the engines: a stop skips every provider after the one that stopped.
Index ¶
- Constants
- Variables
- func InvalidDetail(err error) string
- func ParseRetryAfter(v string, now time.Time) time.Duration
- func RetryDelay(err error) time.Duration
- func SideEffectful(i Interaction) bool
- func Validate(d Decision, t Taxonomy) error
- func ValidateScoreRequest(req ScoreRequest) error
- func ValidateScoreResult(req ScoreRequest, res ScoreResult) error
- type Answer
- type Attempt
- type Candidate
- type Chain
- type Decision
- type DeterministicProvider
- type Interaction
- type ModuleSpec
- type Outcome
- type Provenance
- type Provider
- type Question
- type QuestionKind
- type Reference
- type Report
- type Request
- type RetryDelayer
- type Score
- type ScoreRequest
- type ScoreResult
- type Scored
- type ScoredProvider
- type Selection
- type SelectionPolicy
- func (p SelectionPolicy) AtLeast(o SelectionPolicy) SelectionPolicy
- func (p SelectionPolicy) Evaluate(a Answer) Selection
- func (p SelectionPolicy) EvaluateDecision(d Decision) Selection
- func (p SelectionPolicy) JudgeDecision(d Decision) (Decision, Selection)
- func (p SelectionPolicy) Validate() error
- type StopPolicy
- type Taxonomy
- type Trace
- type TracedProvider
- type TracedScorer
- type Usage
Constants ¶
const ( // Narrowing policy: choosing which candidates to examine first. A wrong // pick costs extra work, nothing more. NarrowingMinConfidence = 0.50 NarrowingMinGap = 0.20 NarrowingMinProbability = 0.60 NarrowingStrongProbability = 0.85 NarrowingPotentialProbability = 0.30 // NarrowingAcceptUncalibratedAt is the self-reported confidence at which an // uncalibrated decision is accepted under the narrowing policy: the same 0.70 // floor decision.Chain applies without a policy. A decision acted on at that // bar is a proposal for narrowing the work, never durable knowledge. NarrowingAcceptUncalibratedAt = 0.70 // Durable policy: answers that will be stored and reused as fact. The bar // is higher; callers should still add a deterministic check // or a person's confirmation. DurableMinConfidence = 0.90 DurableMinGap = 0.20 DurableMinProbability = 0.90 DurableStrongProbability = 0.95 DurablePotentialProbability = 0.60 // DurableAcceptUncalibratedAt is 0: the durable policy never acts on an // uncalibrated decision. A product that must may set AcceptUncalibratedAt. DurableAcceptUncalibratedAt = 0 )
Documented defaults of the two named policies. They are PROVISIONAL: they come from a single small measurement against one decision model version, they are not a calibration, and they MUST be re-measured on a product's own corpus and for the exact model id in use (a moved model alias moves the numbers; see typesafe.Config.Model) before anything depends on them. Override them per product or decision type through configuration; they live here so no caller hard-codes its own.
const ( ReasonNotCalibrated = "not_calibrated" ReasonNoScores = "no_scores" ReasonNoneOfThese = "none_of_these" ReasonLowConfidence = "low_confidence" ReasonNarrowGap = "narrow_gap" ReasonNothingAbove = "nothing_above_floor" ReasonOnlyPotential = "only_potential" ReasonTruncatedToMax = "truncated_to_max_picks" ReasonInvalidPolicy = "invalid_policy" // ReasonAcceptedUncalibrated marks OutcomeAccepted: the policy's // AcceptUncalibratedAt opt-in, not a calibrated selection. ReasonAcceptedUncalibrated = "accepted_uncalibrated" // ReasonDeterministic marks OutcomeDeterministic: exact logic, no threshold. ReasonDeterministic = "deterministic_rule" // ReasonSideEffectUncalibrated: an uncalibrated side-effectful interaction // without the AcceptUncalibratedSideEffects opt-in. ReasonSideEffectUncalibrated = "side_effect_uncalibrated" // ReasonInteractionLowConfidence: an opted-in uncalibrated side-effectful // interaction whose InteractionConfidence is 0 (not reported) or below the bar. ReasonInteractionLowConfidence = "interaction_low_confidence" // ReasonInteractionNarrowGap: a calibrated side-effectful interaction whose // InteractionScores lead the runner-up by less than the (durable) gap. ReasonInteractionNarrowGap = "interaction_narrow_gap" // ReasonInteractionNotTop: a calibrated side-effectful interaction whose own // InteractionScores contradict it (OutcomeInvalid under a policy). ReasonInteractionNotTop = "interaction_not_top" // ReasonBadConfidence: a non-finite or out-of-range confidence or interaction // probability on a decision (OutcomeInvalid); JudgeDecision does not trust a // number Validate would have refused. ReasonBadConfidence = "bad_confidence" // ReasonDecisionNotScored: a calibrated decision whose own module/intent is not // among its Scores (OutcomeInvalid). ReasonDecisionNotScored = "decision_not_scored" // ReasonDecisionNotTop: a calibrated decision whose own module/intent scores // below another option of its own Scores (OutcomeInvalid). ReasonDecisionNotTop = "decision_not_top" // ReasonBadScores: a calibrated decision with a non-finite or out-of-range // probability in Scores (OutcomeInvalid). ReasonBadScores = "bad_scores" )
Reasons reported in Selection.Reason.
const ( AttemptDecided = "decided" AttemptAbstained = "abstained" AttemptLowConfidence = "low_confidence" AttemptInvalid = "invalid" AttemptError = "error" AttemptTimeout = "timeout" // called at all. AttemptUnavailable = "unavailable" // AttemptCancelled: the engine was started and then cancelled because // another engine answered first (a hedge or race loser). It is not a failure // of the engine and does not count against a breaker. AttemptCancelled = "cancelled" // AttemptUncertain: the engine answered, but its calibrated answer was // judged uncertain by the combinator's policy. AttemptUncertain = "uncertain" // AttemptUnsupported: the engine does not implement the asked operation // (for example it is not a ScoredProvider). AttemptUnsupported = "unsupported" // AttemptRejected: the engine refused the request itself as invalid (a // caller fault: an oversized state, too many options, a malformed // question). It says nothing about the engine's health. AttemptRejected = "rejected" // AttemptAuth: the engine refused the caller's credentials. Distinct and // loud on purpose: retrying or failing over hides a misconfiguration that a // person has to fix. It says nothing about the engine's health. AttemptAuth = "auth" // AttemptQuota: the engine refused because the caller's allowance is // exhausted (ErrQuota). It is not a fault of the engine (a circuit breaker // ignores it) and not transient (retrying cannot help), so a combinator does // not hand the call to a backup unless configured to (compose.OnQuota): a // paid backup would silently take over a metered caller's traffic. AttemptQuota = "quota" // AttemptBudget: a spending cap the caller set on an engine (compose.NewBudget) // is used up, so the engine was not called (ErrBudget). Like AttemptQuota it // is neither a fault of the engine nor transient, and a chain stops at it by // default instead of handing the call to the next, possibly paid, provider. AttemptBudget = "budget" // AttemptMisconfigured: the engine's endpoint answered in a way that means // the CONFIGURATION is wrong (an unknown product, a base URL that does not // speak the protocol; ErrMisconfigured). A person has to fix it, so it is // loud: a breaker ignores it and a combinator does not hide it behind a // backup. AttemptMisconfigured = "misconfigured" )
Attempt outcomes recorded in Attempt.Outcome.
const MaxRetryDelay = 10 * time.Minute
MaxRetryDelay caps any retry delay an engine asks for, so a bad header cannot take an engine out of service for longer.
Variables ¶
var ( // its engine because the engine is known to be down (a circuit breaker is // open). Chain records it as AttemptUnavailable. ErrUnavailable = errors.New("decision: engine unavailable") // ErrUnsupported is returned (wrapped) when an engine does not implement // the asked operation. ErrUnsupported = errors.New("decision: operation not supported by engine") // ErrInvalidRequest is matched (errors.Is) by an engine error that says the // REQUEST was refused as invalid (HTTP 400/422, an oversized state, too many // options). It is the caller's fault, not the engine's: a circuit breaker // does not count it, because tripping everyone's breaker over one caller's // bad request would take a healthy engine out of service. ErrInvalidRequest = errors.New("decision: request rejected as invalid") // ErrAuth is matched by an engine error that says the credentials were // refused (HTTP 401/403). It does not count against a circuit breaker // either, and is recorded as AttemptAuth so it stays visible. ErrAuth = errors.New("decision: authentication failed") // ErrQuota is matched by an engine error that says the caller's allowance is // exhausted (an HTTP 429 with the error code "quota"). Neither the engine's // fault (a breaker ignores it) nor transient: retrying cannot help, and // handing the call to a paid backup is an explicit choice (compose.OnQuota). // It is recorded as AttemptQuota and surfaced to the caller. A plain // rate limit (429 "rate_limited") is NOT a quota: it is transient and counts // as an engine-health failure. ErrQuota = errors.New("decision: allowance exhausted") // ErrBudget is matched by an error that says a spending cap the CALLER set on // an engine is used up (compose.NewBudget): the engine was not called. It is the // guard for the one case a provider's own errors cannot cover: an engine that // answers a transient error (a rate limit it does not tell from a spent // account) while a paid backup behind it takes every call. Like ErrQuota it is // not the engine's fault (a breaker ignores it), not transient, and not handed // to a backup unless compose.OnQuota says so; a Chain stops at it by default // (Chain.StopOnQuota). It is recorded as AttemptBudget. ErrBudget = errors.New("decision: budget exhausted") // ErrMisconfigured is matched by an engine error that says the endpoint is // configured wrongly (for example a base URL that does not speak the // protocol, or an unknown product). A breaker ignores it and Fallback does // not start its backup for it: it must be seen and fixed, not absorbed by an // uncalibrated backup forever. ErrMisconfigured = errors.New("decision: engine misconfigured") )
Functions ¶
func InvalidDetail ¶ added in v0.6.0
InvalidDetail is the text recorded in Attempt.Detail when an answer fails validation (Validate, ValidateScoreResult). It states the number of problems and nothing else: the messages of those errors quote candidate and question ids, and a trace must never carry caller-supplied content.
func ParseRetryAfter ¶ added in v0.6.0
ParseRetryAfter reads a Retry-After header value: a number of seconds or an HTTP date (relative to now). It returns 0 for a missing, malformed, negative or past value, and never more than MaxRetryDelay (a huge number of seconds cannot overflow).
func RetryDelay ¶ added in v0.6.0
RetryDelay returns the delay err asks for (0 when it carries none), capped at MaxRetryDelay. A circuit breaker uses it as a minimum open time.
func SideEffectful ¶ added in v0.7.0
func SideEffectful(i Interaction) bool
SideEffectful reports whether i acts on a pending or previous action (confirmation, rejection, correction, cancellation, undo), so a wrong answer is a side effect rather than a wasted lookup. A policy never accepts such an interaction from an uncalibrated engine without an explicit opt-in (SelectionPolicy.AcceptUncalibratedSideEffects), and never at confidence 0.
func Validate ¶
Validate checks d against the taxonomy: Interaction must be a known, non- empty value; module and intent must be declared UNLESS Interaction is one of the module-optional kinds (confirmation/rejection/cancellation/undo) and Module is empty, in which case both checks are skipped; confidences (module, intent, interaction) must be in [0,1]; scopes, presentation, Reference.Kind and RequiredData must be known to the taxonomy when the taxonomy declares a non-empty list for that dimension. A decision that fails validation is treated as an abstention.
func ValidateScoreRequest ¶ added in v0.6.0
func ValidateScoreRequest(req ScoreRequest) error
ValidateScoreRequest checks req is well formed: at least one question, unique non-empty question ids, a known kind, at least one candidate, unique non-empty candidate ids, and a NoneID (if set) that names a candidate of a KindChoice question. The error matches ErrInvalidRequest (the caller is at fault, not the engine) and names problems by position (question 2, candidate 3), never by id: ids are the caller's content, and an error ends up in traces and logs.
func ValidateScoreResult ¶ added in v0.6.0
func ValidateScoreResult(req ScoreRequest, res ScoreResult) error
ValidateScoreResult checks res answers every question of req with only known candidates and finite probabilities in [0,1] (a KindChoice answer must also sum to about 1), and a confidence in [0,1]. A result that fails is treated by combinators as no answer from that engine.
Types ¶
type Answer ¶ added in v0.6.0
type Answer struct {
QuestionID string `json:"questionId"`
Kind QuestionKind `json:"kind"`
// Scores are sorted by probability descending, ties by ID ascending.
Scores []Score `json:"scores"`
// Confidence in [0,1] is the engine's own certainty about the whole answer
// (for a Choice, derived from the distribution). It is only meaningful
// when HasConfidence is true: an independent relevance answer has none.
Confidence float64 `json:"confidence,omitempty"`
HasConfidence bool `json:"hasConfidence,omitempty"`
// Calibrated is true only when the numbers are calibrated probabilities
// (true for Jev, false for an LLM emulator). Thresholds are applied only
// to calibrated answers.
Calibrated bool `json:"calibrated"`
NoneID string `json:"noneId,omitempty"`
}
Answer is an engine's scores for one Question.
type Attempt ¶
type Attempt struct {
Provider string `json:"provider"`
Outcome string `json:"outcome"` // see the Attempt* constants
Detail string `json:"detail,omitempty"`
// Latency is the wall-clock time the attempt took. On the wire it is the
// integer "latencyMs" (milliseconds, rounded down); a reader that still sees
// the legacy "latency" field (nanoseconds, the encoding of a Go duration) uses
// it only when "latencyMs" is absent, and the encoder still writes it, so
// readers of either vintage work. See Attempt.MarshalJSON.
Latency time.Duration `json:"-"`
// Role is the engine's part in a combinator: "primary", "backup" or
// "racer"; empty for a plain chain provider.
Role string `json:"role,omitempty"`
// Usage is what THIS attempt consumed, when its engine reported it (a scored
// call answered by the engine). A hedged or fallen-back call has several
// attempts, each possibly billed, so metering per engine needs the per-attempt
// figure, not only the answering engine's ScoreResult.Usage. An attempt that
// reported none (a cancelled loser, a failure) carries nil, not zero: its true
// cost is unknown.
Usage *Usage `json:"usage,omitempty"`
}
Attempt records one provider's outcome for diagnostics.
func MergeReport ¶ added in v0.6.0
MergeReport returns the attempts to record for a TracedProvider: the engines it ran, with the answering engine's outcome overridden by the caller's own judgement (invalid, low_confidence, a policy verdict, carried by judged) when the provider returned an answer. When the provider reported no attempts, judged itself is recorded instead.
A leaf engine reports its own attempt for every upstream call, with the usage the call billed. The merge keeps that attempt, and so its Usage and Latency, and takes from judged only what the engine cannot know: the verdict, the Role in a combinator, and a Usage or Latency the engine did not report. An engine that failed (it reported one attempt, outcome error, under judged's own name) has the outcome and detail replaced by judged's classification (timeout, auth, ...). One upstream call stays one attempt: nothing is added, so usage is never counted twice.
func (Attempt) MarshalJSON ¶ added in v0.6.0
MarshalJSON writes Latency as the integer "latencyMs" and, for readers that predate it, as the legacy "latency" in nanoseconds.
func (*Attempt) UnmarshalJSON ¶ added in v0.6.0
UnmarshalJSON reads "latencyMs" when present, else the legacy "latency".
type Candidate ¶ added in v0.6.0
type Candidate struct {
ID string `json:"id"`
// Description is optional public text that helps the engine understand the
// candidate (for example a table's field names). Engines perform much
// better with it than with a bare name.
Description string `json:"description,omitempty"`
}
Candidate is one option of a Question.
type Chain ¶
type Chain struct {
Providers []Provider
// MinConfidence is the minimum Module and Intent confidence to accept a
// decision. Intent confidence is ignored when Intent is "", and Module
// confidence is ignored entirely for a module-optional decision (see
// Validate). Three cases:
// - MinConfidence == 0 (the zero value): default 0.7.
// - MinConfidence > 0: used as given.
// - MinConfidence < 0: "accept any" -- no confidence floor at all
// (a valid Scored.Confidence is always >= 0, so nothing is ever
// rejected on confidence grounds). Use this for a chain whose
// providers are deterministic and don't produce calibrated
// probabilities worth thresholding.
MinConfidence float64
// Timeout bounds each provider call (default 1500ms) UNLESS the
// provider itself implements `interface{ DecisionTimeout() time.Duration }`,
// in which case that provider's own value is used instead -- a remote
// decision call reasonably wants more time than a local one.
Timeout time.Duration
// Policy, when non-nil, replaces MinConfidence for answers that carry
// calibrated Scores: the policy alone decides selected / uncertain / none.
// Only a selected answer stops the chain; an uncertain or "none" answer is
// recorded in the trace (outcomes "uncertain" and "none") and the chain
// falls through to the next provider, exactly as a low-confidence answer
// does without a policy. A nil Policy keeps the legacy MinConfidence
// behaviour exactly.
Policy *SelectionPolicy
// KeepNonSelected makes a non-actionable answer (uncertain, "none",
// unscored) stop the chain and be returned with ok=true and Decision.Outcome
// set, so a caller can show "not sure" instead of escalating. It is off by
// default because ok=true is then not enough to act on: check
// Decision.Actionable.
KeepNonSelected bool
// StopOnQuota stops the chain, instead of trying its next provider, when a
// provider reports an exhausted allowance (ErrQuota, attempt outcome "quota") or
// an exhausted spending cap (ErrBudget, outcome "budget"; see compose.NewBudget).
// The zero value STOPS: an exhausted allowance must be loud and must not
// silently bill a paid backup that follows it in the chain; the product reads
// Trace.StoppedBy and Trace.Err. Opt out with FallThrough, naming the decision.
// An engine that was given compose.OnQuota has already chosen to fail over, and
// reports no quota error when its backup answers.
//
// A stopped chain returns ok=false, exactly like a chain nobody decided in: a
// product MUST check Trace.StoppedBy before treating ok=false as "use the paid
// main-LLM path", because the stop is there to keep that path from being billed.
//
// A stop also skips every provider after the one that stopped, deterministic
// ones included: put rules BEFORE the engines (aiconfig does).
StopOnQuota StopPolicy
// StopOnMisconfigured is StopOnQuota for ErrMisconfigured (an unknown product,
// a base URL that does not speak the protocol): a person has to fix it, so it is
// never absorbed by the next provider. The zero value stops.
StopOnMisconfigured StopPolicy
}
Chain runs providers in order; the first valid, confident decision wins. A chain with no deciding provider is not an error: the product falls back to its main-LLM path (which classifies and answers in one inference), unless the chain stopped (Trace.StoppedBy; see the package doc). List deterministic providers (ai/decision/rules) first: they are free and exact, and a stop at an engine would otherwise skip them.
func (Chain) Rejudge ¶ added in v0.7.0
Rejudge judges d the way this chain judges a provider's answer (its Policy, or its MinConfidence floor, and the side-effect gate), and reports whether the result is actionable. It is how a product acts on a decision it stored or received: a decision read back from JSON (a database row, a cloud response) is NOT actionable and has lost its provenance class (deterministic included), because the verdict and the class live in unexported fields no data can set.
Rejudge can only know what the decision and THIS chain say. It can never make a decision deterministic (a product that must replay a rule decision as deterministic re-runs its rules, ai/decision/rules), and a policy-less chain knows only its floor: a decision that a stricter policy refused in the live chain (an engine built with compose.WithPolicy(DurablePolicy) inside a policy-less chain with KeepNonSelected) would pass that floor. So the safer rule applies: a refusal recorded in Outcome (uncertain, none, unscored, invalid) is kept, never upgraded: Rejudge returns the decision with that outcome, not actionable. To re-judge a stored refusal under another policy, clear its Outcome first. Rejudge with the chain that carries the policy that should apply (its Policy, not the floor) whenever the decision was judged under one. The taxonomy validates the decision, as Request.Taxonomy does for a provider's answer.
A decision that is not actionable is returned with the outcome its judge gave it (OutcomeInvalid for one that fails validation or the policy's range checks, the policy's own refusal under a Policy, the stored refusal) and ok=false; one the policy-less floor or the side-effect gate refused has an empty Outcome, because no policy outcome describes it.
type Decision ¶
type Decision struct {
Module Scored `json:"module"`
Intent Scored `json:"intent"`
Interaction Interaction `json:"interaction"`
// InteractionConfidence is the engine's own confidence in Interaction, in
// [0,1], when the engine reports one (0 otherwise). It is additive:
// Interaction is only ever set from an answer the engine's selection policy
// selected, so a caller need not threshold it, but it may apply a stricter
// bar of its own to an interaction that acts on a pending action.
InteractionConfidence float64 `json:"interactionConfidence,omitempty"`
// InteractionScores are a calibrated engine's probabilities for the options of
// the Choice that picked Interaction, keyed by Interaction value. It is the
// evidence a calibrated claim about a SIDE-EFFECTFUL interaction needs (see
// SideEffectful): without it, or when it contradicts Interaction, the claim is
// only as strong as a self-report. Additive; empty when the engine has none.
InteractionScores map[string]float64 `json:"interactionScores,omitempty"`
Reference *Reference `json:"reference,omitempty"`
// RequiredScopes is the MINIMUM context needed. It does not mean other
// cached scopes must be dropped; see package ctxmgr.
RequiredScopes []string `json:"requiredScopes,omitempty"`
// RequiredData names dynamic data to fetch, e.g. "relevant_happenings".
RequiredData []string `json:"requiredData,omitempty"`
// Slots are extracted arguments ("when": "Friday 16:00", "title": ...).
Slots map[string]string `json:"slots,omitempty"`
CanHandleDeterministically bool `json:"canHandleDeterministically"`
NeedsLLM bool `json:"needsLLM"`
Presentation string `json:"presentation,omitempty"` // e.g. "day_calendar"
// Scores are the engine's probabilities for the options of the Choice that
// picked Intent, keyed by option id (for a taxonomy of several modules the
// option id is "module/intent"). Empty when the engine provides none.
Scores map[string]float64 `json:"scores,omitempty"`
// Calibrated is true only when Scores and the confidences are calibrated
// probabilities (a real decision model), false for an LLM emulator's
// self-reported confidence. A SelectionPolicy is applied only to
// calibrated scores. Provenance layers a third class (deterministic) over
// this flag.
Calibrated bool `json:"calibrated,omitempty"`
// Outcome is the verdict as a record: a Chain (with or without a
// SelectionPolicy), SelectionPolicy.JudgeDecision and an engine built with a
// policy set it, and it is kept on the wire so traces and telemetry can show it.
// It is INFORMATIONAL: what a remote engine, a stored decision or a provider
// writes here is never trusted. Whether a caller may act is decided by the
// unexported verdict only those judges stamp, which Actionable reads, so a
// decision that came off the wire or out of storage is NOT actionable whatever
// its Outcome says (see Rejudge). A caller that cannot rule out a chain with
// KeepNonSelected, or that calls an engine or provider directly, MUST act only
// on a decision for which Actionable is true.
Outcome Outcome `json:"outcome,omitempty"`
// Model is the model id the engine reported for this decision ("" when it
// reports none). Thresholds only hold for the model they were measured on.
Model string `json:"model,omitempty"`
// contains filtered or unexported fields
}
Decision is a provider's answer.
func Deterministic ¶ added in v0.7.0
Deterministic declares d the product of exact, deterministic logic (a rule match, a lookup) and returns it as such: provenance deterministic, outcome OutcomeDeterministic (actionable), and everything that would claim a model stood behind it cleared (Calibrated, Scores, Model).
This is the ONLY way to make a decision deterministic, and it is deliberately out of reach of anything that merely produces data: the class lives in an unexported field, so it cannot be set by JSON (a cloud or remote response, persisted decisions) nor by an LLM's output, and a Decision copied through compose engines and breakers keeps it. A provider that calls Deterministic asserts, for each answer it returns, that no model produced it: calling it on an LLM's or a remote engine's answer defeats every selection policy and is a bug in that provider. ai/decision/rules.Provider calls it for every matched rule; a product's own deterministic provider (a lookup table, a command parser) may do the same.
A deterministic decision is a value in THIS process: marshalled and read back (a stored row, a trace replay) it is self-reported and not actionable, because the class and the verdict are in unexported fields. See Chain.Rejudge.
A deterministic decision is still validated (Validate) by the chain, and a Chain without a Policy still applies its MinConfidence floor to it, as the caller's explicit bar.
func (Decision) Actionable ¶ added in v0.6.0
Actionable reports whether a caller may act on d. The rule is one and explicit, the same as Selection.Actionable: this package's judges stamped a positive verdict on d (selected, several, accepted, deterministic or floor; see Outcome.Actionable). The exported Outcome is not consulted: it is informational and can be written by JSON or by a provider. An unjudged decision is not actionable: a provider or engine used directly returns one, the zero Decision is one, and so is every decision that was marshalled and read back (a stored or remote decision has lost its verdict and its provenance class; re-judge it with Chain.Rejudge). Chain (with or without a Policy) and every engine built with a policy stamp the verdict.
The verdict is bound to the fields that decide whether to act: module, intent, their confidences, Interaction, InteractionConfidence, Calibrated and the deterministic class. Editing any of them after judging makes the decision not actionable until it is judged again. Other fields (Scores, InteractionScores, Slots, Reference, RequiredScopes, RequiredData, Presentation) are not bound: a product that edits a judged decision's evidence or arguments must re-judge it.
func (Decision) Provenance ¶ added in v0.7.0
func (d Decision) Provenance() Provenance
Provenance returns the class of d. It is deterministic only when a provider declared it through Deterministic; calibrated when Calibrated is set; and self-reported otherwise.
func (*Decision) UnmarshalJSON ¶ added in v0.7.0
UnmarshalJSON decodes d like encoding/json does and then discards the judged state: whatever verdict d held before (decoding INTO a judged value) is cleared, and so is the deterministic class, because the decoded content is new and nothing judged it. The wire form is unchanged; Outcome is read as the informational record it is.
type DeterministicProvider ¶ added in v0.7.0
type DeterministicProvider interface {
Provider
// IsDeterministic reports that every answer is declared with Deterministic.
IsDeterministic() bool
}
DeterministicProvider is implemented by a Provider that answers only by exact logic (ai/decision/rules.Provider does). It lets configuration check the order of a chain: a deterministic provider belongs BEFORE the engines, because a chain that stops at an exhausted allowance or budget never reaches a provider placed after it, and a rule behind a remote engine waits for that engine first (see aiconfig.Build).
type Interaction ¶
type Interaction string
Interaction classifies the user's turn.
const ( InteractionCommand Interaction = "command" InteractionQuestion Interaction = "question" InteractionConfirmation Interaction = "confirmation" InteractionRejection Interaction = "rejection" InteractionCorrection Interaction = "correction" InteractionContinuation Interaction = "continuation" InteractionCancellation Interaction = "cancellation" InteractionUndo Interaction = "undo" InteractionChat Interaction = "chat" // general conversation )
type ModuleSpec ¶
type ModuleSpec struct {
Name string `json:"name"`
Intents []string `json:"intents"`
// Scopes this module's intents may require (defaults to [Name]).
Scopes []string `json:"scopes,omitempty"`
}
ModuleSpec declares a product module and its intents.
type Outcome ¶ added in v0.6.0
type Outcome string
Outcome is a SelectionPolicy's verdict on a scored answer. Callers act on the Outcome, never on a probability compared against a number of their own.
const ( // OutcomeSelected: one clear answer. OutcomeSelected Outcome = "selected" // OutcomeSeveral: more than one candidate is clearly relevant; keep all. OutcomeSeveral Outcome = "several" // OutcomeUncertain: no clear answer; escalate (next rung, or ask a person). OutcomeUncertain Outcome = "uncertain" // OutcomeNone: the engine says none of the candidates fits. OutcomeNone Outcome = "none" // OutcomeUnscored: the engine produced no calibrated probabilities (an LLM // emulator), so no threshold can be applied. The answer is a proposal, never a // "clear winner by margin", and it is NOT actionable. OutcomeUnscored Outcome = "unscored" // OutcomeAccepted: an UNCALIBRATED decision accepted because the caller's // policy explicitly opted in (SelectionPolicy.AcceptUncalibratedAt) and its // self-reported confidence reached that bar. It is actionable, and it is // never a calibrated selection: Decision.Calibrated stays false. OutcomeAccepted Outcome = "accepted" // OutcomeDeterministic: the decision came from exact logic (a rule table; see // Deterministic), so there is no estimate to threshold. It is actionable under // every policy, and no AcceptUncalibratedAt bar applies to it. OutcomeDeterministic Outcome = "deterministic" // OutcomeFloor: a Chain WITHOUT a SelectionPolicy accepted the decision at its // MinConfidence floor, the caller's own explicit bar. It is the explicit // verdict that replaces "actionable because nothing judged it". OutcomeFloor Outcome = "floor" // OutcomeInvalid: the decision contradicts itself (its own intent is not the // top of its own Scores, or is missing from them). It is NOT actionable, and a // chain records it as an invalid answer and falls through. OutcomeInvalid Outcome = "invalid" )
func (Outcome) Actionable ¶ added in v0.6.0
Actionable reports whether an outcome lets a caller act: a calibrated selection (OutcomeSelected, OutcomeSeveral), an explicitly accepted uncalibrated decision (OutcomeAccepted), a deterministic decision (OutcomeDeterministic) or one a policy-less chain accepted at its floor (OutcomeFloor). Uncertain, none, unscored, invalid and the empty outcome (nothing judged it) are not.
type Provenance ¶ added in v0.7.0
type Provenance string
Provenance says what stands behind a Decision's numbers, and so what a caller may do with it. It layers over Decision.Calibrated (which stays, for wire compatibility) and adds the third class that bool could not express.
const ( // ProvenanceCalibrated: the confidences and Scores are calibrated // probabilities from a real decision model (Decision.Calibrated is true). A // SelectionPolicy judges them by probability. ProvenanceCalibrated Provenance = "calibrated" // ProvenanceSelfReported: an LLM emulator's own confidence, which is a // proposal, not a probability. A policy acts on it only through its explicit // opt-in (SelectionPolicy.AcceptUncalibratedAt). The default for every // decision that is neither calibrated nor deterministic, so an engine that // says nothing is treated as the weakest class. ProvenanceSelfReported Provenance = "self_reported" // ProvenanceDeterministic: produced by exact, in-process logic with no model // in the loop (a rule table). It is always actionable (OutcomeDeterministic), // whatever the policy, and no AcceptUncalibratedAt bar applies to it: nothing // was estimated, so there is no confidence to threshold. ProvenanceDeterministic Provenance = "deterministic" )
type Provider ¶
type Provider interface {
Name() string
Decide(ctx context.Context, req Request) (Decision, bool, error)
}
Provider decides or abstains. It returns (d, true, nil) when it decided, (_, false, nil) to abstain, and a non-nil error on failure. Callers treat an error exactly like abstention (after recording it).
type Question ¶ added in v0.6.0
type Question struct {
// ID names the question within a ScoreRequest; answers come back under it.
ID string `json:"id"`
Kind QuestionKind `json:"kind"`
Instructions string `json:"instructions"`
Candidates []Candidate `json:"candidates"`
// NoneID, when it names one of Candidates, is the "none of these" option
// of a KindChoice question: choosing it yields OutcomeNone.
NoneID string `json:"noneId,omitempty"`
}
Question asks an engine to score a closed set of candidates.
type QuestionKind ¶ added in v0.6.0
type QuestionKind string
QuestionKind says how the probabilities of one Question relate to each other.
const ( // KindChoice: exactly one candidate is right; probabilities sum to 1. // "Which ONE of these?" KindChoice QuestionKind = "choice" // KindRelevance: each candidate is judged on its own; probabilities are // independent and need not sum to 1. "Which of these are relevant?" KindRelevance QuestionKind = "relevance" )
type Reference ¶
type Reference struct {
Kind string `json:"kind"` // entity type, e.g. "happening"
Expression string `json:"expression,omitempty"` // "my dentist appointment tomorrow"
// Pronoun is true for "it"/"that"/"this one": resolve from session state.
Pronoun bool `json:"pronoun,omitempty"`
}
Reference is what the user refers to, as an expression to resolve.
type Report ¶ added in v0.6.0
type Report struct {
// Strategy is "single", "fallback", "hedged" or "race" for a combinator,
// and "" for a plain provider.
Strategy string `json:"strategy,omitempty"`
// Engine is the leaf engine that answered ("" when none did).
Engine string `json:"engine,omitempty"`
// Attempts lists each leaf engine tried, in start order.
Attempts []Attempt `json:"attempts"`
// FallbackFired is true when a backup engine ran because the primary
// failed or was unavailable.
FallbackFired bool `json:"fallbackFired,omitempty"`
// HedgeFired is true when a backup engine was started only because the
// primary was slow.
HedgeFired bool `json:"hedgeFired,omitempty"`
// Model is the model id the answering engine reported ("" when it reports
// none). Thresholds are only meaningful for the model they were measured
// on, so every answer says which model produced it.
Model string `json:"model,omitempty"`
}
Report is how a (possibly composite) provider answered one call: which engine produced the answer, by what strategy, and every engine that ran.
type Request ¶
type Request struct {
Product string `json:"product"`
InteractionID string `json:"interactionId,omitempty"`
ClientContext *ai.ClientContext `json:"clientContext,omitempty"`
Text string `json:"text"`
Taxonomy Taxonomy `json:"taxonomy"`
State session.State `json:"state"` // entity refs only, no rendered data
// Recent is a short tail of the transcript for continuations.
Recent []string `json:"recent,omitempty"`
// Context is optional JSON-able context for the engine: names and public
// metadata only, never row data, credentials or user identifiers. It is
// additive and optional.
Context map[string]any `json:"context,omitempty"`
Now time.Time `json:"now"`
TZ string `json:"tz,omitempty"`
}
Request is the input to Decide.
type RetryDelayer ¶ added in v0.6.0
RetryDelayer is implemented by an engine error that carries the delay the engine asked callers to wait before trying again (a Retry-After header).
type ScoreRequest ¶ added in v0.6.0
type ScoreRequest struct {
Product string `json:"product,omitempty"`
// Text is the primary state (for example the user's question).
Text string `json:"text"`
// Context is optional JSON-able context: names and public metadata only,
// never row data or secrets.
Context map[string]any `json:"context,omitempty"`
Questions []Question `json:"questions"`
}
ScoreRequest is the input to ScoredProvider.Score: the state the questions are asked about, and the questions. Several independent questions travel in one request so an engine can answer them in one round trip.
type ScoreResult ¶ added in v0.6.0
type ScoreResult struct {
// Engine is the name of the engine that answered (the leaf engine, not a
// combinator wrapping it).
Engine string `json:"engine"`
// Model is the engine's model id, as the engine reports it.
Model string `json:"model,omitempty"`
Usage Usage `json:"usage"`
Answers map[string]Answer `json:"answers"`
// Report is filled by combinators; see Report.
Report *Report `json:"report,omitempty"`
}
ScoreResult is an engine's answers to a ScoreRequest.
type ScoredProvider ¶ added in v0.6.0
type ScoredProvider interface {
Name() string
Score(ctx context.Context, req ScoreRequest) (ScoreResult, error)
}
ScoredProvider scores candidates. It returns an error on failure; unlike a Provider it has no "abstain": a Choice that picks "none of these" is an answer (see OutcomeNone).
type Selection ¶ added in v0.6.0
type Selection struct {
Outcome Outcome `json:"outcome"`
// Picks are the candidate ids the policy SELECTED, best first: non-empty
// only for a calibrated selected or several verdict (and the decision key
// for an accepted decision). Code that sees len(Picks) > 0 may act on them;
// it never holds a proposal.
Picks []string `json:"picks,omitempty"`
// Proposals are the candidates an uncalibrated engine's self-reported numbers
// point at (OutcomeUnscored): for a relevance answer the candidates at or
// above MinProbability (capped by MaxPicks), for a choice answer its top
// candidate unless that is the NoneID. A proposal is never a selection; a
// caller may use it only where a wrong guess is cheap (for example choosing
// which candidates to examine first, with the full set as the fallback).
// Picks and Proposals are never both set.
Proposals []string `json:"proposals,omitempty"`
// Strong is the subset of Picks at or above the strong threshold
// (relevance answers only).
Strong []string `json:"strong,omitempty"`
// Potential are candidates below the select threshold but worth showing
// (relevance answers only).
Potential []string `json:"potential,omitempty"`
// Reason is a short machine-readable explanation of a non-selected
// outcome, or of a truncation: not_calibrated, no_scores, none_of_these,
// low_confidence, narrow_gap, nothing_above_floor, only_potential,
// truncated_to_max_picks, invalid_policy, accepted_uncalibrated, deterministic_rule,
// side_effect_uncalibrated, interaction_low_confidence, interaction_narrow_gap,
// interaction_not_top, bad_confidence, decision_not_scored, decision_not_top,
// bad_scores.
Reason string `json:"reason,omitempty"`
}
Selection is a policy's verdict on one Answer (or one Decision).
func (Selection) Actionable ¶ added in v0.6.0
Actionable reports whether a caller may act on the verdict: the policy selected a calibrated answer (OutcomeSelected, OutcomeSeveral) or accepted an uncalibrated decision at its explicit bar (OutcomeAccepted). Decision.Actionable applies the same rule.
type SelectionPolicy ¶ added in v0.6.0
type SelectionPolicy struct {
// Name identifies the policy in traces.
Name string `json:"name"`
// KindChoice answers: the engine's own confidence must reach MinConfidence
// (skipped when the engine reports none) and the top candidate must lead
// the runner-up by at least MinGap. A top candidate equal to the
// question's NoneID is OutcomeNone.
MinConfidence float64 `json:"minConfidence"`
MinGap float64 `json:"minGap"`
// KindRelevance answers: a candidate at or above MinProbability is
// selected; at or above StrongProbability it is also "strong" (most
// relevant); at or above PotentialProbability but below MinProbability it
// is "potential" (kept for the trace, used only if validation needs it);
// below that it is dropped.
MinProbability float64 `json:"minProbability"`
StrongProbability float64 `json:"strongProbability"`
PotentialProbability float64 `json:"potentialProbability"`
// MaxPicks caps how many candidates are selected (0 = no cap). When the cap
// bites, the best are kept and the reason says so.
MaxPicks int `json:"maxPicks,omitempty"`
// AcceptUncalibratedAt is the explicit opt-in to acting on an UNCALIBRATED
// decision (an LLM emulator's self-reported confidence; a deterministic rule
// is NOT governed by it, see Deterministic): a Decision whose Module and Intent confidences are both at or above
// it is accepted (OutcomeAccepted, actionable); anything lower is unscored.
// 0 (the zero value) means never: an uncalibrated decision is then only a
// non-actionable proposal. It applies to decisions only; an uncalibrated
// answer to a scored question is always a proposal (see Selection.Proposals).
// DurablePolicy leaves it off; NarrowingPolicy sets it to
// NarrowingAcceptUncalibratedAt. Without this opt-in, a calibrated engine
// that goes down would silently lower the bar from the calibrated threshold
// to an LLM's self-reported number. It never governs a deterministic decision,
// which every policy accepts (OutcomeDeterministic) without comparing any
// confidence: use rules (Deterministic) for what must be certain, not a
// threshold of 1.0 an LLM can also report.
AcceptUncalibratedAt float64 `json:"acceptUncalibratedAt,omitempty"`
// AcceptUncalibratedSideEffects is the separate, explicit opt-in to accepting
// an uncalibrated decision whose Interaction is side-effectful (confirmation,
// rejection, correction, cancellation, undo: see SideEffectful), because a
// wrong "yes" confirms something the user never meant and an LLM's
// self-reported number is a poor guard for that. Without it such a decision
// is unscored (reason side_effect_uncalibrated) however confident it sounds,
// including under AcceptUncalibratedAt. With it, the decision's own
// InteractionConfidence must be above 0 and reach the larger of
// AcceptUncalibratedAt and DurableMinConfidence (an engine that reports no
// interaction confidence is never accepted for these kinds), on top of the
// module and intent bar. False by default for both named policies.
AcceptUncalibratedSideEffects bool `json:"acceptUncalibratedSideEffects,omitempty"`
}
SelectionPolicy turns a scored answer into an Outcome. It replaces scattered per-caller thresholds with one named, documented value, and it never picks "the highest score" blindly: a Choice must be confident AND clear of the runner-up, and an independent relevance list may legitimately select several candidates or none.
A decision has one of three provenances (Decision.Provenance). A policy is applied by probability only to CALIBRATED answers. An uncalibrated answer gets OutcomeUnscored whatever its numbers say, with a PROPOSAL in Selection.Proposals (see Evaluate) that is never a selection. The exceptions are a Decision (EvaluateDecision) under a policy whose AcceptUncalibratedAt is set (that opt-in names the self-reported confidence at which an LLM's decision is accepted: OutcomeAccepted; a side-effectful interaction needs a further, separate opt-in), and a DETERMINISTIC decision (see Deterministic), which every policy accepts as OutcomeDeterministic: exact logic has no estimate to threshold, and AcceptUncalibratedAt never governs it.
There is ONE rule for "may a caller act on this": Selection.Actionable and Decision.Actionable are true for a calibrated selection (selected, several), an explicitly accepted uncalibrated decision (accepted), a deterministic decision (deterministic) and a policy-less chain's floor acceptance (floor), and for nothing else. Decision.Actionable additionally requires that this package's judges stamped the verdict: it is not read from the wire.
Use NarrowingPolicy or DurablePolicy, or build a value and Validate it. The zero value is invalid (it would select everything): Validate rejects it, and Evaluate refuses to select anything with an invalid policy. Chain treats a nil *SelectionPolicy as "no policy" (the legacy MinConfidence behaviour).
func DurablePolicy ¶ added in v0.6.0
func DurablePolicy() SelectionPolicy
DurablePolicy is the stricter policy for answers that will be stored and reused as fact.
func NarrowingPolicy ¶ added in v0.6.0
func NarrowingPolicy() SelectionPolicy
NarrowingPolicy is the default policy for narrowing a set of candidates.
func (SelectionPolicy) AtLeast ¶ added in v0.6.0
func (p SelectionPolicy) AtLeast(o SelectionPolicy) SelectionPolicy
AtLeast returns a policy at least as strict as both p and o: each threshold is the larger of the two, MaxPicks the smaller non-zero cap, and an uncalibrated decision (and an uncalibrated side-effectful one) is accepted only when BOTH accept it (the larger bar, and never when either never does). Its name is p's with "+strict" appended.
func (SelectionPolicy) Evaluate ¶ added in v0.6.0
func (p SelectionPolicy) Evaluate(a Answer) Selection
Evaluate applies the policy to one answer.
An invalid policy (see Validate) selects nothing: the outcome is OutcomeUncertain with ReasonInvalidPolicy. An uncalibrated answer is OutcomeUnscored with ReasonNotCalibrated and a PROPOSAL in Proposals (Picks, Strong and Potential stay empty): a relevance answer proposes the candidates at or above MinProbability (capped by MaxPicks), a choice answer proposes its top candidate unless that is the NoneID. A proposal lets a caller narrow a search space with an LLM engine's self-reported numbers; it is never a selection, and AcceptUncalibratedAt does not apply to it.
func (SelectionPolicy) EvaluateDecision ¶ added in v0.6.0
func (p SelectionPolicy) EvaluateDecision(d Decision) Selection
EvaluateDecision applies the policy to a Decision.
- A decision with a non-finite or out-of-range confidence or interaction probability is OutcomeInvalid (bad_confidence): JudgeDecision cannot take a taxonomy, but it never trusts a number Validate would refuse.
- A deterministic decision (Deterministic) is OutcomeDeterministic, whatever the policy's numbers: exact logic has nothing to threshold.
- A calibrated decision with Scores is judged like a Choice (the policy's confidence and gap thresholds) on the decision's OWN module/intent, never on whichever option tops the scores: a decision whose own option is missing from its Scores, or scores below another option, contradicts itself and is OutcomeInvalid (not actionable; reasons decision_not_scored, decision_not_top, bad_scores).
- A side-effectful interaction (SideEffectful) on a calibrated decision is further gated, because the calibrated flag of a remote engine is only a claim: InteractionScores must back it (the interaction is their top option, clear of the runner-up), the interaction's OWN probability must reach the bar, AND InteractionConfidence must be above 0 and reach the larger of the policy's MinConfidence and DurableMinConfidence. A decision whose InteractionScores contradict its Interaction is OutcomeInvalid (interaction_not_top); one that falls short of the bar is OutcomeUncertain (interaction_low_confidence, interaction_narrow_gap). A calibrated claim with no InteractionScores counts as a self-report, below.
- Any other decision (uncalibrated, calibrated but without Scores, or a calibrated side-effectful claim nothing backs) is OutcomeAccepted when AcceptUncalibratedAt is set and both its Module and Intent confidences reach it (the module confidence is exempt for a module-optional interaction, as in Chain), and OutcomeUnscored, which is not actionable, otherwise. A side-effectful interaction is further refused unless AcceptUncalibratedSideEffects opts in and its InteractionConfidence is above 0 and reaches the larger of AcceptUncalibratedAt and DurableMinConfidence.
An invalid policy selects nothing.
func (SelectionPolicy) JudgeDecision ¶ added in v0.7.0
func (p SelectionPolicy) JudgeDecision(d Decision) (Decision, Selection)
JudgeDecision is EvaluateDecision, plus the verdict stamped on the returned decision: Decision.Outcome is set and Decision.Actionable reports it. It is the judge Chain and compose.WithPolicy use, and the only way outside a Chain to make a decision actionable: the policy has to accept it. A provider that returns a decision never judges it itself; whatever Outcome it writes is ignored.
func (SelectionPolicy) Validate ¶ added in v0.6.0
func (p SelectionPolicy) Validate() error
Validate reports a misconfigured policy: every threshold in [0,1], MinConfidence and MinProbability above zero (so the zero value, which would select everything, is invalid), thresholds ordered potential <= min <= strong, MaxPicks not negative and AcceptUncalibratedAt in [0,1].
type StopPolicy ¶ added in v0.7.0
type StopPolicy uint8
StopPolicy says whether a Chain stops when a provider reports a condition it must not paper over. The zero value STOPS: stopping is the default, and falling through to the next provider is the deliberate, named opt-out.
const ( // StopChain (the zero value) stops the chain: no later provider is called, the // decision is ok=false and Trace.StoppedBy / Trace.Err say why. StopChain StopPolicy = iota // FallThrough lets the chain go on to the next provider, as a plain failure // does. Choosing it for a quota or a budget means a paid provider behind it can // take over the traffic. FallThrough )
func (StopPolicy) MarshalText ¶ added in v0.7.0
func (s StopPolicy) MarshalText() ([]byte, error)
MarshalText writes String, so a policy marshals to JSON as a string.
func (StopPolicy) String ¶ added in v0.7.0
func (s StopPolicy) String() string
String is "stop" or "fall_through" (and "StopPolicy(n)" for anything else).
func (*StopPolicy) UnmarshalText ¶ added in v0.7.0
func (s *StopPolicy) UnmarshalText(b []byte) error
UnmarshalText reads "stop" or "fall_through"; anything else is an error.
type Taxonomy ¶
type Taxonomy struct {
Modules []ModuleSpec `json:"modules"`
Presentations []string `json:"presentations,omitempty"`
DataKinds []string `json:"dataKinds,omitempty"`
EntityTypes []string `json:"entityTypes,omitempty"`
// Descriptions optionally explain taxonomy entries to the engine, keyed by
// entry name: a module name, "module/intent", a presentation, a data kind
// or an entity type. A bare name often scores poorly; one line of public
// description helps. Additive and optional.
Descriptions map[string]string `json:"descriptions,omitempty"`
}
Taxonomy is the product's decision vocabulary, sent with every request so a shared decision service stays product-neutral.
type Trace ¶
type Trace struct {
DecidedBy string `json:"decidedBy,omitempty"` // "" when every provider abstained
Attempts []Attempt `json:"attempts"`
// Engine is the leaf engine that produced the decision (differs from
// DecidedBy when a combinator wrapped it).
Engine string `json:"engine,omitempty"`
// Strategy, FallbackFired and HedgeFired describe the combinator that
// answered, if any (see Report).
Strategy string `json:"strategy,omitempty"`
FallbackFired bool `json:"fallbackFired,omitempty"`
HedgeFired bool `json:"hedgeFired,omitempty"`
// Calibrated, Provenance, Outcome and Model echo the decision's flag, its
// class (calibrated, self_reported or deterministic), the verdict and the
// model id the answering engine reported.
Calibrated bool `json:"calibrated,omitempty"`
Provenance Provenance `json:"provenance,omitempty"`
Outcome Outcome `json:"outcome,omitempty"`
Model string `json:"model,omitempty"`
// StoppedBy is set when the chain stopped early instead of trying its next
// provider: AttemptQuota (an exhausted allowance) or AttemptMisconfigured (an
// endpoint a person has to fix). The decision is then ok=false, like any chain
// that decided nothing, but the product MUST NOT read it as "nobody decided,
// use the paid main-LLM path" without choosing to: Err returns the error.
StoppedBy string `json:"stoppedBy,omitempty"`
}
Trace is the chain's diagnostic record.
func (Trace) Err ¶ added in v0.7.0
Err returns the error a chain that stopped early (Trace.StoppedBy) stands for: it matches (errors.Is) ErrQuota, ErrBudget or ErrMisconfigured and names the provider that said so. It is nil for a trace that was not stopped. The message is the provider's own error text, as the attempt's detail has it, so the condition is stated once.
type TracedProvider ¶ added in v0.6.0
type TracedProvider interface {
Provider
DecideTraced(ctx context.Context, req Request) (Decision, bool, Report, error)
}
TracedProvider is a Provider that also reports how it decided. Chain uses DecideTraced when available so Trace lists every engine tried, not only the outermost wrapper.
type TracedScorer ¶ added in v0.6.0
type TracedScorer interface {
ScoredProvider
ScoreTraced(ctx context.Context, req ScoreRequest) (ScoreResult, Report, error)
}
TracedScorer is a ScoredProvider that also reports how it answered (engine, strategy, attempts, fallbacks), as the compose combinators do.
Directories
¶
| Path | Synopsis |
|---|---|
|
Package compose holds engine combinators over decision.Provider (and decision.ScoredProvider): Single, Fallback, Hedged and Race, plus Breaker, a circuit breaker that stops calling an engine that is down, and Budget, a cap on how often an engine (typically a paid backup) is called.
|
Package compose holds engine combinators over decision.Provider (and decision.ScoredProvider): Single, Fallback, Hedged and Race, plus Breaker, a circuit breaker that stops calling an engine that is down, and Budget, a cap on how often an engine (typically a paid backup) is called. |
|
Package llmdecider is a decision.Provider that makes ONE structured inference against an ai.LLMProvider to produce a decision.Decision.
|
Package llmdecider is a decision.Provider that makes ONE structured inference against an ai.LLMProvider to produce a decision.Decision. |
|
Package rules is a deterministic, table-driven decision.Provider.
|
Package rules is a deterministic, table-driven decision.Provider. |
|
Package typesafe is a client for TypeSafe AI's System One API (the "Jev" decision model) and a decision.Provider / decision.ScoredProvider built on it.
|
Package typesafe is a client for TypeSafe AI's System One API (the "Jev" decision model) and a decision.Provider / decision.ScoredProvider built on it. |