mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-15 18:15:47 +00:00
feat(teams): split dispatched/assigned events + add completed notifications
- Change agent-side broadcasts from EventTeamTaskAssigned to EventTeamTaskDispatched (post-turn, fallback, unblock, retry) - Add completed notification with leader-completion skip logic - Add Completed field to TeamNotifyConfig with *bool backwards compat - Differentiate dispatched messages: unblocked vs regular dispatch - Add EventTeamTaskDispatched to audit event mapper
This commit is contained in:
1 parent
4678065887
commit
b735c16d93
6 files changed
+76
-26
No files matched your search
+41
-18
@@ -542,15 +542,18 @@ func runGateway() {
|
||||
if !ok || payload.TeamID == "" || payload.Channel == "" {
|
||||
return
|
||||
}
|
||||
// Only forward assigned/failed/progress events (completed handled by announce-back).
|
||||
var notifyType string
|
||||
switch evt.Name {
|
||||
case protocol.EventTeamTaskAssigned:
|
||||
case protocol.EventTeamTaskDispatched:
|
||||
notifyType = "dispatched"
|
||||
case protocol.EventTeamTaskAssigned:
|
||||
notifyType = "dispatched" // same config flag — human assign also notifies
|
||||
case protocol.EventTeamTaskFailed:
|
||||
notifyType = "failed"
|
||||
case protocol.EventTeamTaskProgress:
|
||||
notifyType = "progress"
|
||||
case protocol.EventTeamTaskCompleted:
|
||||
notifyType = "completed"
|
||||
default:
|
||||
return
|
||||
}
|
||||
@@ -579,6 +582,10 @@ func runGateway() {
|
||||
if !cfg.Progress {
|
||||
return
|
||||
}
|
||||
case "completed":
|
||||
if !cfg.Completed {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Skip internal channels.
|
||||
@@ -586,22 +593,44 @@ func runGateway() {
|
||||
return
|
||||
}
|
||||
|
||||
// Resolve lead agent key (needed for leader mode routing + completed-by-leader skip).
|
||||
var leadAgentKey string
|
||||
if notifyAgentStore != nil {
|
||||
if la, err := notifyAgentStore.GetByID(context.Background(), team.LeadAgentID); err == nil {
|
||||
leadAgentKey = la.AgentKey
|
||||
}
|
||||
}
|
||||
|
||||
// Skip completed notification if task was completed by the leader
|
||||
// (leader is already talking to the user, notification would be redundant).
|
||||
if notifyType == "completed" && payload.OwnerAgentKey == leadAgentKey {
|
||||
return
|
||||
}
|
||||
|
||||
// Build notification message.
|
||||
var content string
|
||||
agentName := payload.OwnerAgentKey
|
||||
if payload.OwnerDisplayName != "" {
|
||||
agentName = payload.OwnerDisplayName
|
||||
}
|
||||
switch notifyType {
|
||||
case "dispatched":
|
||||
switch evt.Name {
|
||||
case protocol.EventTeamTaskDispatched:
|
||||
if payload.ActorID == "dispatch_unblocked" {
|
||||
content = fmt.Sprintf("▶️ Task #%d \"%s\" → unblocked, dispatched to %s", payload.TaskNumber, payload.Subject, agentName)
|
||||
} else {
|
||||
content = fmt.Sprintf("📋 Task #%d \"%s\" → dispatched to %s", payload.TaskNumber, payload.Subject, agentName)
|
||||
}
|
||||
case protocol.EventTeamTaskAssigned:
|
||||
content = fmt.Sprintf("📋 Task #%d \"%s\" → assigned to %s", payload.TaskNumber, payload.Subject, agentName)
|
||||
case "progress":
|
||||
case protocol.EventTeamTaskCompleted:
|
||||
content = fmt.Sprintf("✅ Task #%d \"%s\" completed", payload.TaskNumber, payload.Subject)
|
||||
case protocol.EventTeamTaskProgress:
|
||||
if payload.ProgressStep != "" {
|
||||
content = fmt.Sprintf("⏳ Task #%d \"%s\": %d%% — %s", payload.TaskNumber, payload.Subject, payload.ProgressPercent, payload.ProgressStep)
|
||||
} else {
|
||||
content = fmt.Sprintf("⏳ Task #%d \"%s\": %d%%", payload.TaskNumber, payload.Subject, payload.ProgressPercent)
|
||||
}
|
||||
case "failed":
|
||||
case protocol.EventTeamTaskFailed:
|
||||
reason := payload.Reason
|
||||
if len(reason) > 200 {
|
||||
reason = reason[:200] + "..."
|
||||
@@ -609,17 +638,9 @@ func runGateway() {
|
||||
content = fmt.Sprintf("❌ Task #%d \"%s\" failed: %s", payload.TaskNumber, payload.Subject, reason)
|
||||
}
|
||||
|
||||
// Resolve leader agent for leader mode routing.
|
||||
var leadAgent string
|
||||
if cfg.Mode == "leader" {
|
||||
if notifyAgentStore != nil {
|
||||
if la, err := notifyAgentStore.GetByID(context.Background(), team.LeadAgentID); err == nil {
|
||||
leadAgent = la.AgentKey
|
||||
}
|
||||
}
|
||||
if leadAgent == "" {
|
||||
return
|
||||
}
|
||||
// In leader mode, require resolved agent key for routing.
|
||||
if cfg.Mode == "leader" && leadAgentKey == "" {
|
||||
return
|
||||
}
|
||||
|
||||
batchKey := payload.TeamID + ":" + payload.ChatID
|
||||
@@ -628,7 +649,7 @@ func runGateway() {
|
||||
Channel: payload.Channel,
|
||||
ChatID: payload.ChatID,
|
||||
UserID: payload.UserID,
|
||||
LeadAgent: leadAgent,
|
||||
LeadAgent: leadAgentKey,
|
||||
})
|
||||
})
|
||||
slog.Info("team progress notification subscriber registered")
|
||||
@@ -889,6 +910,8 @@ func teamTaskEventType(eventName string) string {
|
||||
return "claimed"
|
||||
case protocol.EventTeamTaskAssigned:
|
||||
return "assigned"
|
||||
case protocol.EventTeamTaskDispatched:
|
||||
return "dispatched"
|
||||
case protocol.EventTeamTaskCompleted:
|
||||
return "completed"
|
||||
case protocol.EventTeamTaskFailed:
|
||||
|
||||
@@ -4,9 +4,10 @@ import "encoding/json"
|
||||
|
||||
// TeamNotifyConfig controls which team task events are forwarded to chat channels.
|
||||
type TeamNotifyConfig struct {
|
||||
Dispatched bool `json:"dispatched"` // task assigned to member
|
||||
Dispatched bool `json:"dispatched"` // task dispatched to member
|
||||
Progress bool `json:"progress"` // member updates progress
|
||||
Failed bool `json:"failed"` // task failed
|
||||
Completed bool `json:"completed"` // task completed
|
||||
Mode string `json:"mode"` // "direct" (outbound) or "leader" (through leader agent)
|
||||
}
|
||||
|
||||
@@ -16,6 +17,7 @@ func DefaultTeamNotifyConfig() TeamNotifyConfig {
|
||||
Dispatched: true,
|
||||
Progress: true,
|
||||
Failed: true,
|
||||
Completed: true,
|
||||
Mode: "direct",
|
||||
}
|
||||
}
|
||||
@@ -28,15 +30,30 @@ func ParseTeamNotifyConfig(settings json.RawMessage) TeamNotifyConfig {
|
||||
return cfg
|
||||
}
|
||||
var s struct {
|
||||
Notifications *TeamNotifyConfig `json:"notifications"`
|
||||
Notifications *struct {
|
||||
Dispatched *bool `json:"dispatched"`
|
||||
Progress *bool `json:"progress"`
|
||||
Failed *bool `json:"failed"`
|
||||
Completed *bool `json:"completed"`
|
||||
Mode string `json:"mode"`
|
||||
} `json:"notifications"`
|
||||
}
|
||||
if json.Unmarshal(settings, &s) != nil || s.Notifications == nil {
|
||||
return cfg
|
||||
}
|
||||
n := s.Notifications
|
||||
cfg.Dispatched = n.Dispatched
|
||||
cfg.Progress = n.Progress
|
||||
cfg.Failed = n.Failed
|
||||
if n.Dispatched != nil {
|
||||
cfg.Dispatched = *n.Dispatched
|
||||
}
|
||||
if n.Progress != nil {
|
||||
cfg.Progress = *n.Progress
|
||||
}
|
||||
if n.Failed != nil {
|
||||
cfg.Failed = *n.Failed
|
||||
}
|
||||
if n.Completed != nil {
|
||||
cfg.Completed = *n.Completed
|
||||
}
|
||||
if n.Mode == "leader" {
|
||||
cfg.Mode = "leader"
|
||||
}
|
||||
|
||||
@@ -133,7 +133,7 @@ func (t *TeamTasksTool) executeRetry(ctx context.Context, args map[string]any) *
|
||||
return ErrorResult("failed to retry task: " + err.Error())
|
||||
}
|
||||
|
||||
t.manager.broadcastTeamEvent(protocol.EventTeamTaskAssigned, protocol.TeamTaskEventPayload{
|
||||
t.manager.broadcastTeamEvent(protocol.EventTeamTaskDispatched, protocol.TeamTaskEventPayload{
|
||||
TeamID: team.ID.String(),
|
||||
TaskID: taskID.String(),
|
||||
TaskNumber: task.TaskNumber,
|
||||
|
||||
@@ -73,9 +73,19 @@ func (t *TeamTasksTool) executeComplete(ctx context.Context, args map[string]any
|
||||
}
|
||||
|
||||
ownerKey := t.manager.agentKeyFromID(ctx, agentID)
|
||||
// Fetch task for TaskNumber/Subject needed by notification subscriber.
|
||||
completedTask, _ := t.manager.teamStore.GetTask(ctx, taskID)
|
||||
var taskNumber int
|
||||
var taskSubject string
|
||||
if completedTask != nil {
|
||||
taskNumber = completedTask.TaskNumber
|
||||
taskSubject = completedTask.Subject
|
||||
}
|
||||
t.manager.broadcastTeamEvent(protocol.EventTeamTaskCompleted, protocol.TeamTaskEventPayload{
|
||||
TeamID: team.ID.String(),
|
||||
TaskID: taskID.String(),
|
||||
TaskNumber: taskNumber,
|
||||
Subject: taskSubject,
|
||||
Status: store.TeamTaskStatusCompleted,
|
||||
OwnerAgentKey: ownerKey,
|
||||
OwnerDisplayName: t.manager.agentDisplayName(ctx, ownerKey),
|
||||
|
||||
@@ -234,7 +234,7 @@ func (m *TeamToolManager) DispatchUnblockedTasks(ctx context.Context, teamID uui
|
||||
slog.Warn("DispatchUnblockedTasks: assign failed", "task_id", task.ID, "error", err)
|
||||
continue
|
||||
}
|
||||
m.broadcastTeamEvent(protocol.EventTeamTaskAssigned, protocol.TeamTaskEventPayload{
|
||||
m.broadcastTeamEvent(protocol.EventTeamTaskDispatched, protocol.TeamTaskEventPayload{
|
||||
TeamID: teamID.String(),
|
||||
TaskID: task.ID.String(),
|
||||
TaskNumber: task.TaskNumber,
|
||||
|
||||
@@ -91,7 +91,7 @@ func (m *TeamToolManager) ProcessPendingTasks(ctx context.Context, teamID uuid.U
|
||||
slog.Warn("post_turn: assign failed", "task_id", task.ID, "error", err)
|
||||
continue
|
||||
}
|
||||
m.broadcastTeamEvent(protocol.EventTeamTaskAssigned, protocol.TeamTaskEventPayload{
|
||||
m.broadcastTeamEvent(protocol.EventTeamTaskDispatched, protocol.TeamTaskEventPayload{
|
||||
TeamID: teamID.String(),
|
||||
TaskID: task.ID.String(),
|
||||
TaskNumber: task.TaskNumber,
|
||||
|
||||
Reference in new issue
Block a user