package council import ( "bytes" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "fmt" "hash/fnv" "math/rand" "net/http" "strings" "sync" "time" "chorus/internal/persona" ) // CouncilOpportunity represents a council formation opportunity from WHOOSH. type CouncilOpportunity struct { CouncilID string `json:"council_id"` ProjectName string `json:"project_name"` Repository string `json:"repository"` ProjectBrief string `json:"project_brief"` CoreRoles []CouncilRole `json:"core_roles"` OptionalRoles []CouncilRole `json:"optional_roles"` UCXLAddress string `json:"ucxl_address"` FormationDeadline time.Time `json:"formation_deadline"` CreatedAt time.Time `json:"created_at"` Metadata map[string]interface{} `json:"metadata"` } // CouncilRole represents a single role available within a council. type CouncilRole struct { RoleName string `json:"role_name"` AgentName string `json:"agent_name"` Required bool `json:"required"` RequiredSkills []string `json:"required_skills"` Description string `json:"description"` } // RoleProfile mirrors WHOOSH role profile metadata included in claim responses. type RoleProfile struct { RoleName string `json:"role_name"` DisplayName string `json:"display_name"` PromptKey string `json:"prompt_key"` PromptPack string `json:"prompt_pack"` Capabilities []string `json:"capabilities"` BriefRoutingHint string `json:"brief_routing_hint"` DefaultBriefOwner bool `json:"default_brief_owner"` } // CouncilBrief carries the high-level brief metadata for an activated council. type CouncilBrief struct { CouncilID string `json:"council_id"` RoleName string `json:"role_name"` ProjectName string `json:"project_name"` Repository string `json:"repository"` Summary string `json:"summary"` BriefURL string `json:"brief_url"` IssueID *int64 `json:"issue_id"` UCXLAddress string `json:"ucxl_address"` ExpectedArtifacts []string `json:"expected_artifacts"` HMMMTopic string `json:"hmmm_topic"` } // RoleAssignment keeps track of the agent's current council engagement. type RoleAssignment struct { CouncilID string RoleName string UCXLAddress string AssignedAt time.Time Profile RoleProfile Brief *CouncilBrief Persona *persona.Persona PersonaHash string } var ErrRoleConflict = errors.New("council role already claimed") const defaultModelProvider = "ollama" // Manager handles council opportunity evaluation, persona preparation, and brief handoff. type Manager struct { agentID string agentName string endpoint string p2pAddr string capabilities []string httpClient *http.Client personaLoader *persona.Loader mu sync.Mutex currentAssignment *RoleAssignment } // NewManager creates a new council manager. func NewManager(agentID, agentName, endpoint, p2pAddr string, capabilities []string) *Manager { loader, err := persona.NewLoader() if err != nil { fmt.Printf("⚠️ Persona loader initialisation failed: %v\n", err) } return &Manager{ agentID: agentID, agentName: agentName, endpoint: endpoint, p2pAddr: p2pAddr, capabilities: capabilities, httpClient: &http.Client{Timeout: 10 * time.Second}, personaLoader: loader, } } // AgentID returns the agent's identifier. func (m *Manager) AgentID() string { return m.agentID } // EvaluateOpportunity analyzes a council opportunity and decides whether to claim a role. func (m *Manager) EvaluateOpportunity(opportunity *CouncilOpportunity, whooshEndpoint string) error { fmt.Printf("\n🤔 Evaluating council opportunity for: %s\n", opportunity.ProjectName) if current := m.currentAssignmentSnapshot(); current != nil { fmt.Printf(" ℹ️ Agent already assigned to council %s as %s; skipping new claims\n", current.CouncilID, current.RoleName) return nil } const maxAttempts = 10 const retryDelay = 3 * time.Second var attemptedAtLeastOne bool for attempt := 1; attempt <= maxAttempts; attempt++ { assignment, attemptedCore, err := m.tryClaimRoles(opportunity.CoreRoles, opportunity, whooshEndpoint, "CORE") attemptedAtLeastOne = attemptedAtLeastOne || attemptedCore if assignment != nil { m.setCurrentAssignment(assignment) return nil } if err != nil && !errors.Is(err, ErrRoleConflict) { return err } assignment, attemptedOptional, err := m.tryClaimRoles(opportunity.OptionalRoles, opportunity, whooshEndpoint, "OPTIONAL") attemptedAtLeastOne = attemptedAtLeastOne || attemptedOptional if assignment != nil { m.setCurrentAssignment(assignment) return nil } if err != nil && !errors.Is(err, ErrRoleConflict) { return err } if !attemptedAtLeastOne { fmt.Printf(" ✗ No suitable roles found for this agent\n\n") return nil } fmt.Printf(" ↻ Attempt %d did not secure a council role; retrying in %s...\n", attempt, retryDelay) time.Sleep(retryDelay) } return fmt.Errorf("exhausted council role claim attempts for council %s", opportunity.CouncilID) } func (m *Manager) tryClaimRoles(roles []CouncilRole, opportunity *CouncilOpportunity, whooshEndpoint string, roleType string) (*RoleAssignment, bool, error) { var attempted bool // Shuffle roles deterministically per agent+council to reduce herd on the first role shuffled := append([]CouncilRole(nil), roles...) if len(shuffled) > 1 { h := fnv.New64a() _, _ = h.Write([]byte(m.agentID)) _, _ = h.Write([]byte(opportunity.CouncilID)) seed := int64(h.Sum64()) r := rand.New(rand.NewSource(seed)) r.Shuffle(len(shuffled), func(i, j int) { shuffled[i], shuffled[j] = shuffled[j], shuffled[i] }) } for _, role := range shuffled { if !m.shouldClaimRole(role, opportunity) { continue } attempted = true fmt.Printf(" ✓ Attempting to claim %s role: %s (%s)\n", roleType, role.AgentName, role.RoleName) assignment, err := m.claimRole(opportunity, role, whooshEndpoint) if assignment != nil { return assignment, attempted, nil } if errors.Is(err, ErrRoleConflict) { fmt.Printf(" ⚠️ Role %s already claimed by another agent, trying next role...\n", role.RoleName) continue } if err != nil { return nil, attempted, err } } return nil, attempted, nil } func (m *Manager) shouldClaimRole(role CouncilRole, _ *CouncilOpportunity) bool { if m.hasActiveAssignment() { return false } // TODO: implement capability-based selection. For now, opportunistically claim any available role. return true } func (m *Manager) claimRole(opportunity *CouncilOpportunity, role CouncilRole, whooshEndpoint string) (*RoleAssignment, error) { claimURL := fmt.Sprintf("%s/api/v1/councils/%s/claims", strings.TrimRight(whooshEndpoint, "/"), opportunity.CouncilID) claim := map[string]interface{}{ "agent_id": m.agentID, "agent_name": m.agentName, "role_name": role.RoleName, "capabilities": m.capabilities, "confidence": 0.75, // TODO: calculate based on capability match quality. "reasoning": fmt.Sprintf("Agent has capabilities matching role: %s", role.RoleName), "endpoint": m.endpoint, "p2p_addr": m.p2pAddr, } payload, err := json.Marshal(claim) if err != nil { return nil, fmt.Errorf("failed to marshal claim: %w", err) } req, err := http.NewRequest(http.MethodPost, claimURL, bytes.NewBuffer(payload)) if err != nil { return nil, fmt.Errorf("failed to create claim request: %w", err) } req.Header.Set("Content-Type", "application/json") resp, err := m.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("failed to send claim: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { var errorResp map[string]interface{} _ = json.NewDecoder(resp.Body).Decode(&errorResp) if resp.StatusCode == http.StatusConflict { reason := "role already claimed" if msg, ok := errorResp["error"].(string); ok && msg != "" { reason = msg } return nil, fmt.Errorf("%w: %s", ErrRoleConflict, reason) } return nil, fmt.Errorf("claim rejected (status %d): %v", resp.StatusCode, errorResp) } var claimResp roleClaimResponse if err := json.NewDecoder(resp.Body).Decode(&claimResp); err != nil { return nil, fmt.Errorf("failed to decode claim response: %w", err) } assignment := &RoleAssignment{ CouncilID: opportunity.CouncilID, RoleName: role.RoleName, UCXLAddress: claimResp.UCXLAddress, Profile: claimResp.RoleProfile, } if t, err := time.Parse(time.RFC3339, claimResp.AssignedAt); err == nil { assignment.AssignedAt = t } if claimResp.CouncilBrief != nil { assignment.Brief = claimResp.CouncilBrief } fmt.Printf("\n✅ ROLE CLAIM ACCEPTED!\n") fmt.Printf(" Council ID: %s\n", opportunity.CouncilID) fmt.Printf(" Role: %s (%s)\n", role.AgentName, role.RoleName) fmt.Printf(" UCXL: %s\n", assignment.UCXLAddress) fmt.Printf(" Assigned At: %s\n", claimResp.AssignedAt) if err := m.preparePersonaAndAck(opportunity.CouncilID, role.RoleName, &assignment.Profile, claimResp.CouncilBrief, whooshEndpoint, assignment); err != nil { fmt.Printf(" ⚠️ Persona preparation encountered an issue: %v\n", err) } fmt.Printf("\n") return assignment, nil } func (m *Manager) preparePersonaAndAck(councilID, roleName string, profile *RoleProfile, brief *CouncilBrief, whooshEndpoint string, assignment *RoleAssignment) error { if m.personaLoader == nil { return m.sendPersonaAck(councilID, roleName, whooshEndpoint, nil, "", "failed", []string{"persona loader unavailable"}) } promptKey := profile.PromptKey if promptKey == "" { promptKey = roleName } personaCapabilities := profile.Capabilities personaCapabilities = append([]string{}, personaCapabilities...) personaEntry, err := m.personaLoader.Compose(promptKey, profile.DisplayName, "", personaCapabilities) if err != nil { return m.sendPersonaAck(councilID, roleName, whooshEndpoint, nil, "", "failed", []string{err.Error()}) } hash := sha256.Sum256([]byte(personaEntry.SystemPrompt)) personaHash := hex.EncodeToString(hash[:]) assignment.Persona = personaEntry assignment.PersonaHash = personaHash if err := m.sendPersonaAck(councilID, roleName, whooshEndpoint, personaEntry, personaHash, "loaded", nil); err != nil { return err } return nil } func (m *Manager) sendPersonaAck(councilID, roleName, whooshEndpoint string, personaEntry *persona.Persona, personaHash string, status string, errs []string) error { ackURL := fmt.Sprintf("%s/api/v1/councils/%s/roles/%s/personas", strings.TrimRight(whooshEndpoint, "/"), councilID, roleName) payload := map[string]interface{}{ "agent_id": m.agentID, "status": status, "model_provider": defaultModelProvider, "capabilities": m.capabilities, "metadata": map[string]interface{}{ "endpoint": m.endpoint, "p2p_addr": m.p2pAddr, "agent_name": m.agentName, }, } if personaEntry != nil { payload["system_prompt_hash"] = personaHash payload["model_name"] = personaEntry.Model if len(personaEntry.Capabilities) > 0 { payload["capabilities"] = personaEntry.Capabilities } } if len(errs) > 0 { payload["errors"] = errs } body, err := json.Marshal(payload) if err != nil { return fmt.Errorf("marshal persona ack: %w", err) } req, err := http.NewRequest(http.MethodPost, ackURL, bytes.NewBuffer(body)) if err != nil { return fmt.Errorf("create persona ack request: %w", err) } req.Header.Set("Content-Type", "application/json") resp, err := m.httpClient.Do(req) if err != nil { return fmt.Errorf("send persona ack: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusAccepted { return fmt.Errorf("persona ack rejected with status %d", resp.StatusCode) } fmt.Printf(" 📫 Persona status '%s' acknowledged by WHOOSH\n", status) return nil } // HandleCouncilBrief records the design brief assigned to this agent once WHOOSH dispatches it. func (m *Manager) HandleCouncilBrief(councilID, roleName string, brief *CouncilBrief) { if brief == nil { return } m.mu.Lock() defer m.mu.Unlock() if m.currentAssignment == nil { fmt.Printf("⚠️ Received council brief for %s (%s) but agent has no active assignment\n", councilID, roleName) return } if m.currentAssignment.CouncilID != councilID || !strings.EqualFold(m.currentAssignment.RoleName, roleName) { fmt.Printf("⚠️ Received council brief for %s (%s) but agent is assigned to %s (%s)\n", councilID, roleName, m.currentAssignment.CouncilID, m.currentAssignment.RoleName) return } brief.CouncilID = councilID brief.RoleName = roleName m.currentAssignment.Brief = brief fmt.Printf("📦 Design brief received for council %s (%s)\n", councilID, roleName) if brief.BriefURL != "" { fmt.Printf(" Brief URL: %s\n", brief.BriefURL) } if brief.Summary != "" { fmt.Printf(" Summary: %s\n", brief.Summary) } if len(brief.ExpectedArtifacts) > 0 { fmt.Printf(" Expected Artifacts: %v\n", brief.ExpectedArtifacts) } if brief.HMMMTopic != "" { fmt.Printf(" HMMM Topic: %s\n", brief.HMMMTopic) } } func (m *Manager) hasActiveAssignment() bool { m.mu.Lock() defer m.mu.Unlock() return m.currentAssignment != nil } func (m *Manager) setCurrentAssignment(assignment *RoleAssignment) { m.mu.Lock() defer m.mu.Unlock() m.currentAssignment = assignment } func (m *Manager) currentAssignmentSnapshot() *RoleAssignment { m.mu.Lock() defer m.mu.Unlock() return m.currentAssignment } // GetCurrentAssignment returns the current role assignment (public accessor) func (m *Manager) GetCurrentAssignment() *RoleAssignment { return m.currentAssignmentSnapshot() } // roleClaimResponse mirrors WHOOSH role claim response payload. type roleClaimResponse struct { Status string `json:"status"` CouncilID string `json:"council_id"` RoleName string `json:"role_name"` UCXLAddress string `json:"ucxl_address"` AssignedAt string `json:"assigned_at"` RoleProfile RoleProfile `json:"role_profile"` CouncilBrief *CouncilBrief `json:"council_brief"` PersonaStatus string `json:"persona_status"` }