Skip to content

Commit efad481

Browse files
authored
Small fixes for ag UI (#481)
fix ootb workflow cache better google mcp restart more reliable initial prompt
1 parent e7cf372 commit efad481

File tree

4 files changed

+285
-126
lines changed

4 files changed

+285
-126
lines changed

components/backend/handlers/sessions.go

Lines changed: 72 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"path/filepath"
1515
"sort"
1616
"strings"
17+
"sync"
1718
"time"
1819
"unicode/utf8"
1920

@@ -46,6 +47,20 @@ var (
4647

4748
const runnerTokenRefreshedAtAnnotation = "ambient-code.io/token-refreshed-at"
4849

50+
// ootbWorkflowsCache provides in-memory caching for OOTB workflows to avoid GitHub API rate limits.
51+
// The cache stores workflows by repo URL key and expires after ootbCacheTTL.
52+
type ootbWorkflowsCache struct {
53+
mu sync.RWMutex
54+
workflows []OOTBWorkflow
55+
cachedAt time.Time
56+
cacheKey string // repo+branch+path combination
57+
}
58+
59+
var (
60+
ootbCache = &ootbWorkflowsCache{}
61+
ootbCacheTTL = 5 * time.Minute // Cache OOTB workflows for 5 minutes
62+
)
63+
4964
// isBinaryContentType checks if a MIME type represents binary content that should be base64 encoded.
5065
// This includes images, archives, documents, executables, and other non-text formats.
5166
func isBinaryContentType(contentType string) bool {
@@ -1718,35 +1733,10 @@ type OOTBWorkflow struct {
17181733
}
17191734

17201735
// ListOOTBWorkflows returns the list of out-of-the-box workflows dynamically discovered from GitHub
1721-
// Attempts to use user's GitHub token for better rate limits, falls back to unauthenticated for public repos
1736+
// Uses in-memory caching (5 min TTL) to avoid GitHub API rate limits.
1737+
// Attempts to use user's GitHub token for better rate limits when cache miss occurs.
17221738
// GET /api/workflows/ootb?project=<projectName>
17231739
func ListOOTBWorkflows(c *gin.Context) {
1724-
// Try to get user's GitHub token (best effort - not required)
1725-
// This gives better rate limits (5000/hr vs 60/hr) and supports private repos
1726-
// Project is optional - if provided, we'll try to get the user's token
1727-
token := ""
1728-
project := c.Query("project") // Optional query parameter
1729-
if project != "" {
1730-
usrID, _ := c.Get("userID")
1731-
k8sClt, sessDyn := GetK8sClientsForRequest(c)
1732-
if k8sClt == nil || sessDyn == nil {
1733-
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid or missing token"})
1734-
c.Abort()
1735-
return
1736-
}
1737-
if userIDStr, ok := usrID.(string); ok && userIDStr != "" {
1738-
if githubToken, err := GetGitHubToken(c.Request.Context(), k8sClt, sessDyn, project, userIDStr); err == nil {
1739-
token = githubToken
1740-
log.Printf("ListOOTBWorkflows: using user's GitHub token for project %s (better rate limits)", project)
1741-
} else {
1742-
log.Printf("ListOOTBWorkflows: failed to get GitHub token for project %s: %v", project, err)
1743-
}
1744-
}
1745-
}
1746-
if token == "" {
1747-
log.Printf("ListOOTBWorkflows: proceeding without GitHub token (public repo, lower rate limits)")
1748-
}
1749-
17501740
// Read OOTB repo configuration from environment
17511741
ootbRepo := strings.TrimSpace(os.Getenv("OOTB_WORKFLOWS_REPO"))
17521742
if ootbRepo == "" {
@@ -1763,6 +1753,43 @@ func ListOOTBWorkflows(c *gin.Context) {
17631753
ootbWorkflowsPath = "workflows"
17641754
}
17651755

1756+
// Build cache key from repo configuration
1757+
cacheKey := fmt.Sprintf("%s|%s|%s", ootbRepo, ootbBranch, ootbWorkflowsPath)
1758+
1759+
// Check cache first (read lock)
1760+
ootbCache.mu.RLock()
1761+
if ootbCache.cacheKey == cacheKey && time.Since(ootbCache.cachedAt) < ootbCacheTTL && len(ootbCache.workflows) > 0 {
1762+
workflows := ootbCache.workflows
1763+
ootbCache.mu.RUnlock()
1764+
log.Printf("ListOOTBWorkflows: returning %d cached workflows (age: %v)", len(workflows), time.Since(ootbCache.cachedAt).Round(time.Second))
1765+
c.JSON(http.StatusOK, gin.H{"workflows": workflows})
1766+
return
1767+
}
1768+
ootbCache.mu.RUnlock()
1769+
1770+
// Cache miss - need to fetch from GitHub
1771+
// Try to get user's GitHub token (best effort - not required)
1772+
// This gives better rate limits (5000/hr vs 60/hr) and supports private repos
1773+
token := ""
1774+
project := c.Query("project") // Optional query parameter
1775+
if project != "" {
1776+
usrID, _ := c.Get("userID")
1777+
k8sClt, sessDyn := GetK8sClientsForRequest(c)
1778+
if k8sClt != nil && sessDyn != nil {
1779+
if userIDStr, ok := usrID.(string); ok && userIDStr != "" {
1780+
if githubToken, err := GetGitHubToken(c.Request.Context(), k8sClt, sessDyn, project, userIDStr); err == nil {
1781+
token = githubToken
1782+
log.Printf("ListOOTBWorkflows: using user's GitHub token for project %s (better rate limits)", project)
1783+
} else {
1784+
log.Printf("ListOOTBWorkflows: failed to get GitHub token for project %s: %v", project, err)
1785+
}
1786+
}
1787+
}
1788+
}
1789+
if token == "" {
1790+
log.Printf("ListOOTBWorkflows: proceeding without GitHub token (public repo, lower rate limits)")
1791+
}
1792+
17661793
// Parse GitHub URL
17671794
owner, repoName, err := git.ParseGitHubURL(ootbRepo)
17681795
if err != nil {
@@ -1775,6 +1802,16 @@ func ListOOTBWorkflows(c *gin.Context) {
17751802
entries, err := fetchGitHubDirectoryListing(c.Request.Context(), owner, repoName, ootbBranch, ootbWorkflowsPath, token)
17761803
if err != nil {
17771804
log.Printf("ListOOTBWorkflows: failed to list workflows directory: %v", err)
1805+
// On error, try to return stale cache if available
1806+
ootbCache.mu.RLock()
1807+
if len(ootbCache.workflows) > 0 && ootbCache.cacheKey == cacheKey {
1808+
workflows := ootbCache.workflows
1809+
ootbCache.mu.RUnlock()
1810+
log.Printf("ListOOTBWorkflows: returning stale cached workflows due to GitHub error")
1811+
c.JSON(http.StatusOK, gin.H{"workflows": workflows})
1812+
return
1813+
}
1814+
ootbCache.mu.RUnlock()
17781815
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to discover OOTB workflows"})
17791816
return
17801817
}
@@ -1822,7 +1859,14 @@ func ListOOTBWorkflows(c *gin.Context) {
18221859
})
18231860
}
18241861

1825-
log.Printf("ListOOTBWorkflows: discovered %d workflows from %s", len(workflows), ootbRepo)
1862+
// Update cache (write lock)
1863+
ootbCache.mu.Lock()
1864+
ootbCache.workflows = workflows
1865+
ootbCache.cachedAt = time.Now()
1866+
ootbCache.cacheKey = cacheKey
1867+
ootbCache.mu.Unlock()
1868+
1869+
log.Printf("ListOOTBWorkflows: discovered %d workflows from %s (cached for %v)", len(workflows), ootbRepo, ootbCacheTTL)
18261870
c.JSON(http.StatusOK, gin.H{"workflows": workflows})
18271871
}
18281872

components/backend/websocket/agui_proxy.go

Lines changed: 49 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -147,26 +147,58 @@ func HandleAGUIRunProxy(c *gin.Context) {
147147
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Hour)
148148
defer cancel()
149149

150-
proxyReq, err := http.NewRequestWithContext(ctx, "POST", runnerURL, bytes.NewReader(bodyBytes))
151-
if err != nil {
152-
log.Printf("AGUI Proxy: Failed to create request in background: %v", err)
153-
updateRunStatus(runID, "error")
154-
return
155-
}
156-
157-
// Forward headers
158-
proxyReq.Header.Set("Content-Type", "application/json")
159-
proxyReq.Header.Set("Accept", "text/event-stream")
160-
161-
// Execute request
150+
// Execute request with retries (runner may not be ready immediately after startup)
162151
client := &http.Client{
163152
Timeout: 0, // No timeout, context handles it
164153
}
165-
resp, err := client.Do(proxyReq)
166-
if err != nil {
167-
log.Printf("AGUI Proxy: Background request failed: %v", err)
168-
updateRunStatus(runID, "error")
169-
return
154+
155+
var resp *http.Response
156+
maxRetries := 15
157+
retryDelay := 500 * time.Millisecond
158+
159+
for attempt := 1; attempt <= maxRetries; attempt++ {
160+
// Create fresh request for each attempt (body reader needs reset)
161+
proxyReq, err := http.NewRequestWithContext(ctx, "POST", runnerURL, bytes.NewReader(bodyBytes))
162+
if err != nil {
163+
log.Printf("AGUI Proxy: Failed to create request in background: %v", err)
164+
updateRunStatus(runID, "error")
165+
return
166+
}
167+
168+
// Forward headers
169+
proxyReq.Header.Set("Content-Type", "application/json")
170+
proxyReq.Header.Set("Accept", "text/event-stream")
171+
172+
resp, err = client.Do(proxyReq)
173+
if err == nil {
174+
break // Success!
175+
}
176+
177+
// Check if it's a connection refused error (runner not ready yet)
178+
errStr := err.Error()
179+
isConnectionRefused := strings.Contains(errStr, "connection refused") ||
180+
strings.Contains(errStr, "no such host") ||
181+
strings.Contains(errStr, "dial tcp")
182+
183+
if !isConnectionRefused || attempt == maxRetries {
184+
log.Printf("AGUI Proxy: Background request failed after %d attempts: %v", attempt, err)
185+
updateRunStatus(runID, "error")
186+
return
187+
}
188+
189+
log.Printf("AGUI Proxy: Runner not ready (attempt %d/%d), retrying in %v...", attempt, maxRetries, retryDelay)
190+
191+
select {
192+
case <-ctx.Done():
193+
log.Printf("AGUI Proxy: Context cancelled during retry for run %s", runID)
194+
return
195+
case <-time.After(retryDelay):
196+
// Exponential backoff with cap at 5 seconds
197+
retryDelay = time.Duration(float64(retryDelay) * 1.5)
198+
if retryDelay > 5*time.Second {
199+
retryDelay = 5 * time.Second
200+
}
201+
}
170202
}
171203
defer resp.Body.Close()
172204

0 commit comments

Comments
 (0)