decisions

package
v0.0.11 Latest Latest
Warning

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

Go to latest
Published: Sep 24, 2026 License: MIT Imports: 26 Imported by: 0

Documentation

Overview

Package decisions is kb's decision engine: one entry point (Engine.Decide) that owns transport to the decision model, receipt validation, the per-topic receipts cache, banding, budget, concurrency and run summaries.

Index

Constants

View Source
const (
	// DefaultAPIURL is OpenRouter's API base; the engine posts to
	// <api_url>/alpha/decisions.
	DefaultAPIURL = "https://openrouter.ai/api"

	// MaxQuestionsPerRequest is the batch size: larger question sets are split
	// into several requests over the same state.
	MaxQuestionsPerRequest = 48

	// MaxStateTokens caps the estimated tokens of one state. The estimate is
	// bytes/4 of the canonical JSON (about four bytes per token for English
	// text and JSON punctuation); it only matters when max_state_bytes is
	// raised above 120 KB, and it keeps the state plus the longest question
	// under the model's 32k-token limit.
	MaxStateTokens = 30000
)
View Source
const (
	RouteDecisions       = "openrouter:alpha/decisions"
	RouteChatCompletions = "openrouter:v1/chat/completions"
)

Routes recorded in receipts.

View Source
const (
	ReasonTimeout         = "timeout"
	ReasonRetries         = "retries"
	ReasonBudget          = "budget"
	ReasonInvalidReceipt  = "invalid_receipt"
	ReasonStateTooLarge   = "state_too_large"
	ReasonHTTPStatus      = "http_status"
	ReasonMissingAnswer   = "missing_answer"
	ReasonExcluded        = "excluded"
	ReasonContextCanceled = "canceled"
)

Undecided / not-checked reasons.

View Source
const ReceiptsDir = ".decisions"

ReceiptsDir is the per-topic directory holding decision records.

View Source
const ReceiptsFile = "receipts.jsonl"

ReceiptsFile is the append-only receipts log inside ReceiptsDir.

View Source
const Redacted = "[REDACTED]"

Redacted replaces every secret removed by Redact.

Variables

View Source
var (
	// ErrNotConfigured is returned by New when no OpenRouter API key is set.
	ErrNotConfigured = errors.New("decisions: OPENROUTER_API_KEY is not set; the decision model is required ([decisions] + [openrouter])")

	// ErrAuth marks a 401/402/403 from the decision endpoint. After the first
	// one, every later Decide returns it immediately.
	ErrAuth = errors.New("decisions: the decision endpoint refused the key or credits")

	// ErrInvalidRequest marks a request the engine cannot send: no or
	// duplicate question ids, malformed criteria, or a state that is not
	// JSON-serializable. It is a caller bug, not a model outcome.
	ErrInvalidRequest = errors.New("decisions: invalid request")
)
View Source
var OwnedKeys = []string{
	"triage", "triage_reason", "genre", "depth", "relevance", "concepts",
	"summary", "criterion", "entities", "questions", "quality",
	"ingest_batch", "ingest_query",
	"related", "extends", "prerequisite", "example_of", "contradicts",
	"affects", "supersedes", "aliases", "recaptured",
}

OwnedKeys are the frontmatter keys kb writes (spec §6). The writer, ingest's extra-frontmatter guard and lint all use this list. `locked` is user-owned and never written.

View Source
var RelationKeys = []string{"related", "extends", "prerequisite", "example_of", "contradicts"}

RelationKeys are the typed relation lists written by link.

Functions

func CanonicalJSON

func CanonicalJSON(v any) ([]byte, error)

CanonicalJSON encodes v as JSON with object keys sorted, numbers kept as written, no HTML escaping and no trailing newline, so equal values always hash equally. Cache keys and value hashes are computed over it.

func HashJSON

func HashJSON(v any) (string, error)

HashJSON returns the hex sha256 of CanonicalJSON(v).

func IsOwnedKey

func IsOwnedKey(key string) bool

IsOwnedKey reports whether key is kb-owned.

func ParseCost

func ParseCost(raw json.RawMessage) *float64

ParseCost reads a reported cost (a JSON number or numeric string). A missing, null, negative or non-numeric cost is unknown (nil), never zero.

func ReceiptsPath

func ReceiptsPath(topicRoot string) string

ReceiptsPath returns the receipts log path of a topic.

func Redact

func Redact(v any) any

Redact returns a copy of v, as a generic JSON tree, with obvious secrets replaced by Redacted in every string value (API keys, bearer tokens, PEM private keys, `api_key=...` assignments) and in the string values of secret-looking keys (password, token, api_key, ...). A string in returns a string. A value that cannot be encoded as JSON is returned unchanged; the engine then rejects it as an invalid request.

func RedactString

func RedactString(s string) string

RedactString replaces obvious secrets in s with Redacted.

func SleepContext

func SleepContext(ctx context.Context, d time.Duration) error

SleepContext waits for d or until ctx ends, returning ctx.Err() then.

Types

type Answer

type Answer struct {
	Type       string             `json:"type"`
	Status     Status             `json:"status"`
	Reason     string             `json:"reason,omitempty"`
	Band       Band               `json:"band,omitempty"`
	Noul       *float64           `json:"noul,omitempty"`
	Choice     string             `json:"choice,omitempty"`
	Score      *float64           `json:"score,omitempty"`
	Probs      map[string]float64 `json:"probabilities,omitempty"`
	Confidence *float64           `json:"confidence,omitempty"`
	// Receipt is the receipts key of the request that produced the answer
	// (requests over 48 questions are split, so answers of one Result may
	// come from different receipts).
	Receipt string `json:"receipt,omitempty"`
}

Answer is one banded answer. Noul/Score/Confidence are pointers because absence differs from zero.

func (Answer) Decided

func (a Answer) Decided() bool

Decided reports whether the answer carries a usable judgment.

func (Answer) P

func (a Answer) P(option string) (float64, bool)

P returns P(yes) for a decided noul, the probability of option for a decided choice, or 0 with ok=false.

type Band

type Band string

Band is the confidence band of a decided answer.

const (
	BandApply  Band = "apply"
	BandReview Band = "review"
	BandIgnore Band = "ignore"
)

Confidence bands.

func BandFor

func BandFor(value, apply, review float64) Band

BandFor bands a named gate quantity: value ≥ apply → apply, ≥ review → review, else ignore.

type Budget

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

Budget is a per-run spend ceiling in US$, shared by the decision engine and the generation client. It is safe for concurrent use. A nil *Budget is unlimited.

func NewBudget

func NewBudget(limitUSD float64) *Budget

NewBudget returns a budget with the given ceiling. A ceiling of 0 allows no new calls, which makes a cache-only run.

func (*Budget) Charge

func (b *Budget) Charge(cost float64)

Charge records a cost. Negative, NaN and infinite costs are ignored.

func (*Budget) Exceeded

func (b *Budget) Exceeded() bool

Exceeded reports whether no new call may start.

func (*Budget) Limit

func (b *Budget) Limit() float64

Limit returns the ceiling.

func (*Budget) Remaining

func (b *Budget) Remaining() float64

Remaining returns the ceiling minus what was spent, never below zero.

func (*Budget) Spent

func (b *Budget) Spent() float64

Spent returns what has been charged so far.

type Engine

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

Engine asks the decision model typed questions and returns banded answers. It is safe for concurrent use.

func New

func New(opts Options) (*Engine, error)

New builds an Engine. It returns ErrNotConfigured when opts.APIKey is empty and a validation error when opts.Config is unusable.

func (*Engine) Budget

func (e *Engine) Budget() *Budget

Budget returns the run budget shared with generation.

func (*Engine) Concurrency

func (e *Engine) Concurrency() int

Concurrency returns the number of requests the engine runs at once, so callers can size their fan-out.

func (*Engine) Decide

func (e *Engine) Decide(ctx context.Context, r Request) (Result, error)

Decide asks every question of r against r.State and returns one banded answer per question id. It returns an error only for fatal conditions: ErrAuth (401/402/403, sticky for the engine), ctx cancellation, and ErrInvalidRequest. Every other failure is a per-answer status: undecided (timeout, retries, budget, invalid_receipt, http_status, missing_answer) or not_checked (state_too_large, or excluded: a Subject matching r.Topic.Exclude is answered without any call).

func (*Engine) Model

func (e *Engine) Model() string

Model returns the pinned decision model slug.

func (*Engine) Receipts

func (e *Engine) Receipts() *Receipts

Receipts returns the receipts store shared with generation.

func (*Engine) Summary

func (e *Engine) Summary() Summary

Summary returns a snapshot of the run summary (Generation left empty).

type GenerationSummary

type GenerationSummary struct {
	Calls       int
	CacheHits   int
	Fallbacks   int
	Failures    int
	CostUSD     float64
	CostUnknown int
}

GenerationSummary is the generation client's contribution to a run.

type Options

type Options struct {
	Config     config.DecisionsConfig
	APIKey     string
	APIURL     string
	HTTPClient *http.Client
	// Budget is the run budget; nil creates one from Config.BudgetUSD.
	Budget *Budget
	// Receipts is the receipts store; nil creates one. Share it with the
	// generation client through Engine.Receipts.
	Receipts *Receipts
	Now      func() time.Time
	// Sleep waits between retries; it must return ctx.Err() when ctx ends.
	Sleep  func(ctx context.Context, d time.Duration) error
	Logger *slog.Logger
}

Options configure an Engine. Only Config and APIKey are required.

type Purpose

type Purpose string

Purpose names why a decision is asked; it selects thresholds and groups the run summary.

const (
	PurposeRelevance     Purpose = "relevance"
	PurposeQuality       Purpose = "quality"
	PurposeDuplicate     Purpose = "duplicate"
	PurposeClassify      Purpose = "classify"
	PurposeConcept       Purpose = "concept"
	PurposeLink          Purpose = "link"
	PurposeMention       Purpose = "mention"
	PurposeFind          Purpose = "find"
	PurposeOKFType       Purpose = "okf_type"
	PurposeContractCheck Purpose = "contract_check"
)

Purposes asked by kb.

type PurposeSummary

type PurposeSummary struct {
	Decided    int
	Bands      map[Band]int
	Undecided  map[string]int // by reason
	NotChecked map[string]int // by reason
}

PurposeSummary counts the answers of one purpose.

type Receipt

type Receipt struct {
	Key           string                     `json:"key"`
	Time          string                     `json:"time"`
	Purpose       string                     `json:"purpose"`
	Subject       string                     `json:"subject"`
	Bank          string                     `json:"bank"`
	BankVersion   string                     `json:"bank_version"`
	BankHash      string                     `json:"bank_hash"`
	Contract      string                     `json:"contract"`
	Model         string                     `json:"model"`
	ReportedModel string                     `json:"reported_model"`
	Route         string                     `json:"route"`
	ResponseID    string                     `json:"response_id,omitempty"`
	Attempts      int                        `json:"attempts"`
	LatencyMS     int64                      `json:"latency_ms"`
	InputTokens   *int64                     `json:"input_tokens"`
	Cost          *float64                   `json:"cost"`
	CostUnknown   bool                       `json:"cost_unknown"`
	Status        Status                     `json:"status"`
	Reason        string                     `json:"reason,omitempty"`
	Questions     []string                   `json:"questions"`
	Answers       map[string]json.RawMessage `json:"answers"`
}

Receipt is one row of <topic>/.decisions/receipts.jsonl: one network call (one decision request, or one generation request). Answers hold the raw answers as returned by the model, never bands: bands are recomputed from the current thresholds on every read. Generation rows use purpose "generate:<kind>" and answers {"output": <json>}.

func LoadReceipts

func LoadReceipts(topicRoot string) ([]Receipt, error)

LoadReceipts reads every well-formed row of a topic's receipts log in file order. A missing log yields no rows; malformed lines (for example a line cut by a crash) are skipped. Readers that need one row per key take the last one.

type Receipts

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

Receipts is the per-topic receipts log plus its in-memory cache. The engine and the generation client share one instance (Engine.Receipts) so appends to a topic's log are serialized. Each topic's log is loaded lazily, once; only rows with status decided serve as cache (last one per key wins). A topic with an empty root is cached in memory only.

func NewReceipts

func NewReceipts() *Receipts

NewReceipts returns an empty store.

func (*Receipts) Append

func (s *Receipts) Append(topicRoot string, row Receipt) error

Append writes row as one whole JSON line to the topic's log and, when the row is decided, makes it the cached row for its key.

func (*Receipts) Lookup

func (s *Receipts) Lookup(topicRoot, key string) (Receipt, bool, error)

Lookup returns a copy of the cached decided row for key.

type Request

type Request struct {
	Topic     TopicRef
	Purpose   Purpose
	Subject   string
	Bank      *questions.Bank
	State     any
	Questions []questions.Q
}

Request is one decision request: one state, many questions.

type Result

type Result struct {
	Answers   map[string]Answer
	ReceiptID string
	CostUSD   float64
	CacheHit  bool
}

Result carries the answers of one request, keyed by question id.

type Status

type Status string

Status is the outcome of one answer. Failure is never a "no".

const (
	StatusDecided    Status = "decided"
	StatusUndecided  Status = "undecided"
	StatusNotChecked Status = "not_checked"
)

Answer statuses.

type Summary

type Summary struct {
	// Calls counts decision requests that reached the network.
	Calls int
	// CacheHits counts decision requests served from receipts.
	CacheHits int
	// CostUSD sums the reported cost of decision calls.
	CostUSD float64
	// CostUnknown counts decision calls with no reported cost (failed calls
	// included); their spend is never assumed to be zero.
	CostUnknown int
	// Purposes holds per-purpose answer counts.
	Purposes map[Purpose]PurposeSummary
	// Generation is the generation client's part of the run.
	Generation GenerationSummary
}

Summary is the run summary every command prints: calls, cache hits, cost, and per-purpose outcomes. Engine.Summary fills everything but Generation; callers that also generate set Generation from generation.Client.Summary before printing Lines.

func (Summary) Lines

func (s Summary) Lines() []string

Lines renders the summary as printable lines, for example

decisions: 12 calls, 3 cache hits, US$ 0.0041
relevance: 10 decided (apply 2, review 3, ignore 5), 1 undecided:timeout

type Thresholds

type Thresholds map[string]float64

Thresholds maps named thresholds (link_apply, relevance_quarantine, ...) to values. Missing names fall back to DefaultThresholds.

func DefaultThresholds

func DefaultThresholds() Thresholds

DefaultThresholds are the measured defaults of spec §12.3.

func (Thresholds) For

func (t Thresholds) For(purpose Purpose) (apply, review float64)

For returns the (apply, review) pair the engine bands a purpose's answers with. Noul answers are banded on P(yes); choice and score answers on confidence. Callers that gate on another quantity (P(off_topic), the sum of kept roles, the primary-concept probability, ...) call BandFor with the named threshold they need.

relevance       relevance_quarantine / relevance_review
quality         quality_apply / quality_review
duplicate       duplicate / duplicate
classify        0.5 / 0.5 (facets are written from the answer itself)
concept         concept_noul / primary_concept
link            link_apply / link_review
mention         mention_sense / link_review
find            find_keep / find_keep
okf_type        okf_type / 0.5
contract_check  contract_conflict / contract_conflict
anything else   0.8 / 0.5

func (Thresholds) Get

func (t Thresholds) Get(name string) float64

Get returns the named threshold, falling back to the default.

func (Thresholds) Merge

func (t Thresholds) Merge(override map[string]float64) Thresholds

Merge returns a copy of t with every entry of override applied.

type TopicRef

type TopicRef struct {
	Slug       string
	Root       string
	Contract   string
	Thresholds Thresholds
	// Exclude are the topic.yaml `decisions.exclude` globs (topic-relative,
	// `**`-aware): a request whose Subject matches one is never sent to any
	// model (spec §14).
	Exclude []string
}

TopicRef identifies the topic a decision belongs to: receipts live under Root/.decisions/, and Contract (hash) enters every cache key.

func (TopicRef) Excluded

func (t TopicRef) Excluded(subject string) bool

Excluded reports whether subject, a topic-relative path, matches one of the topic's `decisions.exclude` globs. Subjects that are not paths (query ids, batch ids) never match a valid glob.

Jump to

Keyboard shortcuts

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