diff --git a/backend/agent/api.py b/backend/agent/api.py index fc7b27b8..965c7e8b 100644 --- a/backend/agent/api.py +++ b/backend/agent/api.py @@ -1767,7 +1767,6 @@ async def import_agent_from_json( request: JsonImportRequestModel, user_id: str = Depends(get_current_user_id_from_jwt) ): - """Import an agent from JSON with credential mappings""" logger.info(f"Importing agent from JSON - user: {user_id}") if not await is_enabled("custom_agents"): @@ -1776,6 +1775,21 @@ async def import_agent_from_json( detail="Custom agents currently disabled. This feature is not available at the moment." ) + client = await db.client + from .utils import check_agent_count_limit + limit_check = await check_agent_count_limit(client, user_id) + + if not limit_check['can_create']: + error_detail = { + "message": f"Maximum of {limit_check['limit']} agents allowed for your current plan. You have {limit_check['current_count']} agents.", + "current_count": limit_check['current_count'], + "limit": limit_check['limit'], + "tier_name": limit_check['tier_name'], + "error_code": "AGENT_LIMIT_EXCEEDED" + } + logger.warning(f"Agent limit exceeded for account {user_id}: {limit_check['current_count']}/{limit_check['limit']} agents") + raise HTTPException(status_code=402, detail=error_detail) + try: from agent.json_import_service import JsonImportService, JsonImportRequest import_service = JsonImportService(db) @@ -1817,6 +1831,20 @@ async def create_agent( ) client = await db.client + from .utils import check_agent_count_limit + limit_check = await check_agent_count_limit(client, user_id) + + if not limit_check['can_create']: + error_detail = { + "message": f"Maximum of {limit_check['limit']} agents allowed for your current plan. You have {limit_check['current_count']} agents.", + "current_count": limit_check['current_count'], + "limit": limit_check['limit'], + "tier_name": limit_check['tier_name'], + "error_code": "AGENT_LIMIT_EXCEEDED" + } + logger.warning(f"Agent limit exceeded for account {user_id}: {limit_check['current_count']}/{limit_check['limit']} agents") + raise HTTPException(status_code=402, detail=error_detail) + try: if agent_data.is_default: await client.table('agents').update({"is_default": False}).eq("account_id", user_id).eq("is_default", True).execute() @@ -1874,6 +1902,10 @@ async def create_agent( await client.table('agents').delete().eq('agent_id', agent['agent_id']).execute() raise HTTPException(status_code=500, detail="Failed to create initial version") + # Invalidate agent count cache after successful creation + from utils.cache import Cache + await Cache.invalidate(f"agent_count_limit:{user_id}") + logger.info(f"Created agent {agent['agent_id']} with v1 for user: {user_id}") return AgentResponse( agent_id=agent['agent_id'], @@ -2275,7 +2307,20 @@ async def delete_agent(agent_id: str, user_id: str = Depends(get_current_user_id if agent['is_default']: raise HTTPException(status_code=400, detail="Cannot delete default agent") - await client.table('agents').delete().eq('agent_id', agent_id).execute() + if agent.get('metadata', {}).get('is_suna_default', False): + raise HTTPException(status_code=400, detail="Cannot delete Suna default agent") + + delete_result = await client.table('agents').delete().eq('agent_id', agent_id).execute() + + if not delete_result.data: + logger.warning(f"No agent was deleted for agent_id: {agent_id}, user_id: {user_id}") + raise HTTPException(status_code=403, detail="Unable to delete agent - permission denied or agent not found") + + try: + from utils.cache import Cache + await Cache.invalidate(f"agent_count_limit:{user_id}") + except Exception as cache_error: + logger.warning(f"Cache invalidation failed for user {user_id}: {str(cache_error)}") logger.info(f"Successfully deleted agent: {agent_id}") return {"message": "Agent deleted successfully"} diff --git a/backend/agent/json_import_service.py b/backend/agent/json_import_service.py index 40cc5211..8e0fb767 100644 --- a/backend/agent/json_import_service.py +++ b/backend/agent/json_import_service.py @@ -114,6 +114,9 @@ class JsonImportService: request.custom_system_prompt or json_data.get('system_prompt', '') ) + from utils.cache import Cache + await Cache.invalidate(f"agent_count_limit:{request.account_id}") + logger.info(f"Successfully imported agent {agent_id} from JSON") return JsonImportResult( diff --git a/backend/agent/utils.py b/backend/agent/utils.py index 48fdd372..9162f138 100644 --- a/backend/agent/utils.py +++ b/backend/agent/utils.py @@ -138,3 +138,63 @@ async def check_agent_run_limit(client, account_id: str) -> Dict[str, Any]: 'running_count': 0, 'running_thread_ids': [] } + + +async def check_agent_count_limit(client, account_id: str) -> Dict[str, Any]: + try: + try: + result = await Cache.get(f"agent_count_limit:{account_id}") + if result: + logger.debug(f"Cache hit for agent count limit: {account_id}") + return result + except Exception as cache_error: + logger.warning(f"Cache read failed for agent count limit {account_id}: {str(cache_error)}") + + agents_result = await client.table('agents').select('agent_id, metadata').eq('account_id', account_id).execute() + + non_suna_agents = [] + for agent in agents_result.data or []: + metadata = agent.get('metadata', {}) or {} + is_suna_default = metadata.get('is_suna_default', False) + if not is_suna_default: + non_suna_agents.append(agent) + + current_count = len(non_suna_agents) + logger.debug(f"Account {account_id} has {current_count} custom agents (excluding Suna defaults)") + + try: + from services.billing import get_subscription_tier + tier_name = await get_subscription_tier(client, account_id) + logger.debug(f"Account {account_id} subscription tier: {tier_name}") + except Exception as billing_error: + logger.warning(f"Could not get subscription tier for {account_id}: {str(billing_error)}, defaulting to free") + tier_name = 'free' + + agent_limit = config.AGENT_LIMITS.get(tier_name, config.AGENT_LIMITS['free']) + + can_create = current_count < agent_limit + + result = { + 'can_create': can_create, + 'current_count': current_count, + 'limit': agent_limit, + 'tier_name': tier_name + } + + try: + await Cache.set(f"agent_count_limit:{account_id}", result, ttl=300) + except Exception as cache_error: + logger.warning(f"Cache write failed for agent count limit {account_id}: {str(cache_error)}") + + logger.info(f"Account {account_id} has {current_count}/{agent_limit} agents (tier: {tier_name}) - can_create: {can_create}") + + return result + + except Exception as e: + logger.error(f"Error checking agent count limit for account {account_id}: {str(e)}", exc_info=True) + return { + 'can_create': True, + 'current_count': 0, + 'limit': config.AGENT_LIMITS['free'], + 'tier_name': 'free' + } diff --git a/backend/services/billing.py b/backend/services/billing.py index 9e6bac26..0205d9bf 100644 --- a/backend/services/billing.py +++ b/backend/services/billing.py @@ -592,6 +592,30 @@ async def can_use_model(client, user_id: str, model_name: str): return False, f"Your current subscription plan does not include access to {model_name}. Please upgrade your subscription or choose from your available models: {', '.join(allowed_models)}", allowed_models +async def get_subscription_tier(client, user_id: str) -> str: + try: + subscription = await get_user_subscription(user_id) + + if not subscription: + return 'free' + + price_id = None + if subscription.get('items') and subscription['items'].get('data') and len(subscription['items']['data']) > 0: + price_id = subscription['items']['data'][0]['price']['id'] + else: + price_id = subscription.get('price_id', config.STRIPE_FREE_TIER_ID) + + tier_info = SUBSCRIPTION_TIERS.get(price_id) + if tier_info: + return tier_info['name'] + + logger.warning(f"Unknown price_id {price_id} for user {user_id}, defaulting to free tier") + return 'free' + + except Exception as e: + logger.error(f"Error getting subscription tier for user {user_id}: {str(e)}") + return 'free' + async def check_billing_status(client, user_id: str) -> Tuple[bool, str, Optional[Dict]]: """ Check if a user can run agents based on their subscription and usage. @@ -634,8 +658,6 @@ async def check_billing_status(client, user_id: str) -> Tuple[bool, str, Optiona # Calculate current month's usage current_usage = await calculate_monthly_usage(client, user_id) - # TODO: also do user's AAL check - # Check if within limits if current_usage >= tier_info['cost']: return False, f"Monthly limit of {tier_info['cost']} dollars reached. Please upgrade your plan or wait until next month.", subscription diff --git a/backend/templates/api.py b/backend/templates/api.py index 5a6678c2..9b45c60d 100644 --- a/backend/templates/api.py +++ b/backend/templates/api.py @@ -325,15 +325,22 @@ async def install_template( request: InstallTemplateRequest, user_id: str = Depends(get_current_user_id_from_jwt) ): - """ - Install a template as a new agent instance. - - Requires: - - User must have access to the template (own it or it's public) - """ try: - # Validate template access first - template = await validate_template_access_and_get(request.template_id, user_id) + await validate_template_access_and_get(request.template_id, user_id) + client = await db.client + from agent.utils import check_agent_count_limit + limit_check = await check_agent_count_limit(client, user_id) + + if not limit_check['can_create']: + error_detail = { + "message": f"Maximum of {limit_check['limit']} agents allowed for your current plan. You have {limit_check['current_count']} agents.", + "current_count": limit_check['current_count'], + "limit": limit_check['limit'], + "tier_name": limit_check['tier_name'], + "error_code": "AGENT_LIMIT_EXCEEDED" + } + logger.warning(f"Agent limit exceeded for account {user_id}: {limit_check['current_count']}/{limit_check['limit']} agents") + raise HTTPException(status_code=402, detail=error_detail) logger.info(f"User {user_id} installing template {request.template_id}") @@ -362,7 +369,6 @@ async def install_template( ) except HTTPException: - # Re-raise HTTP exceptions from our validation functions raise except TemplateInstallationError as e: logger.warning(f"Template installation failed: {e}") diff --git a/backend/templates/installation_service.py b/backend/templates/installation_service.py index 553643a9..bcf095d6 100644 --- a/backend/templates/installation_service.py +++ b/backend/templates/installation_service.py @@ -107,6 +107,9 @@ class InstallationService: await self._increment_download_count(template.template_id) + from utils.cache import Cache + await Cache.invalidate(f"agent_count_limit:{request.account_id}") + agent_name = request.instance_name or f"{template.name} (from marketplace)" logger.info(f"Successfully installed template {template.template_id} as agent {agent_id}") diff --git a/backend/utils/config.py b/backend/utils/config.py index 5d6d5d81..9344f9ef 100644 --- a/backend/utils/config.py +++ b/backend/utils/config.py @@ -270,6 +270,30 @@ class Configuration: # Agent execution limits (can be overridden via environment variable) _MAX_PARALLEL_AGENT_RUNS_ENV: Optional[str] = None + + # Agent limits per billing tier + AGENT_LIMITS = { + 'free': 2, + 'tier_2_20': 5, + 'tier_6_50': 20, + 'tier_12_100': 20, + 'tier_25_200': 100, + 'tier_50_400': 100, + 'tier_125_800': 100, + 'tier_200_1000': 100, + # Yearly plans have same limits as monthly + 'tier_2_20_yearly': 5, + 'tier_6_50_yearly': 20, + 'tier_12_100_yearly': 20, + 'tier_25_200_yearly': 100, + 'tier_50_400_yearly': 100, + 'tier_125_800_yearly': 100, + 'tier_200_1000_yearly': 100, + # Yearly commitment plans + 'tier_2_17_yearly_commitment': 5, + 'tier_6_42_yearly_commitment': 20, + 'tier_25_170_yearly_commitment': 100, + } @property def MAX_PARALLEL_AGENT_RUNS(self) -> int: diff --git a/frontend/src/app/(dashboard)/agents/page.tsx b/frontend/src/app/(dashboard)/agents/page.tsx index 18670aec..efe7232a 100644 --- a/frontend/src/app/(dashboard)/agents/page.tsx +++ b/frontend/src/app/(dashboard)/agents/page.tsx @@ -23,6 +23,8 @@ import { PublishDialog } from '@/components/agents/custom-agents-page/publish-di import { LoadingSkeleton } from '@/components/agents/custom-agents-page/loading-skeleton'; import { NewAgentDialog } from '@/components/agents/new-agent-dialog'; import { MarketplaceAgentPreviewDialog } from '@/components/agents/marketplace-agent-preview-dialog'; +import { AgentCountLimitDialog } from '@/components/agents/agent-count-limit-dialog'; +import { AgentCountLimitError } from '@/lib/api'; type ViewMode = 'grid' | 'list'; type AgentSortOption = 'name' | 'created_at' | 'updated_at' | 'tools_count'; @@ -86,6 +88,8 @@ export default function AgentsPage() { const [publishingAgentId, setPublishingAgentId] = useState(null); const [showNewAgentDialog, setShowNewAgentDialog] = useState(false); + const [showAgentLimitDialog, setShowAgentLimitDialog] = useState(false); + const [agentLimitError, setAgentLimitError] = useState(null); const activeTab = useMemo(() => { return searchParams.get('tab') || 'my-agents'; @@ -146,7 +150,6 @@ export default function AgentsPage() { if (marketplaceTemplates) { marketplaceTemplates.forEach(template => { - const item: MarketplaceTemplate = { id: template.template_id, creator_id: template.creator_id, @@ -174,14 +177,12 @@ export default function AgentsPage() { item.creator_name?.toLowerCase().includes(searchLower); })(); - if (!matchesSearch) return; // Skip items that don't match search + if (!matchesSearch) return; - // Always add user's own templates to mineItems for the "mine" filter if (user?.id === template.creator_id) { mineItems.push(item); } - // Categorize all templates (including user's own) for the "all" view if (template.is_kortix_team) { kortixItems.push(item); } else { @@ -392,6 +393,12 @@ export default function AgentsPage() { } catch (error: any) { console.error('Installation error:', error); + if (error instanceof AgentCountLimitError) { + setAgentLimitError(error); + setShowAgentLimitDialog(true); + return; + } + if (error.message?.includes('already in your library')) { toast.error('This agent is already in your library'); } else if (error.message?.includes('Credential profile not found')) { @@ -628,6 +635,15 @@ export default function AgentsPage() { onInstall={handlePreviewInstall} isInstalling={installingItemId === selectedItem?.id} /> + {agentLimitError && ( + + )} ); diff --git a/frontend/src/components/agents/agent-count-limit-dialog.tsx b/frontend/src/components/agents/agent-count-limit-dialog.tsx new file mode 100644 index 00000000..5d822949 --- /dev/null +++ b/frontend/src/components/agents/agent-count-limit-dialog.tsx @@ -0,0 +1,86 @@ +'use client'; + +import React from 'react'; +import { AlertTriangle } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { PricingSection } from '@/components/home/sections/pricing-section'; +import { Separator } from '@/components/ui/separator'; + +interface AgentCountLimitDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + currentCount: number; + limit: number; + tierName: string; +} + +export const AgentCountLimitDialog: React.FC = ({ + open, + onOpenChange, + currentCount, + limit, + tierName, +}) => { + const returnUrl = typeof window !== 'undefined' ? window.location.href : '/'; + + const getNextTierRecommendation = () => { + if (tierName === 'free') { + return { + name: 'Plus', + price: '$20/month', + agentLimit: 5, + }; + } else if (tierName.includes('tier_2_20')) { + return { + name: 'Pro', + price: '$50/month', + agentLimit: 20, + }; + } else if (tierName.includes('tier_6_50')) { + return { + name: 'Business', + price: '$200/month', + agentLimit: 100, + }; + } + return null; + }; + + const nextTier = getNextTierRecommendation(); + + return ( + + + +
+
+ +
+ + Agent Limit Reached + +
+ + You've reached the maximum number of agents allowed on your current plan. + +
+
+ +
+
+
+ ); +}; \ No newline at end of file diff --git a/frontend/src/components/agents/json-import-dialog.tsx b/frontend/src/components/agents/json-import-dialog.tsx index ad416f10..22b28468 100644 --- a/frontend/src/components/agents/json-import-dialog.tsx +++ b/frontend/src/components/agents/json-import-dialog.tsx @@ -11,6 +11,8 @@ import { ProfileConnector } from './installation/streamlined-profile-connector'; import { CustomServerStep } from './installation/custom-server-step'; import type { SetupStep } from './installation/types'; import { useAnalyzeJsonForImport, useImportAgentFromJson, type JsonAnalysisResult, type JsonImportResult } from '@/hooks/react-query/agents/use-json-import'; +import { AgentCountLimitDialog } from './agent-count-limit-dialog'; +import { AgentCountLimitError } from '@/lib/api'; import { cn } from '@/lib/utils'; interface JsonImportDialogProps { @@ -34,6 +36,8 @@ export const JsonImportDialog: React.FC = ({ const [currentStep, setCurrentStep] = useState(0); const [profileMappings, setProfileMappings] = useState>({}); const [customMcpConfigs, setCustomMcpConfigs] = useState>>({}); + const [showAgentLimitDialog, setShowAgentLimitDialog] = useState(false); + const [agentLimitError, setAgentLimitError] = useState(null); const analyzeJsonMutation = useAnalyzeJsonForImport(); const importJsonMutation = useImportAgentFromJson(); @@ -47,6 +51,8 @@ export const JsonImportDialog: React.FC = ({ setCurrentStep(0); setProfileMappings({}); setCustomMcpConfigs({}); + setShowAgentLimitDialog(false); + setAgentLimitError(null); }, []); useEffect(() => { @@ -215,6 +221,13 @@ export const JsonImportDialog: React.FC = ({ } onOpenChange(false); } + }, + onError: (error) => { + if (error instanceof AgentCountLimitError) { + setAgentLimitError(error); + setShowAgentLimitDialog(true); + onOpenChange(false); + } } } ); @@ -380,7 +393,6 @@ export const JsonImportDialog: React.FC = ({ Import Agent from JSON - {analysis && !analysis.requires_setup && step === 'paste' && ( @@ -389,7 +401,6 @@ export const JsonImportDialog: React.FC = ({ )} - {analysis && analysis.requires_setup && step === 'paste' && ( @@ -400,7 +411,6 @@ export const JsonImportDialog: React.FC = ({ )} - {analyzeJsonMutation.isError && step === 'paste' && ( @@ -409,7 +419,6 @@ export const JsonImportDialog: React.FC = ({ )} - {importJsonMutation.isError && (step === 'setup' || step === 'importing') && ( @@ -418,11 +427,19 @@ export const JsonImportDialog: React.FC = ({ )} - {step === 'paste' && renderPasteStep()} {step === 'setup' && renderSetupStep()} {step === 'importing' && renderImportingStep()} + {agentLimitError && ( + + )} ); }; \ No newline at end of file diff --git a/frontend/src/components/agents/new-agent-dialog.tsx b/frontend/src/components/agents/new-agent-dialog.tsx index 39dff436..c8938b2b 100644 --- a/frontend/src/components/agents/new-agent-dialog.tsx +++ b/frontend/src/components/agents/new-agent-dialog.tsx @@ -1,6 +1,6 @@ 'use client'; -import React, { useState, useRef } from 'react'; +import React, { useState, useRef, useEffect } from 'react'; import { Loader2, Plus, FileJson, Code } from 'lucide-react'; import { AlertDialog, @@ -14,6 +14,8 @@ import { } from '@/components/ui/alert-dialog'; import { useCreateNewAgent } from '@/hooks/react-query/agents/use-agents'; import { JsonImportDialog } from './json-import-dialog'; +import { AgentCountLimitDialog } from './agent-count-limit-dialog'; +import { AgentCountLimitError } from '@/lib/api'; import { toast } from 'sonner'; interface NewAgentDialogProps { @@ -25,19 +27,43 @@ interface NewAgentDialogProps { export function NewAgentDialog({ open, onOpenChange, onSuccess }: NewAgentDialogProps) { const [showJsonImport, setShowJsonImport] = useState(false); const [jsonImportText, setJsonImportText] = useState(''); + const [showAgentLimitDialog, setShowAgentLimitDialog] = useState(false); + const [agentLimitError, setAgentLimitError] = useState(null); const fileInputRef = useRef(null); const createNewAgentMutation = useCreateNewAgent(); + useEffect(() => { + console.log('[DEBUG] NewAgentDialog state:', { + agentLimitError: !!agentLimitError, + showAgentLimitDialog, + errorDetail: agentLimitError?.detail + }); + }, [agentLimitError, showAgentLimitDialog]); + const handleCreateNewAgent = () => { + console.log('[DEBUG] Creating new agent...'); createNewAgentMutation.mutate(undefined, { onSuccess: () => { + console.log('[DEBUG] Agent created successfully'); onOpenChange(false); onSuccess?.(); }, - onError: () => { - // Keep dialog open on error so user can see the error and try again - // The useCreateNewAgent hook already shows error toasts + onError: (error) => { + console.log('[DEBUG] Error creating agent:', error); + console.log('[DEBUG] Error type:', typeof error); + console.log('[DEBUG] Error constructor:', error.constructor.name); + console.log('[DEBUG] Is AgentCountLimitError?', error instanceof AgentCountLimitError); + + if (error instanceof AgentCountLimitError) { + console.log('[DEBUG] Setting agent limit error state'); + setAgentLimitError(error); + setShowAgentLimitDialog(true); + onOpenChange(false); + } else { + console.log('[DEBUG] Not an agent limit error, keeping dialog open'); + toast.error(error instanceof Error ? error.message : 'Failed to create agent'); + } } }); }; @@ -147,6 +173,16 @@ export function NewAgentDialog({ open, onOpenChange, onSuccess }: NewAgentDialog onSuccess?.(); }} /> + + {agentLimitError && ( + + )} ); } \ No newline at end of file diff --git a/frontend/src/components/home/sections/pricing-section.tsx b/frontend/src/components/home/sections/pricing-section.tsx index fe63f487..809dc8d1 100644 --- a/frontend/src/components/home/sections/pricing-section.tsx +++ b/frontend/src/components/home/sections/pricing-section.tsx @@ -566,7 +566,7 @@ function PricingTier({ variant={buttonVariant || 'default'} className={cn( 'w-full font-medium transition-all duration-200', - isCompact || insideDialog ? 'h-8 rounded-md text-xs' : 'h-10 rounded-full text-sm', + isCompact || insideDialog ? 'h-8 text-xs' : 'h-10 rounded-full text-sm', buttonClassName, isPlanLoading && 'animate-pulse', )} @@ -584,13 +584,17 @@ interface PricingSectionProps { showTitleAndTabs?: boolean; hideFree?: boolean; insideDialog?: boolean; + showInfo?: boolean; + noPadding?: boolean; } export function PricingSection({ returnUrl = typeof window !== 'undefined' ? window.location.href : '/', showTitleAndTabs = true, hideFree = false, - insideDialog = false + insideDialog = false, + showInfo = true, + noPadding = false, }: PricingSectionProps) { const [deploymentType, setDeploymentType] = useState<'cloud' | 'self-hosted'>( 'cloud', @@ -598,18 +602,14 @@ export function PricingSection({ const { data: subscriptionData, isLoading: isFetchingPlan, error: subscriptionQueryError, refetch: refetchSubscription } = useSubscription(); const subCommitmentQuery = useSubscriptionCommitment(subscriptionData?.subscription_id); - // Derive authentication and subscription status from the hook data const isAuthenticated = !!subscriptionData && subscriptionQueryError === null; const currentSubscription = subscriptionData || null; - // Determine default billing period based on user's current subscription const getDefaultBillingPeriod = useCallback((): 'monthly' | 'yearly' | 'yearly_commitment' => { if (!isAuthenticated || !currentSubscription) { - // Default to yearly_commitment for non-authenticated users (the new yearly plans) return 'yearly_commitment'; } - // Find current tier to determine if user is on monthly, yearly, or yearly commitment plan const currentTier = siteConfig.cloudPricingItems.find( (p) => p.stripePriceId === currentSubscription.price_id || p.yearlyStripePriceId === currentSubscription.price_id || @@ -685,7 +685,7 @@ export function PricingSection({ return (
{showTitleAndTabs && ( <> @@ -747,14 +747,15 @@ export function PricingSection({ ))} )} -
-

- What are AI tokens? Tokens are units of text that AI models process. - Your plan includes credits to spend on various AI models - the more complex the task, - the more tokens used. -

-
- + {showInfo && ( +
+

+ What are AI tokens? Tokens are units of text that AI models process. + Your plan includes credits to spend on various AI models - the more complex the task, + the more tokens used. +

+
+ )}
); diff --git a/frontend/src/hooks/react-query/agents/use-agents.ts b/frontend/src/hooks/react-query/agents/use-agents.ts index 08703f19..30e74ae6 100644 --- a/frontend/src/hooks/react-query/agents/use-agents.ts +++ b/frontend/src/hooks/react-query/agents/use-agents.ts @@ -42,6 +42,14 @@ export const useCreateAgent = () => { queryClient.setQueryData(agentKeys.detail(data.agent_id), data); toast.success('Agent created successfully'); }, + onError: async (error) => { + const { AgentCountLimitError } = await import('@/lib/api'); + if (error instanceof AgentCountLimitError) { + return; + } + console.error('Error creating agent:', error); + toast.error(error instanceof Error ? error.message : 'Failed to create agent'); + }, } )(); }; diff --git a/frontend/src/hooks/react-query/agents/use-json-import.ts b/frontend/src/hooks/react-query/agents/use-json-import.ts index 59733a5d..cc770498 100644 --- a/frontend/src/hooks/react-query/agents/use-json-import.ts +++ b/frontend/src/hooks/react-query/agents/use-json-import.ts @@ -73,7 +73,18 @@ export const useImportAgentFromJson = () => { const response = await backendApi.post('/agents/json/import', request); return response.data; } catch (error: any) { - // Extract error message from different error formats + const errorData = error.response?.data; + const isAgentLimitError = (error.response?.status === 402) && ( + errorData?.error_code === 'AGENT_LIMIT_EXCEEDED' || + errorData?.detail?.error_code === 'AGENT_LIMIT_EXCEEDED' + ); + + if (isAgentLimitError) { + const { AgentCountLimitError } = await import('@/lib/api'); + const errorDetail = errorData?.detail || errorData; + throw new AgentCountLimitError(error.response.status, errorDetail); + } + const message = error.response?.data?.detail || error.message || 'Failed to import agent'; throw new Error(message); } diff --git a/frontend/src/hooks/react-query/agents/utils.ts b/frontend/src/hooks/react-query/agents/utils.ts index 03ba9ba8..71649dd4 100644 --- a/frontend/src/hooks/react-query/agents/utils.ts +++ b/frontend/src/hooks/react-query/agents/utils.ts @@ -256,6 +256,25 @@ export const createAgent = async (agentData: AgentCreateRequest): Promise if (!response.ok) { const errorData = await response.json().catch(() => ({ message: 'Unknown error' })); + console.log('[DEBUG] Error response data:', errorData); + console.log('[DEBUG] Response status:', response.status); + console.log('[DEBUG] Error code check:', errorData.error_code); + + // Check for agent limit error - handle both direct error_code and nested in detail + const isAgentLimitError = (response.status === 402) && ( + errorData.error_code === 'AGENT_LIMIT_EXCEEDED' || + errorData.detail?.error_code === 'AGENT_LIMIT_EXCEEDED' + ); + + if (isAgentLimitError) { + console.log('[DEBUG] Converting to AgentCountLimitError'); + const { AgentCountLimitError } = await import('@/lib/api'); + // Use the nested detail if it exists, otherwise use the errorData directly + const errorDetail = errorData.detail || errorData; + throw new AgentCountLimitError(response.status, errorDetail); + } + + console.log('[DEBUG] Throwing generic error'); throw new Error(errorData.message || `HTTP ${response.status}: ${response.statusText}`); } diff --git a/frontend/src/hooks/react-query/secure-mcp/use-secure-mcp.ts b/frontend/src/hooks/react-query/secure-mcp/use-secure-mcp.ts index 1da3e523..9946235e 100644 --- a/frontend/src/hooks/react-query/secure-mcp/use-secure-mcp.ts +++ b/frontend/src/hooks/react-query/secure-mcp/use-secure-mcp.ts @@ -447,6 +447,22 @@ export function useInstallTemplate() { if (!response.ok) { const errorData = await response.json().catch(() => ({ message: 'Unknown error' })); + console.log('[DEBUG] Template install error data:', errorData); + + // Check for agent limit error - handle both direct error_code and nested in detail + const isAgentLimitError = (response.status === 402) && ( + errorData.error_code === 'AGENT_LIMIT_EXCEEDED' || + errorData.detail?.error_code === 'AGENT_LIMIT_EXCEEDED' + ); + + if (isAgentLimitError) { + console.log('[DEBUG] Converting template install to AgentCountLimitError'); + const { AgentCountLimitError } = await import('@/lib/api'); + // Use the nested detail if it exists, otherwise use the errorData directly + const errorDetail = errorData.detail || errorData; + throw new AgentCountLimitError(response.status, errorDetail); + } + throw new Error(errorData.message || `HTTP ${response.status}: ${response.statusText}`); } diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index ac91d97a..58b8f020 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -59,6 +59,36 @@ export class AgentRunLimitError extends Error { } } +export class AgentCountLimitError extends Error { + status: number; + detail: { + message: string; + current_count: number; + limit: number; + tier_name: string; + error_code: string; + }; + + constructor( + status: number, + detail: { + message: string; + current_count: number; + limit: number; + tier_name: string; + error_code: string; + [key: string]: any; + }, + message?: string, + ) { + super(message || detail.message || `Agent Count Limit Exceeded: ${status}`); + this.name = 'AgentCountLimitError'; + this.status = status; + this.detail = detail; + Object.setPrototypeOf(this, AgentCountLimitError.prototype); + } +} + export class NoAccessTokenAvailableError extends Error { constructor(message?: string, options?: { cause?: Error }) { super(message || 'No access token available', options); diff --git a/frontend/src/lib/home.tsx b/frontend/src/lib/home.tsx index bcc0ebdb..6cbc3a53 100644 --- a/frontend/src/lib/home.tsx +++ b/frontend/src/lib/home.tsx @@ -124,6 +124,7 @@ export const siteConfig = { hours: '60 min', features: [ '$5 free AI tokens included', + '2 custom agents', 'Public projects', 'Basic Models', 'Community support', @@ -145,6 +146,7 @@ export const siteConfig = { hours: '2 hours', features: [ '$20 AI token credits/month', + '5 custom agents', 'Private projects', 'Premium AI Models', 'Community support', @@ -168,6 +170,7 @@ export const siteConfig = { hours: '6 hours', features: [ '$50 AI token credits/month', + '20 custom agents', 'Private projects', 'Premium AI Models', 'Community support', @@ -190,6 +193,7 @@ export const siteConfig = { hours: '12 hours', features: [ '$100 AI token credits/month', + '20 custom agents', 'Private projects', 'Premium AI Models', 'Community support', @@ -212,6 +216,7 @@ export const siteConfig = { hours: '25 hours', features: [ '$200 AI token credits/month', + '100 custom agents', 'Private projects', 'Premium AI Models', 'Priority support',