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