package cmd import ( "fmt" "log/slog" "strings" "github.com/nextlevelbuilder/goclaw/internal/channels" ) // Matching TS pi-embedded-helpers/errors.ts error classification. // Never expose raw JSON/API payloads to the user. func formatAgentError(err error) string { raw := err.Error() lower := strings.ToLower(raw) // 1. Timeout — must be checked BEFORE context overflow because // "context deadline exceeded" contains both "context" and "exceeded", // which would false-positive match the context overflow heuristic. if containsAny(lower, "timeout", "timed out", "deadline exceeded") { return "⚠️ Request timed out. Please try again." } // 2. Context overflow if isContextOverflowError(lower) { return "⚠️ Context overflow — message too large for this model. Try /new to start a fresh session." } // 3. Role ordering / message format errors (tool_use_id mismatch, roles must alternate, etc.) if isMessageFormatError(lower) { return "⚠️ Session history conflict — please try again. If this persists, use /new to start a fresh session." } // 4. Rate limit if containsAny(lower, "rate limit", "rate_limit", "too many requests", "429", "quota exceeded", "resource_exhausted") { return "⚠️ API rate limit reached. Please try again later." } // 5. Overloaded if strings.Contains(lower, "overloaded") { return "⚠️ The AI service is temporarily overloaded. Please try again in a moment." } // 6. Billing if containsAny(lower, "billing", "insufficient credits", "credit balance", "payment required", "402") { return "⚠️ API billing error — your API key may have run out of credits. Check your provider's billing dashboard." } // 7. Auth errors if containsAny(lower, "invalid api key", "invalid_api_key", "unauthorized", "forbidden", "authentication", "401", "403", "access denied") { return "⚠️ Authentication error. Please check your API key configuration." } // 8. Model config if strings.Contains(lower, "not a valid model") { return "⚠️ Model configuration error. Please check your config and restart." } // 9. Generic — log the full error but show only a safe message to user slog.Warn("unclassified agent error", "error", raw) return "⚠️ Sorry, something went wrong processing your message. Please try again." } // isContextOverflowError checks for context window/size overflow patterns. func isContextOverflowError(lower string) bool { return containsAny(lower, "request_too_large", "context length exceeded", "maximum context length", "prompt is too long", "exceeds model context window", "request exceeds the maximum size", ) || (strings.Contains(lower, "context") && containsAny(lower, "overflow", "too large", "too long", "limit", "exceeded")) } // isMessageFormatError checks for tool_use/tool_result mismatch, role ordering, // and other message format errors that indicate corrupted session history. func isMessageFormatError(lower string) bool { return containsAny(lower, "tool_use_id", "tool_use.id", "unexpected tool", "roles must alternate", "incorrect role information", "invalid request format", "tool_result block", "tool_use block", ) } // containsAny returns true if s contains any of the given substrings. func containsAny(s string, substrs ...string) bool { for _, sub := range substrs { if strings.Contains(s, sub) { return true } } return false } // formatQuotaExceeded formats a user-friendly quota exceeded message. func formatQuotaExceeded(result channels.QuotaResult) string { labels := map[string]string{"hour": "Hourly", "day": "Daily", "week": "Weekly"} return fmt.Sprintf("⚠️ %s request limit reached (%d/%d). Please try again later.", labels[result.Window], result.Used, result.Limit) }