Multi-agent coordination layer: - AgentRegistry interface + MemoryRegistry (heartbeat, reap, discovery) - TaskRouter interface + DefaultRouter (capability matching, load balancing) - Dispatcher orchestrates registry + router + allowance for task dispatch Co-Authored-By: Virgil <virgil@lethean.io>
148 lines
4.4 KiB
Go
148 lines
4.4 KiB
Go
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
|
|
}
|