loop

package
v0.0.0-...-a6983c7 Latest Latest
Warning

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

Go to latest
Published: Feb 20, 2026 License: MIT Imports: 16 Imported by: 0

Documentation

Overview

Package loop implements the implementation loop runner with rate-limit recovery.

Index

Constants

View Source
const DefaultImplementTemplate = `` /* 810-byte string literal not displayed */

DefaultImplementTemplate is the built-in prompt template used when no custom template file is configured. It uses [[ and ]] as delimiters to avoid conflicts with {{ and }} that commonly appear in task spec content (e.g. Go template syntax, JSON, shell substitutions).

Variables

This section is empty.

Functions

This section is empty.

Types

type AgentErrorRecovery

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

AgentErrorRecovery tracks consecutive agent errors and decides whether the implementation loop should continue or abort.

func NewAgentErrorRecovery

func NewAgentErrorRecovery(maxConsecutiveErrors int, logger interface {
	Warn(msg string, kv ...interface{})
}) *AgentErrorRecovery

NewAgentErrorRecovery creates an AgentErrorRecovery. Set maxConsecutiveErrors to 0 or negative to disable the limit (the loop never aborts due to consecutive errors). logger may be nil.

func (*AgentErrorRecovery) RecordError

func (aer *AgentErrorRecovery) RecordError(err error) bool

RecordError records an agent error and returns whether the loop should continue. Returns false when the consecutive error limit has been reached, signalling the caller to abort the loop.

func (*AgentErrorRecovery) RecordSuccess

func (aer *AgentErrorRecovery) RecordSuccess()

RecordSuccess resets the consecutive error counter. Call this after each successful agent invocation.

func (*AgentErrorRecovery) ShouldAbort

func (aer *AgentErrorRecovery) ShouldAbort() bool

ShouldAbort returns true if the consecutive error count has reached or exceeded the configured maximum. Returns false when the limit is disabled (maxConsecutiveErrors <= 0).

type CompletionSignal

type CompletionSignal string

CompletionSignal represents a signal detected in agent output.

const (
	SignalPhaseComplete CompletionSignal = "PHASE_COMPLETE"
	SignalTaskBlocked   CompletionSignal = "TASK_BLOCKED"
	SignalRavenError    CompletionSignal = "RAVEN_ERROR"
)

func DetectSignals

func DetectSignals(output string) (CompletionSignal, string)

DetectSignals scans output for completion signal strings. It returns the first CompletionSignal found and any trailing detail text (e.g., reason following TASK_BLOCKED or RAVEN_ERROR). Returns an empty signal if none found.

This function is exported for use in tests.

func DetectSignalsFromJSONL

func DetectSignalsFromJSONL(output string) (CompletionSignal, string)

DetectSignalsFromJSONL scans JSONL output (stream-json format) for completion signals embedded within assistant text content blocks. Each line is parsed as a StreamEvent; text blocks within assistant messages are scanned for signals. Returns an empty signal if none is found.

This function is exported for use in tests.

type DirtyTreeRecovery

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

DirtyTreeRecovery detects and handles uncommitted changes in the working tree before an agent run, ensuring the agent starts from a clean state.

func NewDirtyTreeRecovery

func NewDirtyTreeRecovery(
	gitClient *git.GitClient,
	events chan<- RecoveryEvent,
	logger interface {
		Info(msg string, kv ...interface{})
		Warn(msg string, kv ...interface{})
	},
) *DirtyTreeRecovery

NewDirtyTreeRecovery creates a DirtyTreeRecovery. events may be nil.

func (*DirtyTreeRecovery) CheckAndStash

func (dtr *DirtyTreeRecovery) CheckAndStash(ctx context.Context, taskID string) (bool, error)

CheckAndStash checks for uncommitted changes and stashes them if present. Returns true if changes were stashed, false if the working tree was already clean (or if git stash reported nothing to save).

func (*DirtyTreeRecovery) EnsureCleanTree

func (dtr *DirtyTreeRecovery) EnsureCleanTree(ctx context.Context, taskID string) error

EnsureCleanTree verifies the working tree is clean. If dirty, it stashes the changes. Returns an error if the status check or stash operation fails.

func (*DirtyTreeRecovery) RestoreStash

func (dtr *DirtyTreeRecovery) RestoreStash(ctx context.Context) error

RestoreStash pops the most recent stash entry created by CheckAndStash.

type LoopEvent

type LoopEvent struct {
	Type      LoopEventType
	Iteration int
	TaskID    string
	AgentName string
	Message   string
	Timestamp time.Time
	Duration  time.Duration
	WaitTime  time.Duration

	// Stream-level observability fields (populated for tool/thinking/stats events).
	ToolName  string  // Name of the tool called (EventToolStarted) or tool_use_id (EventToolCompleted).
	CostUSD   float64 // Session cost in USD (EventSessionStats).
	TokensIn  int     // Input token count (EventSessionStats).
	TokensOut int     // Output token count (EventSessionStats).
}

LoopEvent represents a structured event emitted during loop execution.

type LoopEventType

type LoopEventType string

LoopEventType identifies the type of loop event.

const (
	EventLoopStarted     LoopEventType = "loop_started"
	EventTaskSelected    LoopEventType = "task_selected"
	EventPromptGenerated LoopEventType = "prompt_generated"
	EventAgentStarted    LoopEventType = "agent_started"
	EventAgentCompleted  LoopEventType = "agent_completed"
	EventAgentError      LoopEventType = "agent_error"
	EventRateLimitWait   LoopEventType = "rate_limit_wait"
	EventRateLimitResume LoopEventType = "rate_limit_resume"
	EventTaskCompleted   LoopEventType = "task_completed"
	EventTaskBlocked     LoopEventType = "task_blocked"
	EventPhaseComplete   LoopEventType = "phase_complete"
	EventLoopError       LoopEventType = "loop_error"
	EventLoopAborted     LoopEventType = "loop_aborted"
	EventMaxIterations   LoopEventType = "max_iterations"
	EventSleeping        LoopEventType = "sleeping"
	EventDryRun          LoopEventType = "dry_run"

	// Fine-grained stream observability events (emitted when Claude is
	// invoked with stream-json output format).
	EventToolStarted   LoopEventType = "tool_started"
	EventToolCompleted LoopEventType = "tool_completed"
	EventAgentThinking LoopEventType = "agent_thinking"
	EventSessionStats  LoopEventType = "session_stats"
)

type PromptContext

type PromptContext struct {
	// Task-specific context.
	TaskSpec  string // Full markdown content of the current task spec.
	TaskID    string // e.g., "T-016"
	TaskTitle string // e.g., "Task Spec Markdown Parser"

	// Phase context.
	PhaseID    int    // Current phase number.
	PhaseName  string // e.g., "Task System & Agent Adapters"
	PhaseRange string // e.g., "T-016 to T-030"

	// Project context.
	ProjectName     string // From raven.toml project.name.
	ProjectLanguage string // From raven.toml project.language.

	// Verification.
	VerificationCommands []string // From raven.toml project.verification_commands.
	VerificationString   string   // Commands joined with " && ".

	// Task progress context.
	CompletedTasks   []string // IDs of completed tasks.
	RemainingTasks   []string // IDs of remaining tasks in current phase.
	CompletedSummary string   // Formatted summary of completed tasks.
	RemainingSummary string   // Formatted summary of remaining tasks.

	// Agent context.
	AgentName string // e.g., "claude"
	Model     string // e.g., "claude-opus-4-6"
}

PromptContext holds all runtime values that are substituted into a prompt template when generating an agent prompt.

func BuildContext

func BuildContext(
	spec *task.ParsedTaskSpec,
	phase *task.Phase,
	cfg *config.Config,
	selector *task.TaskSelector,
	agentName string,
) (*PromptContext, error)

BuildContext constructs a PromptContext from the provided parsed task spec, phase, project configuration, task selector, and agent name.

The selector is used to populate CompletedTasks and RemainingTasks. The agentName is matched against cfg.Agents to look up the configured model.

type PromptGenerator

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

PromptGenerator loads, caches, and renders prompt templates. It uses [[ and ]] as template delimiters so that {{ and }} in task spec content are never misinterpreted as template actions.

func NewPromptGenerator

func NewPromptGenerator(templateDir string) (*PromptGenerator, error)

NewPromptGenerator creates a PromptGenerator. If templateDir is non-empty, it must refer to an existing directory; an error is returned otherwise. The built-in DefaultImplementTemplate is pre-parsed and cached as the fallback template.

func (*PromptGenerator) Generate

func (pg *PromptGenerator) Generate(templateName string, ctx PromptContext) (string, error)

Generate renders a prompt for the given PromptContext. If templateName is non-empty, the named template file is loaded (and cached) from the generator's templateDir. If templateName is empty, the built-in DefaultImplementTemplate is used.

func (*PromptGenerator) GenerateFromString

func (pg *PromptGenerator) GenerateFromString(tmplStr string, ctx PromptContext) (string, error)

GenerateFromString renders a prompt from an inline template string rather than a file. The string must use [[ and ]] as delimiters.

func (*PromptGenerator) LoadTemplate

func (pg *PromptGenerator) LoadTemplate(name string) (*template.Template, error)

LoadTemplate loads the named template file from the generator's templateDir, parses it with [[ / ]] delimiters, and caches the result. Subsequent calls for the same name return the cached template without re-reading the file.

The name must not be empty and must not contain path components that would escape the templateDir (directory traversal is rejected).

type RateLimitWaiter

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

RateLimitWaiter blocks the loop until the rate limit for a given agent has reset, displaying a live countdown to the output writer and emitting structured recovery events.

func NewRateLimitWaiter

func NewRateLimitWaiter(
	coordinator *agent.RateLimitCoordinator,
	output io.Writer,
	events chan<- RecoveryEvent,
	logger interface {
		Info(msg string, kv ...interface{})
	},
) *RateLimitWaiter

NewRateLimitWaiter creates a RateLimitWaiter. output may be nil to suppress the countdown display. events may be nil to disable event emission.

func (*RateLimitWaiter) Wait

func (w *RateLimitWaiter) Wait(ctx context.Context, agentName string) error

Wait blocks until the rate limit for agentName has reset. If no rate limit is active for the agent, Wait returns immediately. If ctx is cancelled before the reset, Wait returns ctx.Err().

type RecoveryEvent

type RecoveryEvent struct {
	// Type identifies what happened.
	Type RecoveryEventType
	// Message is a human-readable description of the event.
	Message string
	// Remaining is the time remaining in a rate-limit countdown (zero otherwise).
	Remaining time.Duration
	// Timestamp is the wall-clock time at which the event was created.
	Timestamp time.Time
}

RecoveryEvent is a structured event emitted during recovery operations.

type RecoveryEventType

type RecoveryEventType string

RecoveryEventType identifies the type of recovery event.

const (
	// EventRateLimitCountdown is emitted when a rate-limit countdown begins.
	EventRateLimitCountdown RecoveryEventType = "rate_limit_countdown"
	// EventRateLimitResuming is emitted when the countdown ends and the agent may retry.
	EventRateLimitResuming RecoveryEventType = "rate_limit_resuming"
	// EventDirtyTreeDetected is emitted when uncommitted changes are found.
	EventDirtyTreeDetected RecoveryEventType = "dirty_tree_detected"
	// EventStashCreated is emitted when changes are successfully stashed.
	EventStashCreated RecoveryEventType = "stash_created"
	// EventStashRestored is emitted when the stash is successfully popped.
	EventStashRestored RecoveryEventType = "stash_restored"
	// EventStashFailed is emitted when a stash push or pop operation fails.
	EventStashFailed RecoveryEventType = "stash_failed"
	// EventRecoveryError is emitted when a recovery step encounters an error.
	EventRecoveryError RecoveryEventType = "recovery_error"
)

type RunConfig

type RunConfig struct {
	AgentName     string
	PhaseID       int
	TaskID        string        // Specific task ID (empty for phase mode).
	MaxIterations int           // default: 50
	MaxLimitWaits int           // default: 5
	SleepBetween  time.Duration // default: 5s
	DryRun        bool
	TemplateName  string
}

RunConfig configures the implementation loop behavior.

type Runner

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

Runner orchestrates the implementation loop. It selects the next actionable task, generates a prompt, invokes the agent, interprets the output, updates task state, and repeats until the phase is complete, limits are reached, or the context is cancelled.

func NewRunner

func NewRunner(
	selector *task.TaskSelector,
	promptGen *PromptGenerator,
	ag agent.Agent,
	stateManager *task.StateManager,
	rateLimiter *agent.RateLimitCoordinator,
	cfg *config.Config,
	phases []task.Phase,
	events chan<- LoopEvent,
	logger interface {
		Info(msg string, kv ...interface{})
		Debug(msg string, kv ...interface{})
	},
) *Runner

NewRunner creates an implementation loop runner with all dependencies. The events channel receives structured LoopEvent values; pass nil to disable event emission. The logger must not be nil.

func (*Runner) Run

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

Run executes the implementation loop in phase mode. It iterates over all not-started tasks in runCfg.PhaseID, running the agent on each, until the phase is complete, max iterations are reached, or ctx is cancelled.

func (*Runner) RunSingleTask

func (r *Runner) RunSingleTask(ctx context.Context, runCfg RunConfig) error

RunSingleTask runs the loop for a specific task ID (--task T-007 mode). It selects the task by ID, generates a prompt, invokes the agent, and returns after one successful invocation (or error).

func (*Runner) SetProgressGenerator

func (r *Runner) SetProgressGenerator(pg *task.ProgressGenerator, progressPath string)

SetProgressGenerator configures a ProgressGenerator that regenerates PROGRESS.md at progressPath after each task state change. If not set, progress file regeneration is skipped. This should be called before Run or RunSingleTask.

Jump to

Keyboard shortcuts

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