Files
CHORUS/pkg/ucxl/decision_publisher.go
2025-09-20 23:21:35 +10:00

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
}
}