Files
Test 927835cb0e feat(T1.3): implement activity timeout tuning automation
- Add internal/tuning package with intelligent timeout analysis
- Implement TimeoutAnalyzer for tracking activity execution metrics
- Calculate percentile-based timeout recommendations (P95, P99)
- Generate confidence scores based on sample size and failure rate
- Implement TimeoutLessonsStore for persistent lesson tracking
- Store lessons in per-task JSONL files with effectiveness tracking
- Generate TimeoutTuningSignal objects for planner integration
- Generate human-readable lesson format for planner context
- Support three-tier priority signaling (high/medium/low)
- Analyze multiple activities concurrently

Analysis Features:
- Track duration, success/failure, timestamps for each execution
- Identify undertuned activities (P99 exceeds timeout)
- Detect overtuned activities (timeout > 2x P99)
- Calculate confidence scores (40% sample data + 60% reliability)
- Generate recommendations with reasoning

Lesson Management:
- Persist lessons per task in JSONL format
- Support lesson effectiveness tracking
- Format lessons for planner input
- Enable feedback loop for timeout optimization

Test Coverage:
- 14 analyzer tests (metrics, analysis, persistence)
- 22 lessons tests (storage, signals, formatting)
- 36 total tuning tests, all passing
- Edge cases: empty metrics, all failures, multiple activities

Key Design:
- P99 + 20% buffer for safe timeout values
- Weighted confidence scoring for reliable recommendations
- Separation: Analyzer (metrics), Lessons (storage), Signals (integration)
- Thread-safe analyzer with RWMutex
- No external dependencies added

Closes T1.3
2026-08-23 16:47:31 -07:00

198 lines
5.4 KiB
Go

package tuning
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"time"
)
// TimeoutLesson represents a learned timeout recommendation
type TimeoutLesson struct {
ActivityType string `json:"activity_type"`
OldTimeout time.Duration `json:"old_timeout"`
NewTimeout time.Duration `json:"new_timeout"`
Reason string `json:"reason"`
FailureRate float64 `json:"failure_rate"`
SampleSize int `json:"sample_size"`
ConfidenceScore float64 `json:"confidence_score"`
AppliedAt time.Time `json:"applied_at"`
Effective bool `json:"effective"` // Whether recommendation helped
}
// TimeoutLessonsStore manages timeout lessons for task-specific tuning
type TimeoutLessonsStore struct {
basePath string
}
// NewTimeoutLessonsStore creates a new timeout lessons store
func NewTimeoutLessonsStore(basePath string) *TimeoutLessonsStore {
return &TimeoutLessonsStore{
basePath: basePath,
}
}
// AppendLesson appends a timeout lesson to the lessons file
func (tls *TimeoutLessonsStore) AppendLesson(taskID string, lesson *TimeoutLesson) error {
lessonsDir := filepath.Join(tls.basePath, "tuning", "lessons")
// Create directory if it doesn't exist
if err := os.MkdirAll(lessonsDir, 0755); err != nil {
return fmt.Errorf("failed to create lessons directory: %w", err)
}
lessonsFile := filepath.Join(lessonsDir, fmt.Sprintf("%s_timeout_lessons.jsonl", taskID))
// Marshal lesson to JSON
data, err := json.Marshal(lesson)
if err != nil {
return fmt.Errorf("failed to marshal lesson: %w", err)
}
// Append to file
f, err := os.OpenFile(lessonsFile, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
return fmt.Errorf("failed to open lessons file: %w", err)
}
defer f.Close()
_, err = f.Write(append(data, '\n'))
if err != nil {
return fmt.Errorf("failed to write lesson: %w", err)
}
return nil
}
// ReadLessons reads all timeout lessons for a task
func (tls *TimeoutLessonsStore) ReadLessons(taskID string) ([]*TimeoutLesson, error) {
lessonsFile := filepath.Join(tls.basePath, "tuning", "lessons", fmt.Sprintf("%s_timeout_lessons.jsonl", taskID))
// If file doesn't exist, return empty list
if _, err := os.Stat(lessonsFile); os.IsNotExist(err) {
return nil, nil
}
data, err := os.ReadFile(lessonsFile)
if err != nil {
return nil, fmt.Errorf("failed to read lessons file: %w", err)
}
var lessons []*TimeoutLesson
content := string(data)
// Parse JSONL line by line
var inLine []byte
for _, ch := range []byte(content) {
if ch == '\n' {
if len(inLine) > 0 {
var lesson TimeoutLesson
if err := json.Unmarshal(inLine, &lesson); err == nil {
lessons = append(lessons, &lesson)
}
}
inLine = nil
} else {
inLine = append(inLine, ch)
}
}
return lessons, nil
}
// GetLatestLesson returns the most recent timeout lesson for a task
func (tls *TimeoutLessonsStore) GetLatestLesson(taskID string) (*TimeoutLesson, error) {
lessons, err := tls.ReadLessons(taskID)
if err != nil {
return nil, err
}
if len(lessons) == 0 {
return nil, nil
}
return lessons[len(lessons)-1], nil
}
// GenerateLessonFromRecommendation creates a lesson from a timeout recommendation
func GenerateLessonFromRecommendation(rec *TimeoutRecommendation) *TimeoutLesson {
if rec == nil {
return nil
}
failureRate := 0.0
if rec.SuccessCount+rec.FailureCount > 0 {
failureRate = float64(rec.FailureCount) / float64(rec.SuccessCount+rec.FailureCount)
}
return &TimeoutLesson{
ActivityType: rec.ActivityType,
OldTimeout: rec.CurrentTimeout,
NewTimeout: rec.RecommendedTimeout,
Reason: rec.Reason,
FailureRate: failureRate,
SampleSize: rec.SuccessCount + rec.FailureCount,
ConfidenceScore: rec.Confidence,
AppliedAt: time.Now(),
Effective: false, // To be determined after next run
}
}
// FormatLessonsForPlanner formats timeout lessons for planner input
func FormatLessonsForPlanner(lessons []*TimeoutLesson) string {
if len(lessons) == 0 {
return "No timeout lessons available."
}
output := "Recent timeout lessons learned:\n"
for i, lesson := range lessons {
output += fmt.Sprintf(
"\n[Lesson %d] %s:\n Old Timeout: %v → New Timeout: %v\n Reason: %s\n Confidence: %.1f%%\n",
i+1,
lesson.ActivityType,
lesson.OldTimeout,
lesson.NewTimeout,
lesson.Reason,
lesson.ConfidenceScore*100,
)
}
return output
}
// TimeoutTuningSignal represents a signal to update timeout tuning
type TimeoutTuningSignal struct {
ActivityType string `json:"activity_type"`
NewTimeout time.Duration `json:"new_timeout"`
Reason string `json:"reason"`
Confidence float64 `json:"confidence"`
Priority string `json:"priority"` // "low", "medium", "high"
}
// GenerateSignalsFromRecommendations generates tuning signals from recommendations
func GenerateSignalsFromRecommendations(recommendations []TimeoutRecommendation) []TimeoutTuningSignal {
signals := make([]TimeoutTuningSignal, 0)
for _, rec := range recommendations {
priority := "low"
if rec.Confidence > 0.7 {
priority = "high"
} else if rec.Confidence > 0.5 {
priority = "medium"
}
signal := TimeoutTuningSignal{
ActivityType: rec.ActivityType,
NewTimeout: rec.RecommendedTimeout,
Reason: rec.Reason,
Confidence: rec.Confidence,
Priority: priority,
}
signals = append(signals, signal)
}
return signals
}