package agentic import ( "sync" "time" ) // AgentStatus represents the availability state of an agent. type AgentStatus string const ( // AgentAvailable indicates the agent is ready to accept tasks. AgentAvailable AgentStatus = "available" // AgentBusy indicates the agent is working but may accept more tasks. AgentBusy AgentStatus = "busy" // AgentOffline indicates the agent has not sent a heartbeat recently. AgentOffline AgentStatus = "offline" ) // AgentInfo describes a registered agent and its current state. type AgentInfo struct { // ID is the unique identifier for the agent. ID string `json:"id"` // Name is the human-readable name of the agent. Name string `json:"name"` // Capabilities lists what the agent can handle (e.g. "go", "testing", "frontend"). Capabilities []string `json:"capabilities,omitempty"` // Status is the current availability state. Status AgentStatus `json:"status"` // LastHeartbeat is the last time the agent reported in. LastHeartbeat time.Time `json:"last_heartbeat"` // CurrentLoad is the number of active jobs the agent is running. CurrentLoad int `json:"current_load"` // MaxLoad is the maximum concurrent jobs the agent supports. 0 means unlimited. MaxLoad int `json:"max_load"` } // AgentRegistry manages the set of known agents and their health. type AgentRegistry interface { // Register adds or updates an agent in the registry. Register(agent AgentInfo) error // Deregister removes an agent from the registry. Deregister(id string) error // Get returns a copy of the agent info for the given ID. Get(id string) (AgentInfo, error) // List returns a copy of all registered agents. List() []AgentInfo // Heartbeat updates the agent's LastHeartbeat timestamp and sets status // to Available if the agent was previously Offline. Heartbeat(id string) error // Reap marks agents as Offline if their last heartbeat is older than ttl. // Returns the IDs of agents that were reaped. Reap(ttl time.Duration) []string } // MemoryRegistry is an in-memory AgentRegistry implementation guarded by a // read-write mutex. It uses copy-on-read semantics consistent with MemoryStore. type MemoryRegistry struct { mu sync.RWMutex agents map[string]*AgentInfo } // NewMemoryRegistry creates a new in-memory agent registry. func NewMemoryRegistry() *MemoryRegistry { return &MemoryRegistry{ agents: make(map[string]*AgentInfo), } } // Register adds or updates an agent in the registry. Returns an error if the // agent ID is empty. func (r *MemoryRegistry) Register(agent AgentInfo) error { if agent.ID == "" { return &APIError{Code: 400, Message: "agent ID is required"} } r.mu.Lock() defer r.mu.Unlock() cp := agent r.agents[agent.ID] = &cp return nil } // Deregister removes an agent from the registry. Returns an error if the agent // is not found. func (r *MemoryRegistry) Deregister(id string) error { r.mu.Lock() defer r.mu.Unlock() if _, ok := r.agents[id]; !ok { return &APIError{Code: 404, Message: "agent not found: " + id} } delete(r.agents, id) return nil } // Get returns a copy of the agent info for the given ID. Returns an error if // the agent is not found. func (r *MemoryRegistry) Get(id string) (AgentInfo, error) { r.mu.RLock() defer r.mu.RUnlock() a, ok := r.agents[id] if !ok { return AgentInfo{}, &APIError{Code: 404, Message: "agent not found: " + id} } return *a, nil } // List returns a copy of all registered agents. func (r *MemoryRegistry) List() []AgentInfo { r.mu.RLock() defer r.mu.RUnlock() result := make([]AgentInfo, 0, len(r.agents)) for _, a := range r.agents { result = append(result, *a) } return result } // Heartbeat updates the agent's LastHeartbeat timestamp. If the agent was // Offline, it transitions to Available. func (r *MemoryRegistry) Heartbeat(id string) error { r.mu.Lock() defer r.mu.Unlock() a, ok := r.agents[id] if !ok { return &APIError{Code: 404, Message: "agent not found: " + id} } a.LastHeartbeat = time.Now().UTC() if a.Status == AgentOffline { a.Status = AgentAvailable } return nil } // Reap marks agents as Offline if their last heartbeat is older than ttl. // Returns the IDs of agents that were reaped. func (r *MemoryRegistry) Reap(ttl time.Duration) []string { r.mu.Lock() defer r.mu.Unlock() var reaped []string now := time.Now().UTC() for id, a := range r.agents { if a.Status != AgentOffline && now.Sub(a.LastHeartbeat) > ttl { a.Status = AgentOffline reaped = append(reaped, id) } } return reaped }