385 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			385 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package ucxl
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"encoding/json"
 | |
| 	"fmt"
 | |
| 	"log"
 | |
| 	"time"
 | |
| 
 | |
| 	"chorus/pkg/config"
 | |
| 	"chorus/pkg/storage"
 | |
| )
 | |
| 
 | |
| // DecisionPublisher handles publishing task completion decisions to encrypted DHT storage
 | |
| type DecisionPublisher struct {
 | |
| 	ctx        context.Context
 | |
| 	config     *config.Config
 | |
| 	dhtStorage storage.UCXLStorage
 | |
| 	nodeID     string
 | |
| 	agentName  string
 | |
| }
 | |
| 
 | |
| // NewDecisionPublisher creates a new decision publisher
 | |
| func NewDecisionPublisher(
 | |
| 	ctx context.Context,
 | |
| 	config *config.Config,
 | |
| 	dhtStorage storage.UCXLStorage,
 | |
| 	nodeID string,
 | |
| 	agentName string,
 | |
| ) *DecisionPublisher {
 | |
| 	return &DecisionPublisher{
 | |
| 		ctx:        ctx,
 | |
| 		config:     config,
 | |
| 		dhtStorage: dhtStorage,
 | |
| 		nodeID:     nodeID,
 | |
| 		agentName:  agentName,
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // TaskDecision represents a decision made by an agent upon task completion
 | |
| type TaskDecision struct {
 | |
| 	Agent         string                 `json:"agent"`
 | |
| 	Role          string                 `json:"role"`
 | |
| 	Project       string                 `json:"project"`
 | |
| 	Task          string                 `json:"task"`
 | |
| 	Decision      string                 `json:"decision"`
 | |
| 	Context       map[string]interface{} `json:"context"`
 | |
| 	Timestamp     time.Time              `json:"timestamp"`
 | |
| 	Success       bool                   `json:"success"`
 | |
| 	ErrorMessage  string                 `json:"error_message,omitempty"`
 | |
| 	FilesModified []string               `json:"files_modified,omitempty"`
 | |
| 	LinesChanged  int                    `json:"lines_changed,omitempty"`
 | |
| 	TestResults   *TestResults           `json:"test_results,omitempty"`
 | |
| 	Dependencies  []string               `json:"dependencies,omitempty"`
 | |
| 	NextSteps     []string               `json:"next_steps,omitempty"`
 | |
| }
 | |
| 
 | |
| // TestResults captures test execution results
 | |
| type TestResults struct {
 | |
| 	Passed      int      `json:"passed"`
 | |
| 	Failed      int      `json:"failed"`
 | |
| 	Skipped     int      `json:"skipped"`
 | |
| 	Coverage    float64  `json:"coverage,omitempty"`
 | |
| 	FailedTests []string `json:"failed_tests,omitempty"`
 | |
| }
 | |
| 
 | |
| // PublishTaskDecision publishes a task completion decision to the DHT
 | |
| func (dp *DecisionPublisher) PublishTaskDecision(decision *TaskDecision) error {
 | |
| 	// Ensure required fields
 | |
| 	if decision.Agent == "" {
 | |
| 		decision.Agent = dp.agentName
 | |
| 	}
 | |
| 	if decision.Role == "" {
 | |
| 		decision.Role = dp.config.Agent.Role
 | |
| 	}
 | |
| 	if decision.Project == "" {
 | |
| 		if project := dp.config.Agent.Project; project != "" {
 | |
| 			decision.Project = project
 | |
| 		} else {
 | |
| 			decision.Project = "chorus"
 | |
| 		}
 | |
| 	}
 | |
| 	if decision.Timestamp.IsZero() {
 | |
| 		decision.Timestamp = time.Now()
 | |
| 	}
 | |
| 
 | |
| 	log.Printf("📤 Publishing task decision: %s/%s/%s", decision.Agent, decision.Project, decision.Task)
 | |
| 
 | |
| 	// Generate UCXL address
 | |
| 	ucxlAddress, err := dp.generateUCXLAddress(decision)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to generate UCXL address: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	// Serialize decision content
 | |
| 	decisionContent, err := json.MarshalIndent(decision, "", "  ")
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to serialize decision: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	// Store in encrypted DHT
 | |
| 	err = dp.dhtStorage.StoreUCXLContent(
 | |
| 		ucxlAddress,
 | |
| 		decisionContent,
 | |
| 		decision.Role,
 | |
| 		"decision",
 | |
| 	)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to store decision in DHT: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	// Announce content availability
 | |
| 	if err := dp.dhtStorage.AnnounceContent(ucxlAddress); err != nil {
 | |
| 		log.Printf("⚠️ Failed to announce decision content: %v", err)
 | |
| 		// Don't fail the publish operation for announcement failure
 | |
| 	}
 | |
| 
 | |
| 	log.Printf("✅ Published task decision: %s", ucxlAddress)
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // PublishTaskCompletion publishes a simple task completion without detailed context
 | |
| func (dp *DecisionPublisher) PublishTaskCompletion(
 | |
| 	taskName string,
 | |
| 	success bool,
 | |
| 	summary string,
 | |
| 	filesModified []string,
 | |
| ) error {
 | |
| 	decision := &TaskDecision{
 | |
| 		Task:          taskName,
 | |
| 		Decision:      summary,
 | |
| 		Success:       success,
 | |
| 		FilesModified: filesModified,
 | |
| 		Context: map[string]interface{}{
 | |
| 			"completion_type": "basic",
 | |
| 			"node_id":         dp.nodeID,
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	return dp.PublishTaskDecision(decision)
 | |
| }
 | |
| 
 | |
| // PublishCodeDecision publishes a coding decision with technical context
 | |
| func (dp *DecisionPublisher) PublishCodeDecision(
 | |
| 	taskName string,
 | |
| 	decision string,
 | |
| 	filesModified []string,
 | |
| 	linesChanged int,
 | |
| 	testResults *TestResults,
 | |
| 	dependencies []string,
 | |
| ) error {
 | |
| 	taskDecision := &TaskDecision{
 | |
| 		Task:          taskName,
 | |
| 		Decision:      decision,
 | |
| 		Success:       testResults == nil || testResults.Failed == 0,
 | |
| 		FilesModified: filesModified,
 | |
| 		LinesChanged:  linesChanged,
 | |
| 		TestResults:   testResults,
 | |
| 		Dependencies:  dependencies,
 | |
| 		Context: map[string]interface{}{
 | |
| 			"decision_type": "code",
 | |
| 			"node_id":       dp.nodeID,
 | |
| 			"language":      dp.detectLanguage(filesModified),
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	return dp.PublishTaskDecision(taskDecision)
 | |
| }
 | |
| 
 | |
| // PublishArchitecturalDecision publishes a high-level architectural decision
 | |
| func (dp *DecisionPublisher) PublishArchitecturalDecision(
 | |
| 	taskName string,
 | |
| 	decision string,
 | |
| 	rationale string,
 | |
| 	alternatives []string,
 | |
| 	implications []string,
 | |
| 	nextSteps []string,
 | |
| ) error {
 | |
| 	taskDecision := &TaskDecision{
 | |
| 		Task:      taskName,
 | |
| 		Decision:  decision,
 | |
| 		Success:   true,
 | |
| 		NextSteps: nextSteps,
 | |
| 		Context: map[string]interface{}{
 | |
| 			"decision_type": "architecture",
 | |
| 			"rationale":     rationale,
 | |
| 			"alternatives":  alternatives,
 | |
| 			"implications":  implications,
 | |
| 			"node_id":       dp.nodeID,
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	return dp.PublishTaskDecision(taskDecision)
 | |
| }
 | |
| 
 | |
| // generateUCXLAddress creates a UCXL address for the decision
 | |
| func (dp *DecisionPublisher) generateUCXLAddress(decision *TaskDecision) (string, error) {
 | |
| 	address := &Address{
 | |
| 		Agent:   decision.Agent,
 | |
| 		Role:    decision.Role,
 | |
| 		Project: decision.Project,
 | |
| 		Task:    decision.Task,
 | |
| 		TemporalSegment: TemporalSegment{
 | |
| 			Type: TemporalLatest, // Latest decision for this agent/role/project/task
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	return address.String(), nil
 | |
| }
 | |
| 
 | |
| // detectLanguage attempts to detect the programming language from modified files
 | |
| func (dp *DecisionPublisher) detectLanguage(files []string) string {
 | |
| 	languageMap := map[string]string{
 | |
| 		".go":   "go",
 | |
| 		".py":   "python",
 | |
| 		".js":   "javascript",
 | |
| 		".ts":   "typescript",
 | |
| 		".rs":   "rust",
 | |
| 		".java": "java",
 | |
| 		".c":    "c",
 | |
| 		".cpp":  "cpp",
 | |
| 		".cs":   "csharp",
 | |
| 		".php":  "php",
 | |
| 		".rb":   "ruby",
 | |
| 		".yaml": "yaml",
 | |
| 		".yml":  "yaml",
 | |
| 		".json": "json",
 | |
| 		".md":   "markdown",
 | |
| 	}
 | |
| 
 | |
| 	languageCounts := make(map[string]int)
 | |
| 
 | |
| 	for _, file := range files {
 | |
| 		for ext, lang := range languageMap {
 | |
| 			if len(file) > len(ext) && file[len(file)-len(ext):] == ext {
 | |
| 				languageCounts[lang]++
 | |
| 				break
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Return the most common language
 | |
| 	maxCount := 0
 | |
| 	primaryLanguage := "unknown"
 | |
| 	for lang, count := range languageCounts {
 | |
| 		if count > maxCount {
 | |
| 			maxCount = count
 | |
| 			primaryLanguage = lang
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return primaryLanguage
 | |
| }
 | |
| 
 | |
| // QueryRecentDecisions retrieves recent decisions from the DHT
 | |
| func (dp *DecisionPublisher) QueryRecentDecisions(
 | |
| 	agent string,
 | |
| 	role string,
 | |
| 	project string,
 | |
| 	limit int,
 | |
| 	since time.Time,
 | |
| ) ([]*storage.UCXLMetadata, error) {
 | |
| 	query := &storage.SearchQuery{
 | |
| 		Agent:        agent,
 | |
| 		Role:         role,
 | |
| 		Project:      project,
 | |
| 		ContentType:  "decision",
 | |
| 		CreatedAfter: since,
 | |
| 		Limit:        limit,
 | |
| 	}
 | |
| 
 | |
| 	return dp.dhtStorage.SearchContent(query)
 | |
| }
 | |
| 
 | |
| // GetDecisionContent retrieves and decrypts a specific decision
 | |
| func (dp *DecisionPublisher) GetDecisionContent(ucxlAddress string) (*TaskDecision, error) {
 | |
| 	content, metadata, err := dp.dhtStorage.RetrieveUCXLContent(ucxlAddress)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to retrieve decision content: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	var decision TaskDecision
 | |
| 	if err := json.Unmarshal(content, &decision); err != nil {
 | |
| 		return nil, fmt.Errorf("failed to parse decision content: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	log.Printf("📥 Retrieved decision: %s (creator: %s)", ucxlAddress, metadata.CreatorRole)
 | |
| 	return &decision, nil
 | |
| }
 | |
| 
 | |
| // SubscribeToDecisions sets up a subscription to new decisions (placeholder for future pubsub)
 | |
| func (dp *DecisionPublisher) SubscribeToDecisions(
 | |
| 	roleFilter string,
 | |
| 	callback func(*TaskDecision, *storage.UCXLMetadata),
 | |
| ) error {
 | |
| 	// This is a placeholder for future pubsub implementation
 | |
| 	// For now, we'll implement a simple polling mechanism
 | |
| 
 | |
| 	go func() {
 | |
| 		ticker := time.NewTicker(30 * time.Second)
 | |
| 		defer ticker.Stop()
 | |
| 
 | |
| 		lastCheck := time.Now()
 | |
| 
 | |
| 		for {
 | |
| 			select {
 | |
| 			case <-dp.ctx.Done():
 | |
| 				return
 | |
| 			case <-ticker.C:
 | |
| 				// Query for recent decisions
 | |
| 				decisions, err := dp.QueryRecentDecisions("", roleFilter, "", 10, lastCheck)
 | |
| 				if err != nil {
 | |
| 					log.Printf("⚠️ Failed to query recent decisions: %v", err)
 | |
| 					continue
 | |
| 				}
 | |
| 
 | |
| 				// Process new decisions
 | |
| 				for _, metadata := range decisions {
 | |
| 					decision, err := dp.GetDecisionContent(metadata.Address)
 | |
| 					if err != nil {
 | |
| 						log.Printf("⚠️ Failed to get decision content: %v", err)
 | |
| 						continue
 | |
| 					}
 | |
| 
 | |
| 					callback(decision, metadata)
 | |
| 				}
 | |
| 
 | |
| 				lastCheck = time.Now()
 | |
| 			}
 | |
| 		}
 | |
| 	}()
 | |
| 
 | |
| 	log.Printf("🔔 Subscribed to decisions for role: %s", roleFilter)
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // PublishSystemStatus publishes current system status as a decision
 | |
| func (dp *DecisionPublisher) PublishSystemStatus(
 | |
| 	status string,
 | |
| 	metrics map[string]interface{},
 | |
| 	healthChecks map[string]bool,
 | |
| ) error {
 | |
| 	decision := &TaskDecision{
 | |
| 		Task:     "system_status",
 | |
| 		Decision: status,
 | |
| 		Success:  dp.allHealthChecksPass(healthChecks),
 | |
| 		Context: map[string]interface{}{
 | |
| 			"decision_type": "system",
 | |
| 			"metrics":       metrics,
 | |
| 			"health_checks": healthChecks,
 | |
| 			"node_id":       dp.nodeID,
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	return dp.PublishTaskDecision(decision)
 | |
| }
 | |
| 
 | |
| // allHealthChecksPass checks if all health checks are passing
 | |
| func (dp *DecisionPublisher) allHealthChecksPass(healthChecks map[string]bool) bool {
 | |
| 	for _, passing := range healthChecks {
 | |
| 		if !passing {
 | |
| 			return false
 | |
| 		}
 | |
| 	}
 | |
| 	return true
 | |
| }
 | |
| 
 | |
| // GetPublisherMetrics returns metrics about the decision publisher
 | |
| func (dp *DecisionPublisher) GetPublisherMetrics() map[string]interface{} {
 | |
| 	dhtMetrics := dp.dhtStorage.GetMetrics()
 | |
| 	project := dp.config.Agent.Project
 | |
| 	if project == "" {
 | |
| 		project = "chorus"
 | |
| 	}
 | |
| 
 | |
| 	return map[string]interface{}{
 | |
| 		"node_id":      dp.nodeID,
 | |
| 		"agent_name":   dp.agentName,
 | |
| 		"current_role": dp.config.Agent.Role,
 | |
| 		"project":      project,
 | |
| 		"dht_metrics":  dhtMetrics,
 | |
| 		"last_publish": time.Now(), // This would be tracked in a real implementation
 | |
| 	}
 | |
| }
 | 
