795 lines
27 KiB
Go
795 lines
27 KiB
Go
package group
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"sync"
|
|
"time"
|
|
|
|
"synapse/pkg/types"
|
|
)
|
|
|
|
// =============================================================================
|
|
// Group Protocol Handler
|
|
// =============================================================================
|
|
|
|
// GroupTaskHandler is the interface for handling group task execution
|
|
type GroupTaskHandler interface {
|
|
// ExecuteSubtask executes a subtask and returns the result
|
|
ExecuteSubtask(ctx context.Context, subtaskID string, payload map[string]interface{}) (any, error)
|
|
|
|
// MergeResults merges multiple subtask results
|
|
MergeResults(results []types.SubtaskResult, strategy types.MergeStrategy) (any, error)
|
|
|
|
// ValidateTaskSplit validates if a task can be split
|
|
ValidateTaskSplit(taskType string, requirements map[string]interface{}) ([]types.Subtask, bool)
|
|
}
|
|
|
|
// =============================================================================
|
|
// GroupProtocol represents the group protocol handler
|
|
// =============================================================================
|
|
|
|
type GroupProtocol struct {
|
|
mu sync.RWMutex
|
|
|
|
// Agent identity
|
|
agentEndpoint types.AgentEndpoint
|
|
|
|
// Group state
|
|
groups map[string]*types.AgentGroup
|
|
|
|
// Membership
|
|
memberships map[string]string // groupID -> member's role in group
|
|
|
|
// Task handler
|
|
taskHandler GroupTaskHandler
|
|
|
|
// Configuration
|
|
config *GroupConfig
|
|
}
|
|
|
|
// GroupConfig holds group protocol configuration
|
|
type GroupConfig struct {
|
|
MaxGroupSize int
|
|
MaxSubtasks int
|
|
ConsensusTimeout time.Duration
|
|
TaskTimeout time.Duration
|
|
}
|
|
|
|
// DefaultGroupConfig returns default group configuration
|
|
func DefaultGroupConfig() *GroupConfig {
|
|
return &GroupConfig{
|
|
MaxGroupSize: 20,
|
|
MaxSubtasks: 50,
|
|
ConsensusTimeout: 5 * time.Minute,
|
|
TaskTimeout: 10 * time.Minute,
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// NewGroupProtocol creates a new group protocol handler
|
|
// =============================================================================
|
|
|
|
func NewGroupProtocol(agentEndpoint types.AgentEndpoint, config *GroupConfig) *GroupProtocol {
|
|
if config == nil {
|
|
config = DefaultGroupConfig()
|
|
}
|
|
return &GroupProtocol{
|
|
agentEndpoint: agentEndpoint,
|
|
groups: make(map[string]*types.AgentGroup),
|
|
memberships: make(map[string]string),
|
|
config: config,
|
|
}
|
|
}
|
|
|
|
// SetTaskHandler sets the group task handler
|
|
func (g *GroupProtocol) SetTaskHandler(h GroupTaskHandler) {
|
|
g.taskHandler = h
|
|
}
|
|
|
|
// =============================================================================
|
|
// HandleMessage processes an incoming group message
|
|
// =============================================================================
|
|
|
|
func (g *GroupProtocol) HandleMessage(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
|
g.mu.Lock()
|
|
defer g.mu.Unlock()
|
|
|
|
if err := g.validateMessage(msg); err != nil {
|
|
a2aErr, ok := err.(*types.SynapseError)
|
|
if !ok {
|
|
a2aErr = types.NewSynapseError(types.ErrMessageFormatInvalid, err.Error(), nil)
|
|
}
|
|
return g.newErrorResponse(msg, a2aErr)
|
|
}
|
|
|
|
switch types.GroupMessageType(msg.Type) {
|
|
// Group management
|
|
case types.GroupCreate:
|
|
return g.handleGroupCreate(ctx, msg)
|
|
case types.GroupDisband:
|
|
return g.handleGroupDisband(ctx, msg)
|
|
case types.GroupJoin:
|
|
return g.handleGroupJoin(ctx, msg)
|
|
case types.GroupLeave:
|
|
return g.handleGroupLeave(ctx, msg)
|
|
case types.GroupInvite:
|
|
return g.handleGroupInvite(ctx, msg)
|
|
case types.GroupAcceptInvite:
|
|
return g.handleGroupAcceptInvite(ctx, msg)
|
|
case types.GroupRejectInvite:
|
|
return g.handleGroupRejectInvite(ctx, msg)
|
|
|
|
// Group communication
|
|
case types.GroupBroadcast:
|
|
return g.handleGroupBroadcast(ctx, msg)
|
|
case types.GroupMulticast:
|
|
return g.handleGroupMulticast(ctx, msg)
|
|
case types.GroupDirect:
|
|
return g.handleGroupDirect(ctx, msg)
|
|
|
|
// Task collaboration
|
|
case types.GroupTaskSplit:
|
|
return g.handleGroupTaskSplit(ctx, msg)
|
|
case types.GroupTaskAssign:
|
|
return g.handleGroupTaskAssign(ctx, msg)
|
|
case types.GroupTaskMerge:
|
|
return g.handleGroupTaskMerge(ctx, msg)
|
|
case types.GroupTaskProgress:
|
|
return g.handleGroupTaskProgress(ctx, msg)
|
|
|
|
// Consensus
|
|
case types.GroupVote:
|
|
return g.handleGroupVote(ctx, msg)
|
|
case types.GroupVoteSubmit:
|
|
return g.handleGroupVoteSubmit(ctx, msg)
|
|
|
|
// Human-in-the-Loop
|
|
case types.GroupConsensusEscalate:
|
|
return g.handleGroupConsensusEscalate(ctx, msg)
|
|
case types.GroupSubtaskEscalate:
|
|
return g.handleGroupSubtaskEscalate(ctx, msg)
|
|
case types.GroupHumanDecision:
|
|
return g.handleGroupHumanDecision(ctx, msg)
|
|
case types.InterGroupEscalate:
|
|
return g.handleInterGroupEscalate(ctx, msg)
|
|
|
|
default:
|
|
return g.newErrorResponse(msg, types.NewSynapseError(
|
|
types.ErrMessageFormatInvalid,
|
|
fmt.Sprintf("Unknown group message type: %s", msg.Type),
|
|
nil,
|
|
))
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Group Management Handlers
|
|
// =============================================================================
|
|
|
|
func (g *GroupProtocol) handleGroupCreate(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
|
var payload types.GroupCreatePayload
|
|
if err := json.Unmarshal(msg.Payload, &payload); err != nil {
|
|
return g.newErrorResponse(msg, types.NewSynapseError(types.ErrMessageFormatInvalid, "Invalid create payload", err.Error()))
|
|
}
|
|
|
|
// Check permissions (only coordinator can create)
|
|
// For now, allow any agent to create
|
|
|
|
group := &types.AgentGroup{
|
|
ID: fmt.Sprintf("group-%d", time.Now().UnixNano()),
|
|
Name: payload.Group.Name,
|
|
Topology: payload.Group.Topology,
|
|
CreatedBy: g.agentEndpoint.AgentID,
|
|
CreatedAt: types.NowTimestamp(),
|
|
Config: payload.Group.Config,
|
|
Members: []types.GroupMember{},
|
|
Roles: payload.Group.Roles,
|
|
Status: types.GroupStatusActive,
|
|
}
|
|
|
|
// Add creator as first member
|
|
creator := types.GroupMember{
|
|
AgentID: g.agentEndpoint.AgentID,
|
|
AgentName: g.agentEndpoint.AgentName,
|
|
Role: "coordinator",
|
|
JoinedAt: types.NowTimestamp(),
|
|
Status: types.GroupMemberActive,
|
|
Capabilities: []string{g.agentEndpoint.Version},
|
|
LoadFactor: 0.0,
|
|
}
|
|
group.Members = append(group.Members, creator)
|
|
|
|
g.groups[group.ID] = group
|
|
g.memberships[group.ID] = "coordinator"
|
|
|
|
log.Printf("[Group] Group created: %s (topology: %s)", group.ID, group.Topology)
|
|
|
|
return g.newSuccessResponse(msg, map[string]interface{}{
|
|
"group_id": group.ID,
|
|
"status": "created",
|
|
})
|
|
}
|
|
|
|
func (g *GroupProtocol) handleGroupDisband(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
|
var payload struct {
|
|
GroupID string `json:"group_id"`
|
|
}
|
|
if err := json.Unmarshal(msg.Payload, &payload); err != nil {
|
|
return g.newErrorResponse(msg, types.NewSynapseError(types.ErrMessageFormatInvalid, "Invalid disband payload", err.Error()))
|
|
}
|
|
|
|
group, ok := g.groups[payload.GroupID]
|
|
if !ok {
|
|
return g.newErrorResponse(msg, types.ErrGroupNotFoundError)
|
|
}
|
|
|
|
// Check if sender is coordinator
|
|
senderRole := g.getMemberRole(payload.GroupID, msg.Sender.AgentID)
|
|
if senderRole != "coordinator" {
|
|
return g.newErrorResponse(msg, types.ErrPermissionDeniedError)
|
|
}
|
|
|
|
group.Status = types.GroupStatusDisbanded
|
|
delete(g.groups, payload.GroupID)
|
|
delete(g.memberships, payload.GroupID)
|
|
|
|
log.Printf("[Group] Group disbanded: %s", payload.GroupID)
|
|
|
|
return g.newSuccessResponse(msg, map[string]interface{}{
|
|
"group_id": payload.GroupID,
|
|
"status": "disbanded",
|
|
})
|
|
}
|
|
|
|
func (g *GroupProtocol) handleGroupJoin(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
|
var payload struct {
|
|
GroupID string `json:"group_id"`
|
|
}
|
|
if err := json.Unmarshal(msg.Payload, &payload); err != nil {
|
|
return g.newErrorResponse(msg, types.NewSynapseError(types.ErrMessageFormatInvalid, "Invalid join payload", err.Error()))
|
|
}
|
|
|
|
group, ok := g.groups[payload.GroupID]
|
|
if !ok {
|
|
return g.newErrorResponse(msg, types.ErrGroupNotFoundError)
|
|
}
|
|
|
|
if group.Status != types.GroupStatusActive {
|
|
return g.newErrorResponse(msg, types.ErrGroupNotActiveError)
|
|
}
|
|
|
|
// Check if already a member
|
|
for _, m := range group.Members {
|
|
if m.AgentID == g.agentEndpoint.AgentID {
|
|
return g.newErrorResponse(msg, types.NewSynapseError(types.ErrAlreadyMember, "Already a member", nil))
|
|
}
|
|
}
|
|
|
|
// Check group size
|
|
if len(group.Members) >= g.config.MaxGroupSize {
|
|
return g.newErrorResponse(msg, types.NewSynapseError(types.ErrGroupFull, "Group is full", nil))
|
|
}
|
|
|
|
// Add as member
|
|
member := types.GroupMember{
|
|
AgentID: g.agentEndpoint.AgentID,
|
|
AgentName: g.agentEndpoint.AgentName,
|
|
Role: "worker",
|
|
JoinedAt: types.NowTimestamp(),
|
|
Status: types.GroupMemberActive,
|
|
LoadFactor: 0.0,
|
|
}
|
|
group.Members = append(group.Members, member)
|
|
g.memberships[payload.GroupID] = "worker"
|
|
|
|
log.Printf("[Group] Agent %s joined group %s", g.agentEndpoint.AgentID, payload.GroupID)
|
|
|
|
return g.newSuccessResponse(msg, map[string]interface{}{
|
|
"group_id": payload.GroupID,
|
|
"role": "worker",
|
|
"member_id": member.AgentID,
|
|
})
|
|
}
|
|
|
|
func (g *GroupProtocol) handleGroupLeave(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
|
var payload struct {
|
|
GroupID string `json:"group_id"`
|
|
}
|
|
if err := json.Unmarshal(msg.Payload, &payload); err != nil {
|
|
return g.newErrorResponse(msg, types.NewSynapseError(types.ErrMessageFormatInvalid, "Invalid leave payload", err.Error()))
|
|
}
|
|
|
|
group, ok := g.groups[payload.GroupID]
|
|
if !ok {
|
|
return g.newErrorResponse(msg, types.ErrGroupNotFoundError)
|
|
}
|
|
|
|
// Check if member
|
|
found := false
|
|
for i, m := range group.Members {
|
|
if m.AgentID == g.agentEndpoint.AgentID {
|
|
group.Members = append(group.Members[:i], group.Members[i+1:]...)
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
return g.newErrorResponse(msg, types.ErrNotGroupMemberError)
|
|
}
|
|
|
|
// Check if can self-leave
|
|
if !group.Config.AllowSelfLeave && g.getMemberRole(payload.GroupID, g.agentEndpoint.AgentID) == "coordinator" {
|
|
return g.newErrorResponse(msg, types.ErrPermissionDeniedError)
|
|
}
|
|
|
|
delete(g.memberships, payload.GroupID)
|
|
|
|
log.Printf("[Group] Agent %s left group %s", g.agentEndpoint.AgentID, payload.GroupID)
|
|
|
|
return g.newSuccessResponse(msg, map[string]interface{}{
|
|
"group_id": payload.GroupID,
|
|
"status": "left",
|
|
})
|
|
}
|
|
|
|
func (g *GroupProtocol) handleGroupInvite(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
|
var payload types.GroupInvitePayload
|
|
if err := json.Unmarshal(msg.Payload, &payload); err != nil {
|
|
return g.newErrorResponse(msg, types.NewSynapseError(types.ErrMessageFormatInvalid, "Invalid invite payload", err.Error()))
|
|
}
|
|
|
|
_, ok := g.groups[payload.GroupID]
|
|
if !ok {
|
|
return g.newErrorResponse(msg, types.ErrGroupNotFoundError)
|
|
}
|
|
|
|
// Check if inviter is coordinator or has invite permission
|
|
inviterRole := g.getMemberRole(payload.GroupID, msg.Sender.AgentID)
|
|
if inviterRole != "coordinator" && inviterRole != "" {
|
|
return g.newErrorResponse(msg, types.ErrPermissionDeniedError)
|
|
}
|
|
|
|
log.Printf("[Group] Invitation sent to %s for group %s", payload.Invitee.AgentID, payload.GroupID)
|
|
|
|
return g.newSuccessResponse(msg, map[string]interface{}{
|
|
"group_id": payload.GroupID,
|
|
"invitee": payload.Invitee.AgentID,
|
|
"status": "invited",
|
|
})
|
|
}
|
|
|
|
func (g *GroupProtocol) handleGroupAcceptInvite(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
|
var payload struct {
|
|
GroupID string `json:"group_id"`
|
|
}
|
|
if err := json.Unmarshal(msg.Payload, &payload); err != nil {
|
|
return g.newErrorResponse(msg, types.NewSynapseError(types.ErrMessageFormatInvalid, "Invalid accept payload", err.Error()))
|
|
}
|
|
|
|
// Join the group
|
|
return g.handleGroupJoin(ctx, msg)
|
|
}
|
|
|
|
func (g *GroupProtocol) handleGroupRejectInvite(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
|
var payload struct {
|
|
GroupID string `json:"group_id"`
|
|
Reason string `json:"reason"`
|
|
}
|
|
if err := json.Unmarshal(msg.Payload, &payload); err != nil {
|
|
return g.newErrorResponse(msg, types.NewSynapseError(types.ErrMessageFormatInvalid, "Invalid reject payload", err.Error()))
|
|
}
|
|
|
|
log.Printf("[Group] Invitation rejected for group %s: %s", payload.GroupID, payload.Reason)
|
|
|
|
return g.newSuccessResponse(msg, map[string]interface{}{
|
|
"group_id": payload.GroupID,
|
|
"status": "rejected",
|
|
"reason": payload.Reason,
|
|
})
|
|
}
|
|
|
|
// =============================================================================
|
|
// Group Communication Handlers
|
|
// =============================================================================
|
|
|
|
func (g *GroupProtocol) handleGroupBroadcast(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
|
// Broadcast is handled by the transport layer
|
|
// This just validates the message
|
|
log.Printf("[Group] Broadcast received in group %s from %s", msg.GroupID, msg.Sender.AgentID)
|
|
return g.newSuccessResponse(msg, map[string]interface{}{
|
|
"status": "broadcast_received",
|
|
})
|
|
}
|
|
|
|
func (g *GroupProtocol) handleGroupMulticast(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
|
_, ok := g.groups[msg.GroupID]
|
|
if !ok {
|
|
return g.newErrorResponse(msg, types.ErrGroupNotFoundError)
|
|
}
|
|
|
|
// Validate targets exist
|
|
targets := msg.TargetIDs
|
|
for _, t := range targets {
|
|
if !g.isMember(msg.GroupID, t) {
|
|
return g.newErrorResponse(msg, types.NewSynapseError(types.ErrMemberNotFound, fmt.Sprintf("Member not found: %s", t), nil))
|
|
}
|
|
}
|
|
|
|
log.Printf("[Group] Multicast in group %s to %d targets", msg.GroupID, len(targets))
|
|
|
|
return g.newSuccessResponse(msg, map[string]interface{}{
|
|
"status": "multicast_scheduled",
|
|
"target_count": len(targets),
|
|
})
|
|
}
|
|
|
|
func (g *GroupProtocol) handleGroupDirect(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
|
if len(msg.TargetIDs) != 1 {
|
|
return g.newErrorResponse(msg, types.NewSynapseError(types.ErrInvalidParams, "Direct message requires exactly one target", nil))
|
|
}
|
|
|
|
target := msg.TargetIDs[0]
|
|
if !g.isMember(msg.GroupID, target) {
|
|
return g.newErrorResponse(msg, types.ErrMemberNotFoundError)
|
|
}
|
|
|
|
log.Printf("[Group] Direct message in group %s to %s", msg.GroupID, target)
|
|
|
|
return g.newSuccessResponse(msg, map[string]interface{}{
|
|
"status": "direct_scheduled",
|
|
"target": target,
|
|
})
|
|
}
|
|
|
|
// =============================================================================
|
|
// Task Collaboration Handlers
|
|
// =============================================================================
|
|
|
|
func (g *GroupProtocol) handleGroupTaskSplit(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
|
var payload types.GroupTaskSplitPayload
|
|
if err := json.Unmarshal(msg.Payload, &payload); err != nil {
|
|
return g.newErrorResponse(msg, types.NewSynapseError(types.ErrMessageFormatInvalid, "Invalid task split payload", err.Error()))
|
|
}
|
|
|
|
_, ok := g.groups[msg.GroupID]
|
|
if !ok {
|
|
return g.newErrorResponse(msg, types.ErrGroupNotFoundError)
|
|
}
|
|
|
|
// Check if sender is coordinator
|
|
if g.getMemberRole(msg.GroupID, msg.Sender.AgentID) != "coordinator" {
|
|
return g.newErrorResponse(msg, types.ErrPermissionDeniedError)
|
|
}
|
|
|
|
// Validate task split
|
|
if g.taskHandler != nil {
|
|
subtasks, valid := g.taskHandler.ValidateTaskSplit(payload.OriginalTask.Type, payload.OriginalTask.Requirements)
|
|
if !valid {
|
|
return g.newErrorResponse(msg, types.NewSynapseError(types.ErrInvalidParams, "Task cannot be split", nil))
|
|
}
|
|
payload.Subtasks = subtasks
|
|
}
|
|
|
|
// Check subtask count
|
|
if len(payload.Subtasks) > g.config.MaxSubtasks {
|
|
return g.newErrorResponse(msg, types.NewSynapseError(types.ErrInvalidParams, fmt.Sprintf("Too many subtasks: %d", len(payload.Subtasks)), nil))
|
|
}
|
|
|
|
// Assign subtasks to members
|
|
for i := range payload.Subtasks {
|
|
assignee := payload.Subtasks[i].AssignedTo
|
|
if !g.isMember(msg.GroupID, assignee) {
|
|
return g.newErrorResponse(msg, types.NewSynapseError(types.ErrMemberNotFound, fmt.Sprintf("Assignee not found: %s", assignee), nil))
|
|
}
|
|
}
|
|
|
|
log.Printf("[Group] Task split in group %s: %d subtasks", msg.GroupID, len(payload.Subtasks))
|
|
|
|
// Execute subtasks asynchronously
|
|
go g.executeSubtasks(context.Background(), msg.GroupID, payload.OriginalTask.ID, payload.Subtasks, payload.MergeStrategy)
|
|
|
|
return g.newSuccessResponse(msg, map[string]interface{}{
|
|
"status": "task_split_initiated",
|
|
"original_task_id": payload.OriginalTask.ID,
|
|
"subtask_count": len(payload.Subtasks),
|
|
})
|
|
}
|
|
|
|
func (g *GroupProtocol) executeSubtasks(ctx context.Context, groupID, originalTaskID string, subtasks []types.Subtask, strategy types.MergeStrategy) {
|
|
if g.taskHandler == nil {
|
|
log.Printf("[Group] No task handler configured, skipping subtask execution")
|
|
return
|
|
}
|
|
|
|
results := make([]types.SubtaskResult, 0, len(subtasks))
|
|
|
|
for _, st := range subtasks {
|
|
result, err := g.taskHandler.ExecuteSubtask(ctx, st.ID, map[string]interface{}{
|
|
"description": st.Description,
|
|
"dependencies": st.Dependencies,
|
|
})
|
|
|
|
subtaskResult := types.SubtaskResult{
|
|
SubtaskID: st.ID,
|
|
AssignedTo: st.AssignedTo,
|
|
}
|
|
|
|
if err != nil {
|
|
subtaskResult.Status = "failed"
|
|
subtaskResult.Error = err.Error()
|
|
} else {
|
|
subtaskResult.Status = "success"
|
|
subtaskResult.Result = result
|
|
now := types.NowTimestamp()
|
|
subtaskResult.CompletedAt = &now
|
|
}
|
|
|
|
results = append(results, subtaskResult)
|
|
}
|
|
|
|
// Merge results
|
|
_, err := g.taskHandler.MergeResults(results, strategy)
|
|
if err != nil {
|
|
log.Printf("[Group] Failed to merge results: %v", err)
|
|
}
|
|
|
|
log.Printf("[Group] All subtasks completed, merged result ready")
|
|
// In a real implementation, this would send a GROUP_TASK_MERGE message
|
|
}
|
|
|
|
func (g *GroupProtocol) handleGroupTaskAssign(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
|
var payload struct {
|
|
SubtaskID string `json:"subtask_id"`
|
|
AssignTo string `json:"assign_to"`
|
|
}
|
|
if err := json.Unmarshal(msg.Payload, &payload); err != nil {
|
|
return g.newErrorResponse(msg, types.NewSynapseError(types.ErrMessageFormatInvalid, "Invalid task assign payload", err.Error()))
|
|
}
|
|
|
|
if !g.isMember(msg.GroupID, payload.AssignTo) {
|
|
return g.newErrorResponse(msg, types.ErrMemberNotFoundError)
|
|
}
|
|
|
|
log.Printf("[Group] Subtask %s assigned to %s in group %s", payload.SubtaskID, payload.AssignTo, msg.GroupID)
|
|
|
|
return g.newSuccessResponse(msg, map[string]interface{}{
|
|
"status": "assigned",
|
|
"subtask_id": payload.SubtaskID,
|
|
"assignee": payload.AssignTo,
|
|
})
|
|
}
|
|
|
|
func (g *GroupProtocol) handleGroupTaskMerge(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
|
var payload types.GroupTaskMergePayload
|
|
if err := json.Unmarshal(msg.Payload, &payload); err != nil {
|
|
return g.newErrorResponse(msg, types.NewSynapseError(types.ErrMessageFormatInvalid, "Invalid task merge payload", err.Error()))
|
|
}
|
|
|
|
log.Printf("[Group] Task merge for %s: %d results, consensus: %v",
|
|
payload.OriginalTaskID, len(payload.SubtaskResults), payload.ConsensusAchieved)
|
|
|
|
return g.newSuccessResponse(msg, map[string]interface{}{
|
|
"status": "merged",
|
|
"original_task_id": payload.OriginalTaskID,
|
|
"result_ready": true,
|
|
})
|
|
}
|
|
|
|
func (g *GroupProtocol) handleGroupTaskProgress(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
|
var payload struct {
|
|
SubtaskID string `json:"subtask_id"`
|
|
Progress int `json:"progress"`
|
|
Message string `json:"message"`
|
|
}
|
|
if err := json.Unmarshal(msg.Payload, &payload); err != nil {
|
|
return g.newErrorResponse(msg, types.NewSynapseError(types.ErrMessageFormatInvalid, "Invalid progress payload", err.Error()))
|
|
}
|
|
|
|
log.Printf("[Group] Progress update: subtask %s - %d%% (%s)", payload.SubtaskID, payload.Progress, payload.Message)
|
|
|
|
return g.newSuccessResponse(msg, map[string]interface{}{
|
|
"status": "progress_recorded",
|
|
"subtask_id": payload.SubtaskID,
|
|
})
|
|
}
|
|
|
|
// =============================================================================
|
|
// Consensus Handlers
|
|
// =============================================================================
|
|
|
|
func (g *GroupProtocol) handleGroupVote(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
|
var payload types.GroupVotePayload
|
|
if err := json.Unmarshal(msg.Payload, &payload); err != nil {
|
|
return g.newErrorResponse(msg, types.NewSynapseError(types.ErrMessageFormatInvalid, "Invalid vote payload", err.Error()))
|
|
}
|
|
|
|
// Initialize vote
|
|
payload.Status = "open"
|
|
payload.Votes = []types.GroupVoteOption{}
|
|
|
|
_, ok := g.groups[msg.GroupID]
|
|
if !ok {
|
|
return g.newErrorResponse(msg, types.ErrGroupNotFoundError)
|
|
}
|
|
|
|
// Check if sender is allowed to initiate vote
|
|
senderRole := g.getMemberRole(msg.GroupID, msg.Sender.AgentID)
|
|
if senderRole != "coordinator" && senderRole != "reviewer" {
|
|
return g.newErrorResponse(msg, types.ErrPermissionDeniedError)
|
|
}
|
|
|
|
log.Printf("[Group] Vote initiated in group %s: %s (threshold: %.0f%%)",
|
|
msg.GroupID, payload.Question, payload.Threshold*100)
|
|
|
|
// In a real implementation, this would set up a vote session and notify voters
|
|
return g.newSuccessResponse(msg, map[string]interface{}{
|
|
"status": "vote_initiated",
|
|
"vote_id": payload.VoteID,
|
|
"voters": len(payload.Voters),
|
|
})
|
|
}
|
|
|
|
func (g *GroupProtocol) handleGroupVoteSubmit(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
|
var payload struct {
|
|
VoteID string `json:"vote_id"`
|
|
Option string `json:"option"`
|
|
Reason string `json:"reason,omitempty"`
|
|
}
|
|
if err := json.Unmarshal(msg.Payload, &payload); err != nil {
|
|
return g.newErrorResponse(msg, types.NewSynapseError(types.ErrMessageFormatInvalid, "Invalid vote submit payload", err.Error()))
|
|
}
|
|
|
|
log.Printf("[Group] Vote submitted: %s by %s (option: %s)", payload.VoteID, msg.Sender.AgentID, payload.Option)
|
|
|
|
return g.newSuccessResponse(msg, map[string]interface{}{
|
|
"status": "vote_recorded",
|
|
"vote_id": payload.VoteID,
|
|
})
|
|
}
|
|
|
|
// =============================================================================
|
|
// Human-in-the-Loop Handlers
|
|
// =============================================================================
|
|
|
|
func (g *GroupProtocol) handleGroupConsensusEscalate(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
|
var payload types.GroupConsensusEscalatePayload
|
|
if err := json.Unmarshal(msg.Payload, &payload); err != nil {
|
|
return g.newErrorResponse(msg, types.NewSynapseError(types.ErrMessageFormatInvalid, "Invalid consensus escalate payload", err.Error()))
|
|
}
|
|
|
|
log.Printf("[Group] Consensus escalation in group %s (level: %d): %s",
|
|
msg.GroupID, payload.EscalationLevel, payload.EscalateReason.Message)
|
|
|
|
// Store escalation and notify human decision makers
|
|
return g.newSuccessResponse(msg, map[string]interface{}{
|
|
"status": "escalation_recorded",
|
|
"escalation_id": payload.EscalationID,
|
|
"human_notified": true,
|
|
})
|
|
}
|
|
|
|
func (g *GroupProtocol) handleGroupSubtaskEscalate(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
|
var payload types.GroupSubtaskEscalatePayload
|
|
if err := json.Unmarshal(msg.Payload, &payload); err != nil {
|
|
return g.newErrorResponse(msg, types.NewSynapseError(types.ErrMessageFormatInvalid, "Invalid subtask escalate payload", err.Error()))
|
|
}
|
|
|
|
log.Printf("[Group] Subtask escalation: %s in task %s (level: %d)",
|
|
payload.Subtask.ID, payload.OriginalTask.ID, payload.EscalationLevel)
|
|
|
|
return g.newSuccessResponse(msg, map[string]interface{}{
|
|
"status": "escalation_recorded",
|
|
"subtask_id": payload.Subtask.ID,
|
|
"human_notified": true,
|
|
})
|
|
}
|
|
|
|
func (g *GroupProtocol) handleGroupHumanDecision(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
|
var payload types.HumanDecisionPayload
|
|
if err := json.Unmarshal(msg.Payload, &payload); err != nil {
|
|
return g.newErrorResponse(msg, types.NewSynapseError(types.ErrMessageFormatInvalid, "Invalid human decision payload", err.Error()))
|
|
}
|
|
|
|
log.Printf("[Group] Human decision for task %s: %s (by %s)",
|
|
payload.TaskID, payload.Decision.Action, payload.DecidedBy.UserName)
|
|
|
|
// Process the decision and resume task execution
|
|
return g.newSuccessResponse(msg, map[string]interface{}{
|
|
"status": "decision_recorded",
|
|
"task_id": payload.TaskID,
|
|
"action": payload.Decision.Action,
|
|
})
|
|
}
|
|
|
|
func (g *GroupProtocol) handleInterGroupEscalate(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) {
|
|
var payload struct {
|
|
EscalationID string `json:"escalation_id"`
|
|
EscalationType string `json:"escalation_type"`
|
|
// ... other fields
|
|
}
|
|
if err := json.Unmarshal(msg.Payload, &payload); err != nil {
|
|
return g.newErrorResponse(msg, types.NewSynapseError(types.ErrMessageFormatInvalid, "Invalid inter-group escalate payload", err.Error()))
|
|
}
|
|
|
|
log.Printf("[Group] Inter-group escalation: %s (type: %s)", payload.EscalationID, payload.EscalationType)
|
|
|
|
return g.newSuccessResponse(msg, map[string]interface{}{
|
|
"status": "escalation_recorded",
|
|
"escalation_id": payload.EscalationID,
|
|
})
|
|
}
|
|
|
|
// =============================================================================
|
|
// Helper Methods
|
|
// =============================================================================
|
|
|
|
func (g *GroupProtocol) validateMessage(msg *types.UnifiedSynapseMessage) error {
|
|
if msg.GroupID == "" {
|
|
return types.NewSynapseError(types.ErrMessageFormatInvalid, "Missing group_id", nil)
|
|
}
|
|
if msg.Sender.AgentID == "" {
|
|
return types.NewSynapseError(types.ErrMessageFormatInvalid, "Missing sender agent_id", nil)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (g *GroupProtocol) getMemberRole(groupID, agentID string) string {
|
|
group, ok := g.groups[groupID]
|
|
if !ok {
|
|
return ""
|
|
}
|
|
for _, m := range group.Members {
|
|
if m.AgentID == agentID {
|
|
return m.Role
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (g *GroupProtocol) isMember(groupID, agentID string) bool {
|
|
return g.getMemberRole(groupID, agentID) != ""
|
|
}
|
|
|
|
func (g *GroupProtocol) newSuccessResponse(msg *types.UnifiedSynapseMessage, data map[string]interface{}) (*types.UnifiedSynapseMessage, error) {
|
|
resp := &types.UnifiedSynapseMessage{
|
|
ID: types.GenerateMessageID(),
|
|
Version: "1.0.0",
|
|
Type: string(types.P2PAck),
|
|
Sender: g.agentEndpoint,
|
|
CorrelationID: msg.CorrelationID,
|
|
ReplyTo: msg.ID,
|
|
Timestamp: types.NowTimestamp(),
|
|
}
|
|
return resp, nil
|
|
}
|
|
|
|
func (g *GroupProtocol) newErrorResponse(msg *types.UnifiedSynapseMessage, err *types.SynapseError) (*types.UnifiedSynapseMessage, error) {
|
|
return nil, err
|
|
}
|
|
|
|
// GetGroups returns all groups the agent is a member of
|
|
func (g *GroupProtocol) GetGroups() []*types.AgentGroup {
|
|
g.mu.RLock()
|
|
defer g.mu.RUnlock()
|
|
|
|
result := make([]*types.AgentGroup, 0, len(g.groups))
|
|
for _, group := range g.groups {
|
|
if g.isMember(group.ID, g.agentEndpoint.AgentID) {
|
|
result = append(result, group)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// GetGroup returns a specific group
|
|
func (g *GroupProtocol) GetGroup(groupID string) (*types.AgentGroup, bool) {
|
|
g.mu.RLock()
|
|
defer g.mu.RUnlock()
|
|
group, ok := g.groups[groupID]
|
|
return group, ok
|
|
}
|