llm

package
v0.45.0 Latest Latest
Warning

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

Go to latest
Published: Jul 29, 2026 License: MIT Imports: 11 Imported by: 0

Documentation

Overview

Package llm owns every LLM-backed feature in spec: the declared task registry, deterministic prompt assembly, and the human review gate.

Two rules shape this package. First, it never writes to stdout or stderr — the service returns errors and callers decide how to degrade, which is what makes the same task usable from the CLI, the TUI, and a test. Second, prompt assembly is pure: a Task turns inputs into a request with no I/O, so every task is golden-testable and a prompt change is visible in a diff.

Index

Constants

View Source
const (
	// WeightBackground is nice-to-have context, dropped first.
	WeightBackground = 10
	// WeightSupporting is useful context: related sections, metadata.
	WeightSupporting = 20
	// WeightPrimary is the material the task is fundamentally about.
	WeightPrimary = 30
	// WeightCritical never trims in practice. Reserved for reviewer steer
	// notes: dropping the user's explicit instruction to fit a budget would be
	// the one trim that is never acceptable.
	WeightCritical = 100
)

Context part weights. Named rather than inline so a task author picks an intent, not a magic number.

View Source
const PriorDraftLabel = "Previous attempt (rejected)"

PriorDraftLabel is the context-part label carrying a rejected attempt.

View Source
const SteerLabel = "Reviewer feedback"

SteerLabel is the context-part label carrying reviewer feedback. Asserted by golden test, so it is a constant rather than a string literal per task.

Variables

View Source
var ErrNoDraft = errors.New("provider returned an empty draft")

ErrNoDraft reports that generation produced nothing usable on the first attempt, so there is nothing to review.

View Source
var ErrNotInteractive = errors.New("draft review needs an interactive terminal")

ErrNotInteractive reports that the review gate needs a terminal it does not have. Scripted callers pass --accept instead of being silently prompted at a pipe that will never answer.

View Source
var ErrUnavailable = errors.New("no agent completion plane configured")

ErrUnavailable reports that no completion plane is configured or enabled. Callers decide the degradation message — the service never prints, so the same code path serves the CLI, the TUI, and tests.

Functions

func EditInEditor

func EditInEditor(content, editor string) (string, error)

Editing a draft is an interactive, terminal-bound action, so it lives beside the CLI renderer rather than in the service or prompt-assembly path: those must stay free of subprocesses and stdio (see no_print_test.go). EditInEditor opens content in the user's editor and returns the saved result. Exported because the TUI needs the same suspend-and-edit behaviour as the CLI.

func EditInEditorContext

func EditInEditorContext(ctx context.Context, content, editor string) (string, error)

EditInEditorContext is EditInEditor bound to a context, so a cancelled review tears down the editor rather than leaving it holding the terminal.

It attaches the editor to the process's own stdio and blocks, which is only correct for callers that own the terminal in cooked mode (the CLI gate). A TUI must use EditorSession with tea.ExecProcess instead.

func EditorSession

func EditorSession(ctx context.Context, content, editor string) (cmd *exec.Cmd, result func() (string, error), err error)

EditorSession prepares an editor invocation over content without running it.

The returned command has no stdio attached: the caller decides how it meets the terminal. That split exists because running a terminal editor from a background goroutine while a TUI holds the screen corrupts the tty — the editor restores its own cooked termios on exit (IXON back on turns Ctrl+S into flow control) and the TUI never knows it must re-enter raw mode. The TUI therefore hands this command to tea.ExecProcess, which releases the terminal first and restores it after.

result reads the edited content and removes the temp file; call it exactly once after the command finishes, on both the success and failure paths.

func IsInteractive

func IsInteractive() bool

IsInteractive reports whether stdin is a terminal. Callers use it to choose between the gate and an explicit non-interactive path.

Types

type Action

type Action string

Action is a reviewer's decision about the shown draft.

const (
	// ActionAccept writes the shown attempt.
	ActionAccept Action = "accept"
	// ActionEdit opens the shown attempt in $EDITOR, then returns to the gate.
	ActionEdit Action = "edit"
	// ActionRetry regenerates with identical inputs.
	ActionRetry Action = "retry"
	// ActionRetryNote regenerates with an added reviewer instruction.
	ActionRetryNote Action = "retry_note"
	// ActionEscalate hands off to an interactive session, carrying context.
	ActionEscalate Action = "escalate"
	// ActionSkip abandons the review, writing nothing.
	ActionSkip Action = "skip"
	// ActionPrev shows the previous attempt.
	ActionPrev Action = "prev"
	// ActionNext shows the next attempt.
	ActionNext Action = "next"
)

type Attempt

type Attempt struct {
	Content string
	Result  *Result
	// Note is the steer that produced this attempt, empty for the first.
	Note string
	// Edited marks an attempt modified in $EDITOR, so the status line can say
	// the content is no longer exactly what the model returned.
	Edited bool
}

Attempt is one generated draft within a review, retained so the reviewer can navigate between attempts and compare before accepting.

type CLIPrompter

type CLIPrompter struct {
	// Out receives the rendered draft. Defaults to os.Stdout.
	Out io.Writer
	// In supplies keystrokes. Defaults to os.Stdin.
	In io.Reader
	// Title labels the artifact under review ("Problem Statement").
	Title string
	// Editor overrides $EDITOR for the edit action.
	Editor string
	// contains filtered or unexported fields
}

CLIPrompter renders the review gate at a terminal. It is one of two renderers of the same state machine; the TUI modal is the other, and the keys mirror deliberately so the loop is learned once and works over SSH.

func NewCLIPrompter

func NewCLIPrompter(title, editor string) *CLIPrompter

NewCLIPrompter builds a terminal prompter for a titled artifact.

func (*CLIPrompter) Note

func (c *CLIPrompter) Note() (string, error)

Note collects a one-line steer.

func (*CLIPrompter) Notify

func (c *CLIPrompter) Notify(message string)

Notify prints a non-fatal condition without ending the review.

func (*CLIPrompter) Show

func (c *CLIPrompter) Show(a Attempt, index, total int, caps GateCapabilities) (Action, error)

Show renders one attempt and reads the reviewer's choice.

type ContextPart

type ContextPart struct {
	Label   string
	Content string
	// Weight orders trimming. Lower weights are dropped first; ties break by
	// declaration order, so trimming is deterministic and testable.
	Weight int
}

ContextPart is a labelled block of context with a trimming weight. It mirrors adapter.ContextPart but is declared here because weight is an assembly concern: adapters render what they are given and do no trimming.

type GateCapabilities

type GateCapabilities struct {
	// CanEscalate reports whether an interactive session is possible.
	CanEscalate bool
	// CanNavigate reports whether more than one attempt exists.
	CanNavigate bool
}

GateCapabilities tells a renderer which actions to offer. Escalation is only shown when the agent actually has a session plane, so the gate never advertises an action that would fail.

type Generator

type Generator func(ctx context.Context, notes []string, priorDraft string) (*Result, error)

Generator produces one draft. The gate calls it for the first attempt and again for every retry, passing accumulated steer notes.

type Input

type Input struct {
	// SpecID is the target spec (e.g. "SPEC-034"), when there is one.
	SpecID string
	// Section is the target section slug, for section-scoped tasks.
	Section string
	// Sections holds existing spec content keyed by slug, used as context.
	Sections map[string]string
	// Meta carries frontmatter-derived values (title, repos, priority).
	Meta map[string]string
	// Repos lists target repositories, for planning tasks.
	Repos []string
	// Diff is a unified diff, for PR-description tasks.
	Diff string
	// Extra carries task-specific free-form values.
	Extra map[string]string
	// SteerNotes are reviewer instructions from retry-with-note. They are
	// appended as a maximum-weight context part so budgeting never drops the
	// one thing the user explicitly asked for.
	SteerNotes []string
	// PriorDraft is the rejected attempt, included when escalating or retrying
	// so the model can see what missed rather than starting blind.
	PriorDraft string
}

Input carries everything a task's Build may read. It is a single struct rather than per-task types so the registry stays uniform and callers assemble inputs the same way for every task.

type Outcome

type Outcome struct {
	Action Action
	// Content is the accepted text. Empty unless Action is ActionAccept.
	Content string
	// Attempts is every draft generated during the review, for telemetry:
	// retry count is the signal that a task is underperforming.
	Attempts []Attempt
	// SteerNotes is every note the reviewer added, carried into an escalated
	// session so nothing is re-explained.
	SteerNotes []string
}

Outcome is how a review ended.

func Review

func Review(ctx context.Context, gen Generator, p Prompter, caps GateCapabilities) (*Outcome, error)

Review runs the gate loop until the reviewer accepts, skips, or escalates.

Nothing is written here: Review returns the accepted content and the caller persists it. That separation is what makes the gate identical for a section write, a PR description, and a standup.

func (Outcome) RetryCount

func (o Outcome) RetryCount() int

RetryCount reports how many regenerations the reviewer asked for.

type Prompter

type Prompter interface {
	// Show presents an attempt and returns the chosen action. index and total
	// let the renderer show "attempt 2 of 3".
	Show(a Attempt, index, total int, caps GateCapabilities) (Action, error)
	// Note collects a one-line steer for ActionRetryNote.
	Note() (string, error)
	// Notify reports a non-fatal condition (an empty draft, a failed attempt)
	// without ending the review.
	Notify(message string)
}

Prompter renders the gate and collects the reviewer's decision. The CLI implements it with a terminal prompt; the TUI implements it with a modal. Keeping it an interface is what stops the two surfaces from drifting into different action sets.

type Request

type Request struct {
	Task    string
	Title   string
	System  string
	Prompt  string
	Parts   []ContextPart
	Format  adapter.OutputFormat
	Trimmed []string
}

Request is a fully assembled, provider-agnostic generation request. It is the golden-test surface: given fixed inputs, a task must produce a byte-identical Request.

func Assemble

func Assemble(t Task, in Input) Request

Assemble turns a task and its inputs into a Request, applying the task's context budget deterministically.

Steer notes and the prior draft are appended here rather than in each task's Build, so every task supports retry-with-note identically and no task author can forget to honour it.

func (Request) GenerateRequest

func (r Request) GenerateRequest(maxTokens int) adapter.GenerateRequest

GenerateRequest converts an assembled Request into the adapter-level request.

When context was trimmed, the prompt says so: a model that silently received less than the task promised would be misled about what it was given, and a user reading a thin draft deserves to know why.

func (Request) String

func (r Request) String() string

String renders the assembled request as the exact text a provider receives. Golden tests compare against this, so a prompt change shows up as a reviewable diff rather than an invisible behaviour change.

type Result

type Result struct {
	Text     string
	Model    string
	Tokens   adapter.TokenUsage
	Duration time.Duration
	// Raw carries a bounded provider tail when parsing yielded no text, so an
	// empty draft is debuggable rather than mysterious.
	Raw string
}

Result is one generation outcome plus what it cost. Duration and usage feed the review gate's status line and the activity-log telemetry, which is the data source for the latency evidence a future API fast path would need.

type Service

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

Service runs declared tasks against an agent adapter's completion plane.

func NewService

func NewService(agent adapter.AgentAdapter, enabled bool) *Service

NewService creates a task-running service over an agent adapter.

func (*Service) Capabilities

func (s *Service) Capabilities() adapter.Capabilities

Capabilities exposes the resolved agent's capability set so callers can gate affordances without reaching for the adapter themselves.

func (*Service) IsAvailable

func (s *Service) IsAvailable() bool

IsAvailable reports whether a completion plane is configured and enabled. A session-only harness is not available here: it has no Generate.

func (*Service) Run

func (s *Service) Run(ctx context.Context, task Task, in Input) (*Result, error)

Run assembles a task and generates once.

It returns ErrUnavailable when no completion plane exists, so a caller can distinguish "not configured" from "the provider failed" and word its message accordingly. Provider errors are returned verbatim, never printed.

func (*Service) WithMaxTokens

func (s *Service) WithMaxTokens(n int) *Service

WithMaxTokens sets the response cap passed to providers that support one.

type Task

type Task struct {
	// ID is the stable task identifier used in telemetry and budgets
	// (e.g. "draft-section"). It appears in the activity log, so it is part of
	// the tool's observable surface and should not change casually.
	ID string
	// Title is the human label shown in the review gate ("Problem Statement").
	Title string
	// System is the system prompt. Task-specific, because "draft a section" and
	// "propose a PR stack" want different personas.
	System string
	// TokenBudget caps assembled context in approximate tokens. 0 means no cap.
	TokenBudget int
	// Format selects markdown (default) or JSON output.
	Format adapter.OutputFormat
	// Build turns typed inputs into the user prompt and labelled context parts.
	// It must be pure: no file reads, no network, no clock.
	Build func(in Input) (prompt string, parts []ContextPart)
}

Task declares one LLM feature: what to ask, how to assemble context, and how much of it the model may receive. Adding a feature is one Task plus one golden test, not a bespoke prompt-and-plumbing stack.

Tasks are plain data and pure functions. There is deliberately no plugin machinery: a handful of prompts does not need a framework.

Directories

Path Synopsis
Package tasks declares spec's LLM tasks, one file per concern.
Package tasks declares spec's LLM tasks, one file per concern.

Jump to

Keyboard shortcuts

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