Documentation
¶
Overview ¶
Package agent provides the agent runner for executing LLM interactions.
Index ¶
- Constants
- func HandleMetaCommand(input string, cfg *config.Config, registry *tools.Registry, ...) (bool, []llm.Message)
- type BacktrackConfig
- type BacktrackEvent
- type BacktrackTracker
- func (bt *BacktrackTracker) GetDiscardedStats() DiscardedStats
- func (bt *BacktrackTracker) GetLastEvent() *BacktrackEvent
- func (bt *BacktrackTracker) GetMaxRetries() int
- func (bt *BacktrackTracker) GetRetryCount() int
- func (bt *BacktrackTracker) IsEnabled() bool
- func (bt *BacktrackTracker) RecordDiscarded(promptTokens, completionTokens int, cost float64)
- func (bt *BacktrackTracker) Reset()
- func (bt *BacktrackTracker) ResetAtPoint()
- func (bt *BacktrackTracker) SetLastEvent(toolName, reason string)
- func (bt *BacktrackTracker) ShouldBacktrack(historyLen int) bool
- func (bt *BacktrackTracker) ShouldInjectUserMessage() bool
- type DiscardedStats
- type Episode
- type Interrogator
- type LLMClient
- type Logger
- func (l *Logger) AgentIteration(iteration int, toolCallCount int)
- func (l *Logger) Close() error
- func (l *Logger) Debug(msg string, fields ...zap.Field)
- func (l *Logger) Error(msg string, err error)
- func (l *Logger) Info(msg string, fields ...zap.Field)
- func (l *Logger) LLMCall(model string, promptTokens, completionTokens int, duration time.Duration)
- func (l *Logger) ToolExecuted(toolName string, duration time.Duration, success bool, err error)
- type LoopDetector
- func (ld *LoopDetector) DetectAlternatingLoop(threshold int) *LoopInfo
- func (ld *LoopDetector) DetectErrorLoop(threshold int) *LoopInfo
- func (ld *LoopDetector) DetectLoop(threshold int) *LoopInfo
- func (ld *LoopDetector) GetRecentErrorCount(toolName string) int
- func (ld *LoopDetector) Record(toolName, args, result string, isError bool)
- func (ld *LoopDetector) Reset()
- type LoopInfo
- type RunConfig
- type RunResult
- type Runner
- func (r *Runner) ContinueSubagent(ctx context.Context, sess *tools.SubagentSession, prompt string) (tools.SubagentRunResult, error)
- func (r *Runner) Run(ctx context.Context, rcfg RunConfig) (*RunResult, error)
- func (r *Runner) RunSubagent(ctx context.Context, p tools.SubagentRunParams) (tools.SubagentRunResult, error)
- func (r *Runner) SetPersist(persist func([]llm.Message) error, rollback func(int) error)
- func (r *Runner) SetToolStart(fn func(internalName string, tc llm.ToolCall))
- func (r *Runner) StartSubagent(ctx context.Context, p tools.SubagentRunParams) (*tools.SubagentSession, tools.SubagentRunResult, error)
- func (r *Runner) ToolNames() []string
- func (r *Runner) Writer() *ui.Writer
- type RunnerOptions
Constants ¶
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 ¶
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 ¶
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 ¶
AgentIteration logs an agent loop iteration.
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 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 (*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 ¶
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
func (r *Runner) RunSubagent(ctx context.Context, p tools.SubagentRunParams) (tools.SubagentRunResult, error)
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 ¶
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
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
func (r *Runner) StartSubagent(ctx context.Context, p tools.SubagentRunParams) (*tools.SubagentSession, tools.SubagentRunResult, error)
StartSubagent runs the child loop to completion and returns a session handle for follow-ups (structured_output or summary nudges) alongside the result.
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