/** * Typed decisions: questions a model answers with a yes or no, a choice, a score, or a box around * something in a picture, each with a confidence read alike from any provider, so software acts on an * answer without parsing text. */ import type { CacheAdapter } from '../cache/adapters.js'; import type { ImageContent } from '../types/messages.js'; import type { OperationLifecycleLike, ProviderCallContext } from '../types/lifecycle.js'; import type { ResponseCost } from '../types/response.js'; import type { RunKind } from '../types/tracing.js'; /** * The kinds of question there are: a yes or no, a choice among options, a score on levels, or a box * around something in a picture. */ export type DecisionQuestionType = 'yes-no' | 'choice' | 'score' | 'box'; /** A question answered yes or no, with the probability that it is yes. */ export interface YesNoQuestion { /** Discriminates the question. */ type: 'yes-no'; /** The question, or a statement that is true or false, such as "The customer asks for a refund." */ question?: string; /** What counts as yes and as no, when the question alone leaves it open. */ criteria?: { yes?: string; no?: string; }; } /** A question answered by one of its options, with each option's probability. */ export interface ChoiceQuestion { /** Discriminates the question. */ type: 'choice'; /** The question, such as "Which team should handle this?". */ question?: string; /** The options, by name, each with what it means, or only their names. Up to 255. */ options: readonly K[] | Readonly>; } /** A question answered on an ordered rubric: level 0 first, with each level's probability. */ export interface ScoreQuestion { /** Discriminates the question. */ type: 'score'; /** The question, such as "How urgent is this?". */ question?: string; /** What each level means, lowest first. Two to ten. */ levels: readonly string[]; } /** A question answered with where something is in a picture of the state: a box around it. */ export interface BoxQuestion { /** Discriminates the question. */ type: 'box'; /** * What to find, and in which picture, by its path in backticks, such as "the dent in `photo`". A box * is given even for something the picture does not show: ask a yes or no beside it whether it does. */ question: string; } /** Any question. */ export type DecisionQuestion = YesNoQuestion | ChoiceQuestion | ScoreQuestion | BoxQuestion; /** Questions by the names their answers come back under. */ export type DecisionQuestions = Record; /** * What every question of a decision is about: text, or any JSON value. An object or a list may hold * pictures, as image parts (`{ type: 'image', source }`) or image assets, anywhere in it; a question * names each by its path in backticks, such as `photo` or `frames[2].frame`. */ export type DecisionState = string | Record | readonly unknown[]; /** * Where something is in a picture: each edge a fraction, from 0 to 1, of the picture's height (`top`, * `bottom`) or width (`left`, `right`), measured from its top-left corner. */ export interface DecisionBox { /** The top edge, from the picture's top. */ top: number; /** The bottom edge, from the picture's top: at least `top`. */ bottom: number; /** The left edge, from the picture's left. */ left: number; /** The right edge, from the picture's left: at least `left`. */ right: number; } /** A yes-or-no question's answer. */ export interface YesNoAnswer { /** Discriminates the answer. */ type: 'yes-no'; /** Whether it is yes: the probability is at least one half. */ yes: boolean; /** The probability that it is yes, from 0 to 1. */ probability: number; /** How far the probability is from an even chance, from 0 to 1: `|2p - 1|`. */ confidence: number; } /** A choice question's answer. */ export interface ChoiceAnswer { /** Discriminates the answer. */ type: 'choice'; /** The most probable option; of equally probable ones, the first given. */ choice: K; /** Every option's probability, summing to 1. */ probabilities: Record; /** * How far the top probability is above an even split, from 0 to 1: `(pmax - 1/n) / (1 - 1/n)` of * `n` options. */ confidence: number; } /** A score question's answer. */ export interface ScoreAnswer { /** Discriminates the answer. */ type: 'score'; /** The probability-weighted level, which may fall between two. */ score: number; /** The most probable level; of equally probable ones, the lowest. */ level: number; /** Each level's probability, lowest first, summing to 1. */ probabilities: number[]; /** * How close the probability sits around the most probable level, from 0 to 1: one less the * probability-weighted distance from it, over that distance for an even spread, and 0 at an even * spread or wider. */ confidence: number; } /** A box question's answer. */ export interface BoxAnswer { /** Discriminates the answer. */ type: 'box'; /** Where it is. */ box: DecisionBox; /** * How sure the answer is, from 0 to 1: the provider's own, such as TypeSafe AI's Jev gives, or how * far several readings of the box agree, for a model asked more than once. */ confidence: number; } /** Any answer. */ export type DecisionAnswer = YesNoAnswer | ChoiceAnswer | ScoreAnswer | BoxAnswer; /** The options of a choice question, as a union of their names. */ type OptionsOf = O extends readonly (infer K)[] ? K : keyof O; /** The answer a question gets: a choice's answer names only its options. */ export type AnswerOf = Q extends YesNoQuestion ? YesNoAnswer : Q extends { type: 'choice'; options: infer O; } ? ChoiceAnswer, string>> : Q extends ScoreQuestion ? ScoreAnswer : Q extends BoxQuestion ? BoxAnswer : DecisionAnswer; /** Answers by their questions' names, each typed by its question. */ export type DecisionAnswers = { [K in keyof Q]: AnswerOf; }; /** A decision: questions about one state, answered together and each in isolation. */ export interface DecisionRequest { /** What every question is about. */ state: DecisionState; /** The questions, by the names their answers come back under. */ questions: Q; /** The provider to ask. Defaults to the manager's default, then the first registered. */ provider?: string; /** The model to ask. Defaults to the provider's own. */ model?: string; /** The tenant it is for: its credentials, regions, and budget apply. */ tenantId?: string; /** Stops the call. */ signal?: AbortSignal; /** Labels for the trace and the audit log. Never include the state's private data. */ metadata?: Record; /** `false` asks the provider even when the cache holds an answer. */ cache?: boolean; } /** What a decision used, where the provider reports it. */ export interface DecisionUsage { /** Input tokens. */ inputTokens?: number; /** Output tokens. */ outputTokens?: number; /** Model calls made, for a provider that asks a completion model. */ calls?: number; } /** A decision's answers, and who gave them. */ export interface DecisionResult { /** The answers, by question name. */ answers: DecisionAnswers; /** The provider that answered. */ provider: string; /** The model that answered, as the provider names it. */ model: string; /** What it used. */ usage?: DecisionUsage; /** What it cost, when the provider or the manager's prices price it. */ cost?: ResponseCost; /** How long it took, in milliseconds. */ latencyMs: number; /** True when the answers came from the cache. */ cached?: boolean; } /** A question as a provider receives it: a choice's options in order, each with its description. */ export type DecisionProviderQuestion = YesNoQuestion | { type: 'choice'; question?: string; options: Array<{ name: string; description?: string; }>; } | ScoreQuestion | BoxQuestion; /** What a provider is asked. */ export interface DecisionProviderRequest { /** The model to ask. */ model: string; /** What every question is about, with any pictures it holds, which `statePictures()` lists. */ state: DecisionState; /** The questions, by name. */ questions: Record; } /** A provider's probabilities for one question, from which the answer and its confidence are read. */ export type DecisionDistribution = { type: 'yes-no'; yes: number; } | { type: 'choice'; probabilities: Record; } | { type: 'score'; probabilities: readonly number[]; } | { type: 'box'; box: DecisionBox; confidence: number; }; /** What a provider answers. */ export interface DecisionProviderResult { /** The model that answered, when it differs from the one asked, as an alias resolves. */ model?: string; /** Each question's probabilities, by name. */ distributions: Record; /** What it used. */ usage?: DecisionUsage; /** What it cost, when the provider reports it. */ cost?: ResponseCost; } /** What a decision provider takes. */ export interface DecisionProviderCapabilities { /** The kinds of question it answers. */ types: readonly DecisionQuestionType[]; /** Most options a choice may have. */ maxOptions: number; /** Most levels a score may have. */ maxLevels: number; /** Most questions one decision may ask, when it has a limit. */ maxQuestions?: number; /** Whether it reads the pictures a state holds. A provider without it is not asked about a picture. */ images?: boolean; /** The model asked when a request names none. */ defaultModel: string; /** The models it serves, when it lists them. */ models?: readonly string[]; } /** Identifies a decision provider and what it takes. */ export interface DecisionProviderInfo { /** Its name, which results carry. */ name: string; /** What it takes. */ capabilities: DecisionProviderCapabilities; /** * True for a provider whose calls are charged where they are made, such as one asking a client's * own completions: its decisions report their cost, and hold and charge nothing to the budget again. */ chargedByItsCalls?: boolean; } /** What every decision call receives: how to stop, the request's id, and the tenant's credential. */ export interface DecisionCallContext { /** Stops the call. */ signal: AbortSignal; /** The request's id, for logs and traces. */ requestId: string; /** The tenant it is for. */ tenantId?: string; /** * The tenant's credential for a provider, with a credential resolver: an adapter sends it in place * of the key it was configured with. */ credential?: ProviderCallContext['credential']; } /** A backend that answers decisions: a decision model, a completion model, or a stand-in. */ export interface DecisionProvider { /** What it is and takes. */ readonly info: DecisionProviderInfo; /** Answers every question of a decision. */ decide(request: DecisionProviderRequest, context: DecisionCallContext): Promise; } export { DecisionError, type DecisionErrorCode } from './errors.js'; /** A yes-or-no answer's confidence: how far its probability is from an even chance, `|2p - 1|`. */ export declare function yesNoConfidence(probability: number): number; /** * A choice's confidence, from its options' probabilities: how far the top one is above an even split, * `(pmax - 1/n) / (1 - 1/n)`. One option is certain. */ export declare function choiceConfidence(probabilities: readonly number[]): number; /** * A score's confidence, from its levels' probabilities: one less the probability-weighted distance * from the most probable level, over that distance for an even spread, and 0 at an even spread or * wider. One level is certain. */ export declare function scoreConfidence(probabilities: readonly number[]): number; /** * An answer from a provider's probabilities for one question: normalized to sum to 1, with its * confidence read the same way whichever provider gave them. Throws `DecisionError` * `DECISION_INVALID` for probabilities that do not fit the question. */ export declare function decisionAnswer(question: DecisionProviderQuestion, distribution: DecisionDistribution): DecisionAnswer; /** * How far two boxes overlap, from 0 to 1: the area they share over the area they cover together, the * intersection over union by which a box is judged against where the thing is. */ export declare function boxOverlap(a: DecisionBox, b: DecisionBox): number; /** What to do with an answer: act on it, have a person review it, or neither. */ export type GateOutcome = 'act' | 'review' | 'abstain'; /** The confidences a gate acts at, and has reviewed at. */ export interface GateThresholds { /** At or above this confidence, act. */ act: number; /** At or above this, and below `act`, a person reviews it; below it, abstain. Defaults to 0. */ review?: number; } /** * Turns an answer's confidence into what to do: act at or above `act`, have a person review it at or * above `review`, and abstain below. Set the thresholds by risk, and choose them with `calibrate()` * on the application's own labelled examples. */ export declare function gate(answer: { confidence: number; }, thresholds: GateThresholds): GateOutcome; /** One answer whose right answer is known, and whether it was right. */ export interface CalibrationSample { /** The answer. */ answer: DecisionAnswer; /** Whether it was right, as `answeredRight()` reads it. */ correct: boolean; } /** Answers that gave their own answer a probability in one range, and how often they were right. */ export interface CalibrationBin { /** The range's lower bound, included. */ from: number; /** Its upper bound, excluded, except for the last range's 1. */ to: number; /** How many answers fell in it. */ count: number; /** The mean probability they gave their own answers. */ probability: number; /** The share of them that were right. */ accuracy: number; } /** Options for `calibrate()`. */ export interface CalibrateOptions { /** How many equal ranges of probability to count in. Defaults to 10. */ bins?: number; /** The accuracy the threshold must hold: the share of answers at or above it that are right. */ targetAccuracy?: number; /** The fewest answers the threshold must keep, so one lucky answer does not choose it. Defaults to 10. */ minSamples?: number; } /** How well answers' probabilities tracked how often they were right, and the threshold that holds the target. */ export interface CalibrationReport { /** How many answers were counted. */ samples: number; /** The share of all of them that were right. */ accuracy: number; /** * The expected calibration error: the bins' gaps between the probability answers gave themselves and * how often they were right, weighted by their counts. 0 when answers are as often right as they say. */ expectedCalibrationError: number; /** The answers by range of probability, empty ranges left out. */ bins: CalibrationBin[]; /** * The lowest confidence at which the answers at or above it reach `targetAccuracy`, keeping at least * `minSamples`: the `act` threshold for a gate. Absent when no threshold reaches it. */ threshold?: number; /** The share of answers at or above `threshold`: how much a gate there acts on. */ coverage?: number; } /** * The probability an answer gives its own answer: a yes or no's `max(p, 1 - p)`, a choice's for its * option, and a score's for its level. Its confidence measures how far that is above an even spread. * A box has no probabilities, so its confidence stands for one. */ export declare function answerProbability(answer: DecisionAnswer): number; /** * Measures how well answers' probabilities track how often they are right, and, with * `targetAccuracy`, chooses the gate threshold that holds it. Answers are calibrated when those that * give their answer 0.8 are right about 80% of the time. The threshold is on confidence, which gates * act on: it holds on answers like these, not on a different kind of question or state. */ export declare function calibrate(samples: readonly CalibrationSample[], options?: CalibrateOptions): CalibrationReport; /** * Whether an answer is right: a yes-or-no or a choice by its answer, a score by its most probable * level, or by its weighted score within `tolerance` levels when one is given, and a box by how far it * overlaps the expected one, `boxOverlap()`, at least `overlap` (one half by default). */ export declare function answeredRight(answer: DecisionAnswer, expected: boolean | string | number | DecisionBox, options?: { tolerance?: number; overlap?: number; }): boolean; /** A decision model's price, in US dollars per million tokens. */ export interface DecisionPrice { /** Per million input tokens. */ inputPerMillion: number; /** Per million output tokens. Defaults to 0. */ outputPerMillion?: number; } /** * Bundled decision prices, by model name or a prefix of it: TypeSafe AI's Jev at its published * price, $42 per billion input tokens, with output free, as of 2026-10-24. */ export declare const DECISION_PRICES: Readonly>; /** * What a decision cost, from its usage and the model's price; `undefined` for a model without a price * or usage without tokens, never zero. */ export declare function priceDecision(model: string, usage: DecisionUsage | undefined, prices?: Readonly>): ResponseCost | undefined; /** A decision's run in a trace, as decisions finish it. */ export interface DecisionRunLike { /** Finishes the run with its answers, usage, and cost, or its error. */ finish(options?: { outputs?: unknown; error?: unknown; usage?: Record; cost?: number; model?: string; provider?: string; }): Promise; } /** A tracer, or the part of one decisions use: a run per decision, with the provider's calls inside it. */ export interface DecisionTracerLike { /** Starts a run. */ startRun(options: { name: string; kind?: RunKind; inputs?: unknown; metadata?: Record; model?: string; provider?: string; }): DecisionRunLike; /** Runs `fn` with the run in scope, so a model call inside it, through a traced client, nests beneath. */ within(handle: DecisionRunLike, fn: () => T): T; } /** Configuration for `DecisionManager`. */ export interface DecisionConfig { /** Providers, by name. */ providers?: Record; /** The provider a request that names none goes to. Otherwise the first registered. */ defaultProvider?: string; /** * Providers to ask, in order, when the one asked fails in a way another may not: a timeout, a * refused connection, or an overloaded service. A malformed request fails at once. */ fallbacks?: readonly string[]; /** Prices by model or prefix, beside the bundled ones. */ prices?: Readonly>; /** * Keeps each decision's answers, so the same questions about the same state are answered once: a * `CacheAdapter`, such as the memory, Redis, or SQLite one, and how long they keep (a day). */ cache?: { store: CacheAdapter; ttlSeconds?: number; }; } /** A decision's answers as the cache keeps them. */ export interface CachedDecision { /** The answers, by question name. */ answers: Record; /** The provider that answered. */ provider: string; /** The model that answered. */ model: string; /** What it used. */ usage?: DecisionUsage; } /** What the manager runs on. */ export interface DecisionRuntime { /** * Runs every decision as one operation of a client's lifecycle, family `decision`: pass * `ai.lifecycle`, so it is authorized, budgeted, rate limited, and audited, on the tenant's key. */ lifecycle?: OperationLifecycleLike; /** Records each decision as a run of kind `decision`, with its answers, usage, and cost. */ tracer?: DecisionTracerLike; } /** * Decisions behind one handle: each goes to the provider named, the default, or the first * registered, with failover to `fallbacks`, a cache, and every call inside a client's lifecycle. * * ```ts * const decisions = new DecisionManager( * { providers: { jev: new TypeSafeDecisionProvider({ apiKey }) }, fallbacks: ['gpt'] }, * { lifecycle: ai.lifecycle }, * ); * const { answers } = await decisions.decide({ * state: ticket, * questions: { * team: { type: 'choice', options: { billing: 'Charges and invoices', technical: 'Errors and outages' } }, * urgent: { type: 'yes-no', question: 'Does the customer need an answer today?' }, * }, * }); * if (gate(answers.team, { act: 0.8 }) === 'act') route(answers.team.choice); * ``` */ export declare class DecisionManager { private readonly config; private readonly runtime; private readonly providers; constructor(config?: DecisionConfig, runtime?: DecisionRuntime); /** Registers a provider under a name. Returns the manager, for chaining. */ registerDecisionProvider(name: string, provider: DecisionProvider): this; /** Whether a provider is registered. */ hasDecisionProvider(name: string): boolean; /** Every registered provider's name. */ listDecisionProviders(): string[]; /** * Answers every question of a decision, each typed by its question: a choice's answer names only * its options. Throws `DecisionError` when the request is malformed or no provider can answer. */ decide(request: DecisionRequest): Promise>; private ask; private prices; } /** Answers a decision with one provider, outside any manager: no failover, cache, or lifecycle. */ export declare function decide(provider: DecisionProvider, request: Omit, 'provider'>): Promise>; /** A question whose right answer is known, for `runDecisionConformance()`. */ export interface DecisionConformanceCase { /** What the question is about. */ state: DecisionState; /** The question. */ question: DecisionQuestion; /** * The right answer: a yes or no, a choice's option, a score's level, or where a box should be, which * an answer overlapping it by half or more matches. */ expected: boolean | string | number | DecisionBox; } /** Options for `runDecisionConformance()`. */ export interface DecisionConformanceOptions { /** The model to ask. Defaults to the provider's own. */ model?: string; /** * Questions whose answers are known, each of which must be answered right. A stand-in that answers * as it is told passes without them; a real model is held to them. */ cases?: readonly DecisionConformanceCase[]; /** * A picture for the checks of a provider that reads pictures or answers boxes: it is asked a yes or * no about the picture, and a box around what it shows. Without one, those checks are skipped. */ picture?: ImageContent; } /** How a provider did against the decision contract. */ export interface DecisionConformanceReport { /** The provider. */ provider: string; /** Checks that passed. */ passed: string[]; /** Checks that failed, each with why. */ failures: string[]; /** Checks that were not run, each with why, such as a box with no picture given to ask about. */ skipped: string[]; } /** * Checks a provider against the decision contract: its capabilities are coherent; each question type * it declares comes back as probabilities, or a box, that fit the question; a provider that reads * pictures answers about one; several questions in one decision each get their own answer; a stopped * call is not answered; and every case is answered right. Never throws; failures are listed. */ export declare function runDecisionConformance(provider: DecisionProvider, options?: DecisionConformanceOptions): Promise;