package agentic import ( "errors" "slices" "sort" ) // ErrNoEligibleAgent is returned when no agent matches the task requirements. var ErrNoEligibleAgent = errors.New("no eligible agent for task") // TaskRouter selects an agent for a given task from a list of candidates. type TaskRouter interface { // Route picks the best agent for the task and returns its ID. // Returns ErrNoEligibleAgent if no agent qualifies. Route(task *Task, agents []AgentInfo) (string, error) } // DefaultRouter implements TaskRouter with capability matching and load-based // scoring. For critical priority tasks it picks the least-loaded agent directly. type DefaultRouter struct{} // NewDefaultRouter creates a new DefaultRouter. func NewDefaultRouter() *DefaultRouter { return &DefaultRouter{} } // Route selects the best agent for the task: // 1. Filter by availability (Available, or Busy with capacity). // 2. Filter by capabilities (task.Labels must be a subset of agent.Capabilities). // 3. For critical tasks, pick the least-loaded agent. // 4. For other tasks, score by load ratio and pick the highest-scored agent. // 5. Ties are broken by agent ID (alphabetical) for determinism. func (r *DefaultRouter) Route(task *Task, agents []AgentInfo) (string, error) { eligible := r.filterEligible(task, agents) if len(eligible) == 0 { return "", ErrNoEligibleAgent } if task.Priority == PriorityCritical { return r.leastLoaded(eligible), nil } return r.highestScored(eligible), nil } // filterEligible returns agents that are available (or busy with room) and // possess all required capabilities. func (r *DefaultRouter) filterEligible(task *Task, agents []AgentInfo) []AgentInfo { var result []AgentInfo for _, a := range agents { if !r.isAvailable(a) { continue } if !r.hasCapabilities(a, task.Labels) { continue } result = append(result, a) } return result } // isAvailable returns true if the agent can accept work. func (r *DefaultRouter) isAvailable(a AgentInfo) bool { switch a.Status { case AgentAvailable: return true case AgentBusy: // Busy agents can still accept work if they have capacity. return a.MaxLoad == 0 || a.CurrentLoad < a.MaxLoad default: return false } } // hasCapabilities checks that the agent has all required labels. If the task // has no labels, any agent qualifies. func (r *DefaultRouter) hasCapabilities(a AgentInfo, labels []string) bool { if len(labels) == 0 { return true } for _, label := range labels { if !slices.Contains(a.Capabilities, label) { return false } } return true } // leastLoaded picks the agent with the lowest CurrentLoad. Ties are broken by // agent ID (alphabetical). func (r *DefaultRouter) leastLoaded(agents []AgentInfo) string { // Sort: lowest load first, then by ID for determinism. sort.Slice(agents, func(i, j int) bool { if agents[i].CurrentLoad != agents[j].CurrentLoad { return agents[i].CurrentLoad < agents[j].CurrentLoad } return agents[i].ID < agents[j].ID }) return agents[0].ID } // highestScored picks the agent with the highest availability score. // Score = 1.0 - (CurrentLoad / MaxLoad). If MaxLoad is 0, score is 1.0. // Ties are broken by agent ID (alphabetical). func (r *DefaultRouter) highestScored(agents []AgentInfo) string { type scored struct { id string score float64 } scores := make([]scored, len(agents)) for i, a := range agents { s := 1.0 if a.MaxLoad > 0 { s = 1.0 - float64(a.CurrentLoad)/float64(a.MaxLoad) } scores[i] = scored{id: a.ID, score: s} } // Sort: highest score first, then by ID for determinism. sort.Slice(scores, func(i, j int) bool { if scores[i].score != scores[j].score { return scores[i].score > scores[j].score } return scores[i].id < scores[j].id }) return scores[0].id }