decisions

package
v0.2.0-rc.1 Latest Latest
Warning

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

Go to latest
Published: Nov 11, 2026 License: MIT Imports: 21 Imported by: 0

Documentation

Overview

Package decisions extracts the decisions made in an agent session: the choices between plausible alternatives that land in the repository.

It holds what every host shares: the decision schema, the cleaned transcript a model reads (Render), the strict reply schema and parser, and the citation validator that checks a reply against the transcript (Extractor). A host supplies its own system prompt to NewExtractor, or uses the published one (NewPublicExtractor), and its own Model. See ARCHITECTURE.md, "decisions".

Index

Constants

View Source
const CustomVersionPrefix = PublicPromptName + "+custom"

CustomVersionPrefix starts the PromptVersion of an extraction under a prompt other than the published one; "-" and the prompt's hash follow it.

View Source
const ExtractSchemaName = "session_decisions"

ExtractSchemaName names ExtractSchema in ModelRequest.SchemaName.

View Source
const FramingTokens = 1024

FramingTokens allows for what a request adds around its text: message and role framing, and the tool definition Anthropic wraps the schema in.

View Source
const PublicPromptName = "decisions-public-v1"

PublicPromptName names the published extraction prompt.

Variables

View Source
var ErrTranscriptTooLarge = errors.New("transcript exceeds the input token cap")

ErrTranscriptTooLarge reports a transcript over ExtractOptions.MaxInputTokens.

Functions

func ContentHash

func ContentHash(t *Transcript) string

ContentHash is the SHA-256, in hex, of t's Text: the content a cached extraction of t is keyed on.

func EstimateTokens

func EstimateTokens(system, user string) int64

EstimateTokens is the prompt size Extract compares with ExtractOptions.MaxInputTokens: the bytes of the system prompt and the transcript divided by four, rounded up. It ignores the schema and message framing, and overestimates for long words; it only needs to be cheap and deterministic.

func ExtensionCutoff

func ExtensionCutoff(t *Transcript, prefixHash string) (int, bool)

ExtensionCutoff finds the prefix of t's turns, short of all of them, whose text hashes to prefixHash, an earlier extraction's PrefixHash. It returns the ordinal of that prefix's last turn, the cutoff to extend that extraction with, and whether one matched.

func ExtractSchema

func ExtractSchema() map[string]any

ExtractSchema returns the JSON Schema of the model's reply, {"decisions": [...], "superseded_existing": [...]}. It is valid under OpenAI strict structured outputs, so optional values have an empty form: "" for no rationale, rejected alternative or evidence, decided_turn 0 for none and superseded_existing [] outside an extension. Each "properties" value is an orderedObject, so the fields marshal in the order the model should write them.

func HumanText

func HumanText(input session.UserInput) string

HumanText is the prompt a person typed, cleaned as the transcript shows it, or empty for inputs nobody typed.

func MaxRequestTokens

func MaxRequestTokens(req ModelRequest) int64

MaxRequestTokens is the same upper bound for any request: one token per byte of its system prompt, user message and marshalled schema, plus FramingTokens.

func PrefixHash

func PrefixHash(t *Transcript) string

PrefixHash is the ContentHash of t without its last turn, which a later extraction of the grown session extends from (ExtensionCutoff); "" when t has at most one turn.

func PublicPrompt added in v0.2.0

func PublicPrompt() string

PublicPrompt returns the text of the published extraction prompt: the built-in text a custom prompt replaces.

func ReportOnCall

func ReportOnCall(use session.ToolUse) bool

ReportOnCall reports whether a spawning call's result is its subagent's final report (session.SubagentReport).

func ValidateCitations

func ValidateCitations(decisions []Decision, transcript *Transcript, cutoff int) ([]Decision, []Dropped)

ValidateCitations checks parsed decisions against the transcript they were extracted from. It removes turns that are not transcript ordinals and files not edited in a remaining cited turn. A decision is dropped whole when no turn remains or its own shape is invalid. An unknown trigger, a decided turn that is not a remaining cited turn, and evidence not quoted from one are removed on their own. With a positive cutoff, a decision none of whose remaining turns is after it is dropped: an extension's existing decisions already hold it. Order is kept, and each removal is reported.

Types

type BilledError

type BilledError struct {
	Model string
	Usage Usage
	Err   error
}

BilledError is a call the provider answered, and so billed, whose reply cannot be used: truncated, refused, failed or malformed. Model and Usage are what the provider reported for it.

func (*BilledError) Error

func (e *BilledError) Error() string

func (*BilledError) Unwrap

func (e *BilledError) Unwrap() error

type Category

type Category string

Category is the kind of choice a decision makes.

const (
	CategoryArchitecture   Category = "architecture"
	CategoryDataModel      Category = "data_model"
	CategoryInterface      Category = "interface"
	CategoryBehavior       Category = "behavior"
	CategoryImplementation Category = "implementation"
	CategoryDependency     Category = "dependency"
	CategoryTesting        Category = "testing"
	CategoryScope          Category = "scope"
	// CategoryProcess is CI, build and dev tooling changed in the repository,
	// never issue tracking, orchestration or PR mechanics.
	CategoryProcess Category = "process"
)

func (Category) Valid

func (c Category) Valid() bool

Valid reports whether c is a known category.

type CitedPaths

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

CitedPaths matches the files a decision cites against a set of repository paths, such as a diff's. A decision may cite a file relative to the session's working directory rather than to the repository root.

func NewCitedPaths

func NewCitedPaths(paths []string) CitedPaths

NewCitedPaths indexes paths; a path given twice counts once.

func (CitedPaths) Match

func (c CitedPaths) Match(file string) (string, bool)

Match returns the path a cited file names: the file itself when it is one of the paths, else the one path ending in "/" + file. A file that matches no path, or several by suffix, matches none.

type Decision

type Decision struct {
	Title               string   `json:"title,omitempty"`
	Summary             string   `json:"summary"`
	Category            Category `json:"category"`
	Who                 Who      `json:"who"`
	Trigger             Trigger  `json:"trigger,omitempty"`
	Rationale           string   `json:"rationale,omitempty"`
	RejectedAlternative string   `json:"rejected_alternative,omitempty"`
	Superseded          bool     `json:"superseded"`
	Turns               []int    `json:"turns"`
	DecidedTurn         int      `json:"decided_turn,omitempty"`
	Evidence            string   `json:"evidence,omitempty"`
	EvidenceTurn        int      `json:"evidence_turn,omitempty"`
	Files               []string `json:"files"`
	// ID is the decision's ID, given by the host when it first stores the
	// extraction holding it (Identified); empty in a model reply.
	ID string `json:"decision_id,omitempty"`
}

Decision is one decision made in a session. Turns are the root session's UserInput ordinals; files are repository-relative paths edited in one of those turns. DecidedTurn and EvidenceTurn are cited turns, zero for none; Evidence is a verbatim quote from EvidenceTurn. Trigger, Rationale, DecidedTurn and Evidence are empty in extractions from earlier prompt versions; labels carry no evidence.

func Identified

func Identified(decisions []Decision) []Decision

Identified returns decisions with a random ID given to each one without one. The model never supplies an ID and none is derived, so a host calls it once, when it first stores an extraction, and keeps the IDs from then on.

func ParseReply

func ParseReply(data []byte) ([]Decision, []string, error)

ParseReply decodes a model reply to ExtractSchema strictly: invalid JSON, unknown or missing fields and trailing data are errors. It normalizes each decision: text trimmed and NUL-free, turns sorted and deduplicated, files cleaned and deduplicated, evidence whitespace collapsed. Who and category are not checked here, so a bad value drops one decision in ValidateCitations, not the reply. It also returns the superseded_existing handles, trimmed; a reply without them supersedes nothing.

func (Decision) Validate

func (d Decision) Validate() error

Validate checks the decision's own shape. Whether its turns exist and its files were edited in a cited turn needs the session, so the extractor checks that.

type Dropped

type Dropped struct {
	// Decision is the decision's index in the model's reply.
	Decision int    `json:"decision"`
	Summary  string `json:"summary"`
	// Field names a removed field (trigger, decided_turn or evidence); Turn
	// then holds a removed decided turn. Without Field, Turn or File is the
	// removed citation. All three are empty when the whole decision was
	// dropped. Turn is a pointer so a removed turn 0 is kept.
	Field  string `json:"field,omitempty"`
	Turn   *int   `json:"turn,omitempty"`
	File   string `json:"file,omitempty"`
	Reason string `json:"reason"`
}

Dropped is a citation, a field, or a whole decision, removed by citation validation.

type Event

type Event struct {
	Kind EventKind
	// Text is the agent text, plan or subagent report.
	Text string
	// Tool is the denied tool's name.
	Tool      string
	Questions []Question
	// Discuss reports that the human chose to talk the questions over
	// instead of answering them.
	Discuss bool
	Outcome PlanOutcome
	// Feedback is what the human said when rejecting a plan, a question or a
	// tool call.
	Feedback string
}

Event is one kept item in a turn, in session order.

type EventKind

type EventKind string

EventKind names the kinds of content kept from a turn.

const (
	EventAgentText EventKind = "agent_text"
	EventQuestion  EventKind = "question"
	EventPlan      EventKind = "plan"
	EventDenial    EventKind = "denial"
	EventSubagent  EventKind = "subagent_report"
)

type ExtractOptions

type ExtractOptions struct {
	// MaxInputTokens caps the estimated prompt size; a larger transcript
	// returns ErrTranscriptTooLarge without calling the model. Zero means no cap.
	MaxInputTokens int64
	// MaxOutputTokens is passed to ModelRequest.MaxOutputTokens; zero means
	// the provider client's default.
	MaxOutputTokens int64
	// Existing and Cutoff extend an earlier extraction of the same session
	// instead of starting over, so its decisions and their IDs stay as they
	// were. Existing are its decisions, shown to the model by handle (d1, d2,
	// ...) and returned first; Cutoff is the last turn whose decisions they
	// cover, 0 for none. Either one set extends. A cutoff is usually the turn
	// before the earlier extraction's last turn (ExtensionCutoff), since an
	// open last turn keeps growing.
	Existing []Decision
	Cutoff   int
}

ExtractOptions configures Extract.

type Extraction

type Extraction struct {
	SessionDecisions
	// PromptVersion is the Extractor's PromptVersion.
	PromptVersion string    `json:"prompt_version"`
	Model         string    `json:"model"`
	Usage         Usage     `json:"usage"`
	Dropped       []Dropped `json:"dropped,omitempty"`
}

Extraction is the result for one session.

type Extractor

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

Extractor asks a model for the decisions in a transcript with one system prompt, and validates the reply. The host supplies the prompt, so a private hosted prompt and the published one (NewPublicExtractor) share the schema, parser and validator.

func NewExtractor

func NewExtractor(systemPrompt string) *Extractor

NewExtractor returns an Extractor that sends systemPrompt.

func NewExtractorWith added in v0.2.0

func NewExtractorWith(system string) *Extractor

NewExtractorWith returns an Extractor that sends system. The published prompt's text, or an empty one, gives exactly NewPublicExtractor's Extractor. Any other text gives NewExtractor's, with a PromptVersion of CustomVersionPrefix, "-" and NewExtractor's hash, so an extraction under a custom prompt is never taken for one under the published prompt.

func NewPublicExtractor

func NewPublicExtractor() *Extractor

NewPublicExtractor returns an Extractor that sends the published prompt. Its PromptVersion is PublicPromptName and the hash NewExtractor's would be, so an extraction under it is never taken for one under another host's prompt, even one with the same text.

func (*Extractor) EstimateTokens

func (e *Extractor) EstimateTokens(transcript *Transcript, opts ExtractOptions) int64

EstimateTokens is EstimateTokens of the request Extract would send for transcript and opts, the figure ExtractOptions.MaxInputTokens is compared with.

func (*Extractor) Extract

func (e *Extractor) Extract(ctx context.Context, model Model, transcript *Transcript, opts ExtractOptions) (Extraction, error)

Extract asks model for the decisions in transcript and validates their citations against it (ValidateCitations). A transcript with no turns has no new decisions and does not call the model. An extension returns opts.Existing first, unchanged but for Superseded, then the new decisions, which have no IDs; a new decision none of whose turns is after opts.Cutoff is dropped.

func (*Extractor) IsRequest

func (e *Extractor) IsRequest(req ModelRequest) bool

IsRequest reports whether req carries this Extractor's system prompt and the schema name, as Request builds it. A Model that sends only the transcript and lets the far side rebuild the request checks this first.

func (*Extractor) MaxTokens

func (e *Extractor) MaxTokens(transcript *Transcript, opts ExtractOptions) int64

MaxTokens is an upper bound on the prompt tokens of the request Extract would send for transcript and opts, for reserving its cost before the call. Byte-level BPE tokenizers never produce more tokens than the text has bytes, so it counts one token per byte of the system prompt, the user message and the marshalled schema, plus FramingTokens. It is about four times EstimateTokens for English text, and holds for text with fewer bytes per token, such as CJK.

func (*Extractor) PromptVersion

func (e *Extractor) PromptVersion() string

PromptVersion identifies the prompt and schema: the first 12 hex characters of the SHA-256 of the system prompt followed by the marshalled ExtractSchema. It changes whenever either changes, so cached extractions can be keyed on it.

func (*Extractor) Request

func (e *Extractor) Request(user string, maxOutputTokens int64) ModelRequest

Request is the request Extract sends for user, Extract's user message (userMessage): a rendered transcript's text, extended or not.

func (*Extractor) SystemPrompt

func (e *Extractor) SystemPrompt() string

SystemPrompt is the system prompt the Extractor sends.

type Model

type Model interface {
	Complete(ctx context.Context, req ModelRequest) (ModelResponse, error)
}

Model makes one structured-output call. Provider clients implement it.

type ModelRequest

type ModelRequest struct {
	System string
	User   string
	// SchemaName names Schema for providers that require one.
	SchemaName string
	// Schema is a JSON Schema object that satisfies OpenAI's strict structured
	// outputs: every property required, additionalProperties false.
	Schema map[string]any
	// MaxOutputTokens caps the reply. Zero means the provider client's default.
	MaxOutputTokens int64
}

ModelRequest is one call: a system prompt, one user message and the JSON Schema the reply must match.

type ModelResponse

type ModelResponse struct {
	// JSON is the reply object, expected to match the request's Schema.
	JSON []byte
	// Model is the model the provider reports having used.
	Model string
	Usage Usage
}

ModelResponse is the reply to a ModelRequest.

type Option

type Option func(*settings)

Option configures Render.

func WithSessionFiles

func WithSessionFiles(files ...sessioncorrelate.FileEvidence) Option

WithSessionFiles supplies the session's file evidence as sessioncorrelate.Correlate made it: repo-relative files for the session's repository, possibly built with evidence the graph does not hold, such as Entire's files_touched. They anchor relativization, and an edit counts only when Correlate also reports it as an edit, so files in another repository and paths Correlate could not prove stay out. A path a working directory cut short takes the spelling of the one stored edit it names (storedEdit).

func WithVendor

func WithVendor(vendor string) Option

WithVendor selects the vendor's attribute spellings for working directories, as for sessioncorrelate.Correlate.

type PlanOutcome

type PlanOutcome string

PlanOutcome is what the human did with a proposed plan.

const (
	PlanApproved   PlanOutcome = "approved"
	PlanRejected   PlanOutcome = "rejected"
	PlanUnanswered PlanOutcome = "unanswered"
)

type Question

type Question struct {
	Text    string
	Options []string
	// Answer is empty when no answer was recorded. For Cursor the answer, if
	// any, is the next human prompt.
	Answer string
	// Picked reports that Answer is one (or, for multi-select, only) of the
	// offered options rather than free text.
	Picked bool
	// Note is what the human added to their choice.
	Note string
}

Question is one question the agent asked the human.

type Redactor

type Redactor interface {
	RedactString(ctx context.Context, s string) (string, int, error)
}

Redactor masks secrets and identities in free text. bundle.Redactor satisfies it.

type Row

type Row struct {
	Message *session.AssistantMessage
	ToolUse *session.ToolUse
	Index   int
}

Row is one assistant message or tool call to put in session order. Exactly one of Message and ToolUse is set. Index is the row's position in the graph's AssistantMessages or ToolUses; it breaks ties.

func (Row) Timestamp

func (row Row) Timestamp() *time.Time

Timestamp is the message's or tool call's timestamp, nil when unknown or when the row has neither.

type SessionDecisions

type SessionDecisions struct {
	SessionID string     `json:"session_id"`
	Decisions []Decision `json:"decisions"`
}

SessionDecisions is the decisions extracted from one root session.

type Transcript

type Transcript struct {
	SessionID string
	Turns     []Turn
}

Transcript is one root session reduced to what decision extraction needs.

func Render

func Render(ctx context.Context, graph *session.Graph, redactor Redactor, opts ...Option) (*Transcript, error)

Render builds the transcript of graph's root session. Text is redacted with redactor after local paths are made repo-relative.

func (*Transcript) Text

func (t *Transcript) Text() string

Text is the transcript as the extractor reads it (ARCHITECTURE.md, "decisions"). The output is deterministic for a given transcript.

type Tree

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

Tree is a session tree prepared for rendering: its paths resolved against the tree's roots and working directories, the account names its paths reveal, the calls the human rejected, and the order of its rows. Render builds the decisions transcript from one. Its exported methods let another render of the same tree, such as a validation transcript, share these rules rather than copy them.

func NewTree

func NewTree(graph *session.Graph, opts ...Option) (*Tree, error)

NewTree prepares graph for rendering. It fails when graph has no root session.

func (*Tree) EditedPath

func (r *Tree) EditedPath(touch session.FileTouch) (string, bool)

EditedPath is the repo-relative path an edit touch wrote, as the edited-file list shows it. It reports false for a read, for a touch of a call the human rejected (the mappers record the file the agent proposed to write), for a path nothing relativizes, and, when the session's file evidence was supplied, for a path that names no stored edit or several.

func (*Tree) NotificationReport

func (r *Tree) NotificationReport(input session.UserInput) (report, spawnToolUseID string)

NotificationReport is the report in a task notification for one of the tree's Claude Code subagents, with the id of the call that started that subagent; both are empty for any other input.

func (*Tree) Result

func (r *Tree) Result(toolUseID string) *session.ToolResult

Result is the result of a tool call, or nil when it has none.

func (*Tree) ScrubPaths

func (r *Tree) ScrubPaths(text string) string

ScrubPaths rewrites the tree's roots and working directories to repo-relative form, masks every other local path, and masks the account names the tree's own paths reveal where they are joined to other text: a dashed project directory, a hostname, a branch. It masks paths and account names only and redacts no secrets or e-mail addresses: its output must still go through a Redactor, as Render does, before it is shown or sent.

func (*Tree) SortRows

func (r *Tree) SortRows(rows []Row)

SortRows puts rows in session order as the transcript orders a turn: by position in the vendor file when every row has one, else by timestamp when every row has one, else by model invocation; a message comes before a tool call when nothing else orders them. The order depends on which rows are given, so sort exactly the rows that will be shown.

func (*Tree) SpawnedSession

func (r *Tree) SpawnedSession(toolUseID string) (string, bool)

SpawnedSession is the nested session a tool call spawned.

func (*Tree) ToolEvent

func (r *Tree) ToolEvent(use session.ToolUse) (Event, bool)

ToolEvent is what the transcript keeps of a root-session tool call: question, plan or denial dialogue, or a subagent report. It reports false for a call the transcript drops.

type Trigger

type Trigger string

Trigger is what prompted a decision.

const (
	TriggerRequest    Trigger = "request"
	TriggerReview     Trigger = "review"
	TriggerFailure    Trigger = "failure"
	TriggerFinding    Trigger = "finding"
	TriggerInitiative Trigger = "initiative"
)

func (Trigger) Valid

func (t Trigger) Valid() bool

Valid reports whether t is a known trigger.

type Turn

type Turn struct {
	// Ordinal is the stored UserInput.Ordinal. Decision citations use it, so
	// it is never renumbered; dropped inputs leave gaps.
	Ordinal int `json:"ordinal"`
	// Human is the cleaned prompt, empty when the input was not a human
	// prompt (a system notification, an injected skill body).
	Human  string  `json:"-"`
	Events []Event `json:"-"`
	// EditedFiles are the repo-relative paths edited during the turn, sorted.
	// Decision `files` citations are validated against this list.
	EditedFiles []string `json:"edited_files"`
}

Turn is one root-session user input and everything kept from what it caused, including its nested sessions.

type Usage

type Usage struct {
	InputTokens       int64 `json:"input_tokens"`
	CachedInputTokens int64 `json:"cached_input_tokens"`
	OutputTokens      int64 `json:"output_tokens"`
}

Usage is the token usage of one call. InputTokens includes CachedInputTokens, as OpenAI reports it.

type Who

type Who string

Who records which party a decision belongs to.

const (
	WhoHuman    Who = "human"
	WhoAgent    Who = "agent"
	WhoApproved Who = "agent_proposed_human_approved"
)

func (Who) Valid

func (w Who) Valid() bool

Valid reports whether w is a known value.

Jump to

Keyboard shortcuts

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