package protocol import ( "context" "fmt" "strings" "sync" "time" "github.com/libp2p/go-libp2p/core/peer" "github.com/libp2p/go-libp2p/core/peerstore" ) // PeerCapability represents the capabilities of a peer type PeerCapability struct { PeerID peer.ID `json:"peer_id"` Agent string `json:"agent"` Role string `json:"role"` Capabilities []string `json:"capabilities"` Models []string `json:"models"` Specialization string `json:"specialization"` Project string `json:"project"` LastSeen time.Time `json:"last_seen"` Status string `json:"status"` // "online", "busy", "offline" Metadata map[string]string `json:"metadata"` } // PeerAddress represents a resolved peer address type PeerAddress struct { PeerID peer.ID `json:"peer_id"` Addresses []string `json:"addresses"` Priority int `json:"priority"` Metadata map[string]interface{} `json:"metadata"` } // ResolutionResult represents the result of address resolution type ResolutionResult struct { URI *BzzzURI `json:"uri"` Peers []*PeerAddress `json:"peers"` ResolvedAt time.Time `json:"resolved_at"` ResolutionTTL time.Duration `json:"ttl"` Strategy string `json:"strategy"` } // ResolutionStrategy defines how to resolve addresses type ResolutionStrategy string const ( StrategyExact ResolutionStrategy = "exact" // Exact match only StrategyBestMatch ResolutionStrategy = "best_match" // Best available match StrategyLoadBalance ResolutionStrategy = "load_balance" // Load balance among matches StrategyPriority ResolutionStrategy = "priority" // Highest priority first ) // Resolver handles semantic address resolution type Resolver struct { // Peer capability registry capabilities map[peer.ID]*PeerCapability capMutex sync.RWMutex // Address resolution cache cache map[string]*ResolutionResult cacheMutex sync.RWMutex cacheTTL time.Duration // Configuration defaultStrategy ResolutionStrategy maxPeersPerResult int // Peerstore for address information peerstore peerstore.Peerstore } // NewResolver creates a new semantic address resolver func NewResolver(peerstore peerstore.Peerstore, opts ...ResolverOption) *Resolver { r := &Resolver{ capabilities: make(map[peer.ID]*PeerCapability), cache: make(map[string]*ResolutionResult), cacheTTL: 5 * time.Minute, defaultStrategy: StrategyBestMatch, maxPeersPerResult: 5, peerstore: peerstore, } for _, opt := range opts { opt(r) } // Start background cleanup go r.startCleanup() return r } // ResolverOption configures the resolver type ResolverOption func(*Resolver) // WithCacheTTL sets the cache TTL func WithCacheTTL(ttl time.Duration) ResolverOption { return func(r *Resolver) { r.cacheTTL = ttl } } // WithDefaultStrategy sets the default resolution strategy func WithDefaultStrategy(strategy ResolutionStrategy) ResolverOption { return func(r *Resolver) { r.defaultStrategy = strategy } } // WithMaxPeersPerResult sets the maximum peers per result func WithMaxPeersPerResult(max int) ResolverOption { return func(r *Resolver) { r.maxPeersPerResult = max } } // RegisterPeer registers a peer's capabilities func (r *Resolver) RegisterPeer(peerID peer.ID, capability *PeerCapability) { r.capMutex.Lock() defer r.capMutex.Unlock() capability.PeerID = peerID capability.LastSeen = time.Now() r.capabilities[peerID] = capability // Clear relevant cache entries r.invalidateCache() } // UnregisterPeer removes a peer from the registry func (r *Resolver) UnregisterPeer(peerID peer.ID) { r.capMutex.Lock() defer r.capMutex.Unlock() delete(r.capabilities, peerID) // Clear relevant cache entries r.invalidateCache() } // UpdatePeerStatus updates a peer's status func (r *Resolver) UpdatePeerStatus(peerID peer.ID, status string) { r.capMutex.Lock() defer r.capMutex.Unlock() if cap, exists := r.capabilities[peerID]; exists { cap.Status = status cap.LastSeen = time.Now() } } // Resolve resolves a CHORUS:// URI to peer addresses func (r *Resolver) Resolve(ctx context.Context, uri *BzzzURI, strategy ...ResolutionStrategy) (*ResolutionResult, error) { if uri == nil { return nil, fmt.Errorf("nil URI") } // Determine strategy resolveStrategy := r.defaultStrategy if len(strategy) > 0 { resolveStrategy = strategy[0] } // Check cache first cacheKey := r.getCacheKey(uri, resolveStrategy) if result := r.getFromCache(cacheKey); result != nil { return result, nil } // Perform resolution result, err := r.resolveURI(ctx, uri, resolveStrategy) if err != nil { return nil, err } // Cache result r.cacheResult(cacheKey, result) return result, nil } // ResolveString resolves a CHORUS:// URI string to peer addresses func (r *Resolver) ResolveString(ctx context.Context, uriStr string, strategy ...ResolutionStrategy) (*ResolutionResult, error) { uri, err := ParseBzzzURI(uriStr) if err != nil { return nil, fmt.Errorf("failed to parse URI: %w", err) } return r.Resolve(ctx, uri, strategy...) } // resolveURI performs the actual URI resolution func (r *Resolver) resolveURI(ctx context.Context, uri *BzzzURI, strategy ResolutionStrategy) (*ResolutionResult, error) { r.capMutex.RLock() defer r.capMutex.RUnlock() var matchingPeers []*PeerCapability // Find matching peers for _, cap := range r.capabilities { if r.peerMatches(cap, uri) { matchingPeers = append(matchingPeers, cap) } } if len(matchingPeers) == 0 { return &ResolutionResult{ URI: uri, Peers: []*PeerAddress{}, ResolvedAt: time.Now(), ResolutionTTL: r.cacheTTL, Strategy: string(strategy), }, nil } // Apply resolution strategy selectedPeers := r.applyStrategy(matchingPeers, strategy) // Convert to peer addresses var peerAddresses []*PeerAddress for i, cap := range selectedPeers { if i >= r.maxPeersPerResult { break } addr := &PeerAddress{ PeerID: cap.PeerID, Priority: r.calculatePriority(cap, uri), Metadata: map[string]interface{}{ "agent": cap.Agent, "role": cap.Role, "specialization": cap.Specialization, "status": cap.Status, "last_seen": cap.LastSeen, }, } // Get addresses from peerstore if r.peerstore != nil { addrs := r.peerstore.Addrs(cap.PeerID) for _, ma := range addrs { addr.Addresses = append(addr.Addresses, ma.String()) } } peerAddresses = append(peerAddresses, addr) } return &ResolutionResult{ URI: uri, Peers: peerAddresses, ResolvedAt: time.Now(), ResolutionTTL: r.cacheTTL, Strategy: string(strategy), }, nil } // peerMatches checks if a peer matches the URI criteria func (r *Resolver) peerMatches(cap *PeerCapability, uri *BzzzURI) bool { // Check if peer is online if cap.Status == "offline" { return false } // Check agent match if !IsWildcard(uri.Agent) && !componentMatches(uri.Agent, cap.Agent) { return false } // Check role match if !IsWildcard(uri.Role) && !componentMatches(uri.Role, cap.Role) { return false } // Check project match (if specified in metadata) if !IsWildcard(uri.Project) { if project, exists := cap.Metadata["project"]; exists { if !componentMatches(uri.Project, project) { return false } } } // Check task capabilities (if peer has relevant capabilities) if !IsWildcard(uri.Task) { taskMatches := false for _, capability := range cap.Capabilities { if componentMatches(uri.Task, capability) { taskMatches = true break } } if !taskMatches { // Also check specialization if !componentMatches(uri.Task, cap.Specialization) { return false } } } return true } // applyStrategy applies the resolution strategy to matching peers func (r *Resolver) applyStrategy(peers []*PeerCapability, strategy ResolutionStrategy) []*PeerCapability { switch strategy { case StrategyExact: // Return only exact matches (already filtered) return peers case StrategyPriority: // Sort by priority (calculated based on specificity and status) return r.sortByPriority(peers) case StrategyLoadBalance: // Sort by load (prefer less busy peers) return r.sortByLoad(peers) case StrategyBestMatch: fallthrough default: // Sort by best match score return r.sortByMatch(peers) } } // sortByPriority sorts peers by priority score func (r *Resolver) sortByPriority(peers []*PeerCapability) []*PeerCapability { // Simple priority: online > working > busy, then by last seen result := make([]*PeerCapability, len(peers)) copy(result, peers) // Sort by status priority and recency for i := 0; i < len(result)-1; i++ { for j := i + 1; j < len(result); j++ { iPriority := r.getStatusPriority(result[i].Status) jPriority := r.getStatusPriority(result[j].Status) if iPriority < jPriority || (iPriority == jPriority && result[i].LastSeen.Before(result[j].LastSeen)) { result[i], result[j] = result[j], result[i] } } } return result } // sortByLoad sorts peers by current load (prefer less busy) func (r *Resolver) sortByLoad(peers []*PeerCapability) []*PeerCapability { result := make([]*PeerCapability, len(peers)) copy(result, peers) // Sort by status (ready > working > busy) for i := 0; i < len(result)-1; i++ { for j := i + 1; j < len(result); j++ { iLoad := r.getLoadScore(result[i].Status) jLoad := r.getLoadScore(result[j].Status) if iLoad > jLoad { result[i], result[j] = result[j], result[i] } } } return result } // sortByMatch sorts peers by match quality func (r *Resolver) sortByMatch(peers []*PeerCapability) []*PeerCapability { result := make([]*PeerCapability, len(peers)) copy(result, peers) // Simple sorting - prefer online status and recent activity for i := 0; i < len(result)-1; i++ { for j := i + 1; j < len(result); j++ { if r.getMatchScore(result[i]) < r.getMatchScore(result[j]) { result[i], result[j] = result[j], result[i] } } } return result } // Helper functions for scoring func (r *Resolver) getStatusPriority(status string) int { switch status { case "ready": return 3 case "working": return 2 case "busy": return 1 default: return 0 } } func (r *Resolver) getLoadScore(status string) int { switch status { case "ready": return 0 // Lowest load case "working": return 1 case "busy": return 2 // Highest load default: return 3 } } func (r *Resolver) getMatchScore(cap *PeerCapability) int { score := 0 // Status contribution score += r.getStatusPriority(cap.Status) * 10 // Recency contribution (more recent = higher score) timeSince := time.Since(cap.LastSeen) if timeSince < time.Minute { score += 5 } else if timeSince < time.Hour { score += 3 } else if timeSince < 24*time.Hour { score += 1 } // Capability count contribution score += len(cap.Capabilities) return score } // calculatePriority calculates priority for a peer address func (r *Resolver) calculatePriority(cap *PeerCapability, uri *BzzzURI) int { priority := 0 // Exact matches get higher priority if cap.Agent == uri.Agent { priority += 4 } if cap.Role == uri.Role { priority += 3 } if cap.Specialization == uri.Task { priority += 2 } // Status-based priority priority += r.getStatusPriority(cap.Status) return priority } // Cache management func (r *Resolver) getCacheKey(uri *BzzzURI, strategy ResolutionStrategy) string { return fmt.Sprintf("%s:%s", uri.String(), strategy) } func (r *Resolver) getFromCache(key string) *ResolutionResult { r.cacheMutex.RLock() defer r.cacheMutex.RUnlock() if result, exists := r.cache[key]; exists { // Check if result is still valid if time.Since(result.ResolvedAt) < result.ResolutionTTL { return result } // Remove expired entry delete(r.cache, key) } return nil } func (r *Resolver) cacheResult(key string, result *ResolutionResult) { r.cacheMutex.Lock() defer r.cacheMutex.Unlock() r.cache[key] = result } func (r *Resolver) invalidateCache() { r.cacheMutex.Lock() defer r.cacheMutex.Unlock() // Clear entire cache on capability changes r.cache = make(map[string]*ResolutionResult) } // startCleanup starts background cache cleanup func (r *Resolver) startCleanup() { ticker := time.NewTicker(time.Minute) defer ticker.Stop() for range ticker.C { r.cleanupCache() } } func (r *Resolver) cleanupCache() { r.cacheMutex.Lock() defer r.cacheMutex.Unlock() now := time.Now() for key, result := range r.cache { if now.Sub(result.ResolvedAt) > result.ResolutionTTL { delete(r.cache, key) } } } // GetPeerCapabilities returns all registered peer capabilities func (r *Resolver) GetPeerCapabilities() map[peer.ID]*PeerCapability { r.capMutex.RLock() defer r.capMutex.RUnlock() result := make(map[peer.ID]*PeerCapability) for id, cap := range r.capabilities { result[id] = cap } return result } // GetPeerCapability returns a specific peer's capabilities func (r *Resolver) GetPeerCapability(peerID peer.ID) (*PeerCapability, bool) { r.capMutex.RLock() defer r.capMutex.RUnlock() cap, exists := r.capabilities[peerID] return cap, exists } // Close shuts down the resolver func (r *Resolver) Close() error { // Clear all data r.capMutex.Lock() r.capabilities = make(map[peer.ID]*PeerCapability) r.capMutex.Unlock() r.cacheMutex.Lock() r.cache = make(map[string]*ResolutionResult) r.cacheMutex.Unlock() return nil }