agent

package
v0.6.2 Latest Latest
Warning

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

Go to latest
Published: Oct 8, 2026 License: MIT Imports: 24 Imported by: 0

Documentation

Overview

Package agent provides the agent runner for executing LLM interactions.

Index

Constants

View Source
const (
	TriggerDuplicateCall   = "duplicate_call"
	TriggerPendingBlocked  = "pending_blocked"
	TriggerAlternatingLoop = "alternating_loop"
	TriggerFatal           = "fatal"
	TriggerBacktrack       = "backtrack"
)

Anomaly trigger names (must match config.DiagnosticsConfig.InterrogateTriggers values).

Variables

This section is empty.

Functions

func HandleMetaCommand

func HandleMetaCommand(
	input string,
	cfg *config.Config,
	registry *tools.Registry,
	systemPrompt string,
	messages []llm.Message,
) (bool, []llm.Message)

HandleMetaCommand processes REPL meta-commands like :quit, :config, etc. Returns (shouldExit, resetMessages). If resetMessages is non-nil, it replaces the current message list.

Types

type BacktrackConfig

type BacktrackConfig struct {
	Enabled           bool // Enable backtrack mode
	MaxRetries        int  // Max retries at same history point (default: 5)
	InjectUserMessage bool // On limit reached: backtrack + inject user message instead of error-in-history
}

BacktrackConfig holds configuration for backtrack mode

func DefaultBacktrackConfig

func DefaultBacktrackConfig() BacktrackConfig

DefaultBacktrackConfig returns the default backtrack configuration

type BacktrackEvent

type BacktrackEvent struct {
	ToolName string
	Reason   string
}

BacktrackEvent records a single backtrack occurrence for reporting

type BacktrackTracker

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

BacktrackTracker tracks backtrack state and costs

func NewBacktrackTracker

func NewBacktrackTracker(config BacktrackConfig) *BacktrackTracker

NewBacktrackTracker creates a new backtrack tracker with the given config

func (*BacktrackTracker) GetDiscardedStats

func (bt *BacktrackTracker) GetDiscardedStats() DiscardedStats

GetDiscardedStats returns the accumulated stats for discarded requests

func (*BacktrackTracker) GetLastEvent

func (bt *BacktrackTracker) GetLastEvent() *BacktrackEvent

GetLastEvent returns and clears the last backtrack event

func (*BacktrackTracker) GetMaxRetries

func (bt *BacktrackTracker) GetMaxRetries() int

GetMaxRetries returns the maximum retries configured

func (*BacktrackTracker) GetRetryCount

func (bt *BacktrackTracker) GetRetryCount() int

GetRetryCount returns the current retry count at this history point

func (*BacktrackTracker) IsEnabled

func (bt *BacktrackTracker) IsEnabled() bool

IsEnabled returns whether backtrack mode is enabled

func (*BacktrackTracker) RecordDiscarded

func (bt *BacktrackTracker) RecordDiscarded(promptTokens, completionTokens int, cost float64)

RecordDiscarded records costs for a discarded request

func (*BacktrackTracker) Reset

func (bt *BacktrackTracker) Reset()

Reset resets the tracker state (but not accumulated stats)

func (*BacktrackTracker) ResetAtPoint

func (bt *BacktrackTracker) ResetAtPoint()

ResetAtPoint resets the retry counter (called after fallback to error-in-history)

func (*BacktrackTracker) SetLastEvent

func (bt *BacktrackTracker) SetLastEvent(toolName, reason string)

SetLastEvent records the last backtrack event for reporting

func (*BacktrackTracker) ShouldBacktrack

func (bt *BacktrackTracker) ShouldBacktrack(historyLen int) bool

ShouldBacktrack returns true if should backtrack, false if should fall back to error-in-history. It also increments the retry counter for the current history point.

func (*BacktrackTracker) ShouldInjectUserMessage

func (bt *BacktrackTracker) ShouldInjectUserMessage() bool

ShouldInjectUserMessage returns whether user message should be injected on limit

type DiscardedStats

type DiscardedStats struct {
	TotalPromptTokens     int
	TotalCompletionTokens int
	TotalCost             float64
	DiscardCount          int
}

DiscardedStats tracks costs for discarded (backtracked) requests

type Episode

type Episode struct {
	Trigger       string // one of the Trigger* constants
	Key           string // dedup key - interrogation fires once per episode
	TriggerCount  int    // e.g. how many consecutive identical calls
	OffendingTool string
	OffendingArgs string // raw JSON arguments of the offending call
	PriorResult   string // the previous tool result the model saw
	PendingDiff   string // for pending-edit triggers: the pending diff
	Turn          int
	ContextTokens int
}

Episode describes one detected anomaly to (maybe) interrogate and log.

type Interrogator

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

Interrogator asks the model to explain anomalous behavior and logs the Q&A to a side channel. It is observability-only: callers resume their normal recovery path unchanged. Construction returns nil when diagnostics are disabled, so all call sites can treat a nil *Interrogator as "do nothing".

func NewInterrogator

func NewInterrogator(cfg *config.Config, client LLMClient, writer *ui.Writer, logger *Logger, runID string) *Interrogator

NewInterrogator builds an Interrogator, or returns nil when interrogation is disabled.

func (*Interrogator) Enabled

func (it *Interrogator) Enabled() bool

Enabled reports whether interrogation is active (nil-safe).

func (*Interrogator) Interrogate

func (it *Interrogator) Interrogate(ctx context.Context, ep Episode, state *runState)

Interrogate runs one interrogation episode if all gates pass: feature enabled, this trigger enabled, the per-task cap not yet hit, and this episode not already seen. It never mutates production history and never returns an error - failures are logged and swallowed so the caller's recovery path is unaffected.

type LLMClient

type LLMClient interface {
	Chat(context.Context, llm.ChatRequest) (*llm.ChatResponse, error)
	GetGenerationStats(context.Context, string) (*llm.GenerationStats, error)
}

LLMClient is the slice of the LLM client the agent loop actually uses. Declaring it here rather than depending on *llm.Client lets a test drive the loop with a scripted sequence of responses. *llm.Client satisfies it as-is.

type Logger

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

Logger provides structured logging for agent operations.

func NewLogger

func NewLogger(logPath string, development bool) (*Logger, error)

NewLogger creates a new Logger instance that writes to a file. If logPath is empty, logging is disabled. If development is true, uses development config with readable output. Otherwise uses production config with JSON output.

func (*Logger) AgentIteration

func (l *Logger) AgentIteration(iteration int, toolCallCount int)

AgentIteration logs an agent loop iteration.

func (*Logger) Close

func (l *Logger) Close() error

Close syncs the logger (should be called on shutdown).

func (*Logger) Debug

func (l *Logger) Debug(msg string, fields ...zap.Field)

Debug logs a debug message.

func (*Logger) Error

func (l *Logger) Error(msg string, err error)

Error logs an error.

func (*Logger) Info

func (l *Logger) Info(msg string, fields ...zap.Field)

Info logs an info message.

func (*Logger) LLMCall

func (l *Logger) LLMCall(model string, promptTokens, completionTokens int, duration time.Duration)

LLMCall logs an LLM API call.

func (*Logger) ToolExecuted

func (l *Logger) ToolExecuted(toolName string, duration time.Duration, success bool, err error)

ToolExecuted logs a tool execution with details.

type LoopDetector

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

LoopDetector tracks tool calls and detects when the LLM is stuck in a loop

func NewLoopDetector

func NewLoopDetector() *LoopDetector

NewLoopDetector creates a new loop detector

func (*LoopDetector) DetectAlternatingLoop

func (ld *LoopDetector) DetectAlternatingLoop(threshold int) *LoopInfo

DetectAlternatingLoop detects patterns like A→B→A→B→A→B where two calls repeat This catches loops like Edit→Edit.cancel→Edit→Edit.cancel threshold is the number of complete A→B cycles required (e.g., 3 means A→B→A→B→A→B)

func (*LoopDetector) DetectErrorLoop

func (ld *LoopDetector) DetectErrorLoop(threshold int) *LoopInfo

DetectErrorLoop specifically checks for repeated errors (any errors, not necessarily identical) This catches cases where LLM keeps hitting the same error with slightly different args

func (*LoopDetector) DetectLoop

func (ld *LoopDetector) DetectLoop(threshold int) *LoopInfo

DetectLoop checks if the last N calls are identical (same tool, args, and result) Returns loop info if detected, nil otherwise Detects both error loops AND success loops (LLM repeating same successful call)

func (*LoopDetector) GetRecentErrorCount

func (ld *LoopDetector) GetRecentErrorCount(toolName string) int

GetRecentErrorCount returns the count of consecutive recent errors for a specific tool

func (*LoopDetector) Record

func (ld *LoopDetector) Record(toolName, args, result string, isError bool)

Record records a tool call result

func (*LoopDetector) Reset

func (ld *LoopDetector) Reset()

Reset clears the history (call when user provides new input)

type LoopInfo

type LoopInfo struct {
	ToolName  string
	Count     int
	IsError   bool // All calls in the loop were errors
	IsSuccess bool // All calls in the loop were successes (still a loop!)
}

LoopInfo contains information about a detected loop

type RunConfig

type RunConfig struct {
	Messages  []llm.Message
	QuietMode bool
}

RunConfig contains per-run configuration options

type RunResult

type RunResult struct {
	Stats         *stats.AgentStats
	FinalMessages []llm.Message
	Cancelled     bool
	// TimedOut is set when the loop stopped because the context passed to Run
	// hit its deadline, as opposed to being cancelled by the user. Callers that
	// impose a time budget (the thinkbench executor) use it to report the
	// budget rather than the bare "context deadline exceeded".
	TimedOut bool
	// BudgetExhausted is set when the loop ran out of iterations
	// (agent.max_tool_iterations) while the model still wanted to keep going.
	// The run produced no final answer, and without this the caller could not
	// tell that apart from a model that simply finished.
	BudgetExhausted bool
	// Failure is what ended the turn without an answer: a model call that
	// could not be retried into working, or a response with nothing usable in
	// it. It has already been reported to the user by the time Run returns;
	// it is here so the caller can set the process's exit status from it.
	Failure error
}

RunResult contains the results of running the agent loop

type Runner

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

Runner executes the agent loop for LLM interactions

func NewRunner

func NewRunner(opts RunnerOptions) *Runner

NewRunner creates a new agent runner

func (*Runner) ContinueSubagent added in v0.5.0

func (r *Runner) ContinueSubagent(ctx context.Context, sess *tools.SubagentSession, prompt string) (tools.SubagentRunResult, error)

ContinueSubagent re-enters the session's loop with one more user prompt and runs it to completion again. History persists across calls, so a nudge sees everything the child said and did before.

func (*Runner) Run

func (r *Runner) Run(ctx context.Context, rcfg RunConfig) (*RunResult, error)

Run executes the agent loop for a single user input. It returns the updated messages and stats after completion.

func (*Runner) RunSubagent added in v0.5.0

RunSubagent drives a child loop synchronously inside the turn's process. The child's messages are never appended to history.jsonl and never merged into the parent messages: the delegation contract is the summary text in the parent's Subagent tool result.

func (*Runner) SetPersist

func (r *Runner) SetPersist(persist func([]llm.Message) error, rollback func(int) error)

SetPersist installs the hooks that record what the loop produces and what it takes back. Passing nil for both turns recording off, which is what a benchmark run wants.

func (*Runner) SetToolStart added in v0.2.0

func (r *Runner) SetToolStart(fn func(internalName string, tc llm.ToolCall))

SetToolStart installs a function told about each tool call just before its checks and its run, after the model's message naming it has been handed to the persist hook. A question a tool asks while it runs belongs to that call. Passing nil removes it.

func (*Runner) StartSubagent added in v0.5.0

StartSubagent runs the child loop to completion and returns a session handle for follow-ups (structured_output or summary nudges) alongside the result.

func (*Runner) ToolNames

func (r *Runner) ToolNames() []string

ToolNames is what this run offered the model, for the record a session keeps of the settings a turn ran with.

func (*Runner) Writer

func (r *Runner) Writer() *ui.Writer

Writer returns the UI writer for output configuration.

type RunnerOptions

type RunnerOptions struct {
	Cfg       *config.Config
	LLMClient LLMClient
	Registry  *tools.Registry
	Writer    *ui.Writer
	Logger    *Logger
	ToolCtx   *tools.ToolContext
	// Inbox is where anything that arrives mid-turn waits: a line typed at
	// the terminal, a file dropped by "kvit-coder steer", a background
	// process exiting. The loop drains it once per iteration. Optional.
	Inbox *inbox.Inbox
	// Procs owns the processes that outlive a turn. The loop asks it once per
	// iteration what has happened and puts the answer in the inbox. Optional.
	Procs *procs.Registry
	// Persist, when set, is handed each message as the loop produces it, so a
	// turn's work reaches the session as it happens: a crash costs one message
	// rather than the turn, and "tail -f" on the history has something to
	// follow while the turn is still running.
	Persist func([]llm.Message) error
	// Rollback, when set, is told that the last n persisted messages were
	// discarded, which is how backtracking appears in a file that is only ever
	// appended to.
	Rollback func(n int) error
}

RunnerOptions contains all dependencies for creating a Runner

Jump to

Keyboard shortcuts

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