mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-11 03:13:24 +00:00
feat(teams): member task progress reminder + fix broken progress notifications
- Fix progress event payload missing TaskNumber, Subject, OwnerAgentKey, ProgressPercent, ProgressStep — notifications were rendering empty - Fix progress notification format to include task name (consistent with dispatched/failed) and guard empty ProgressStep - Change percent tool schema from number to integer for clarity - Add pre-run member task reminder injecting task context before LLM loop - Add mid-loop progress nudge every 10 iterations with suggested percent based on iteration ratio (handles maxIter=0 unlimited case) - Enhance leader cross-session reminder to show progress % when available - Strengthen TEAM.md member guidance: focus, result quality, progress rules - Add progress bar to task list table view (matches kanban card pattern)
This commit is contained in:
1 parent
9a4a5ef2c1
commit
97cacfe68b
6 files changed
+92
-13
No files matched your search
+5
-1
@@ -575,7 +575,11 @@ func runGateway() {
|
||||
case "dispatched":
|
||||
content = fmt.Sprintf("📋 Task #%d \"%s\" → assigned to %s", payload.TaskNumber, payload.Subject, agentName)
|
||||
case "progress":
|
||||
content = fmt.Sprintf("⏳ Task #%d: %d%% — %s", payload.TaskNumber, payload.ProgressPercent, payload.ProgressStep)
|
||||
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":
|
||||
reason := payload.Reason
|
||||
if len(reason) > 200 {
|
||||
|
||||
+54
-1
@@ -369,7 +369,15 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error)
|
||||
}
|
||||
if t.Status == store.TeamTaskStatusInProgress {
|
||||
age := time.Since(t.UpdatedAt).Truncate(time.Minute)
|
||||
inProgress = append(inProgress, fmt.Sprintf("- %s: \"%s\" (in progress %s)", t.ID, t.Subject, age))
|
||||
progressInfo := fmt.Sprintf("in progress %s", age)
|
||||
if t.ProgressPercent > 0 {
|
||||
if t.ProgressStep != "" {
|
||||
progressInfo = fmt.Sprintf("%d%% — %s, %s", t.ProgressPercent, t.ProgressStep, age)
|
||||
} else {
|
||||
progressInfo = fmt.Sprintf("%d%%, %s", t.ProgressPercent, age)
|
||||
}
|
||||
}
|
||||
inProgress = append(inProgress, fmt.Sprintf("- %s: \"%s\" (%s)", t.ID, t.Subject, progressInfo))
|
||||
}
|
||||
}
|
||||
var parts []string
|
||||
@@ -396,6 +404,28 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error)
|
||||
}
|
||||
}
|
||||
|
||||
// 2g. Member task reminder: inject task context for members working on dispatched tasks.
|
||||
// Caches task subject/number for mid-loop progress nudge (avoids extra DB query).
|
||||
var memberTaskSubject string
|
||||
var memberTaskNumber int
|
||||
if req.TeamTaskID != "" && l.teamStore != nil {
|
||||
if taskUUID, err := uuid.Parse(req.TeamTaskID); err == nil {
|
||||
if task, err := l.teamStore.GetTask(ctx, taskUUID); err == nil && task != nil {
|
||||
memberTaskSubject = task.Subject
|
||||
memberTaskNumber = task.TaskNumber
|
||||
reminder := fmt.Sprintf(
|
||||
"[System] You are working on team task #%d: %q. "+
|
||||
"Stay focused on this task. Your final response becomes the task result — make it clear and complete. "+
|
||||
"For long tasks, report progress: team_tasks(action=\"progress\", percent=50, text=\"status\").",
|
||||
task.TaskNumber, task.Subject)
|
||||
messages = append(messages,
|
||||
providers.Message{Role: "user", Content: reminder},
|
||||
providers.Message{Role: "assistant", Content: "Understood. I'll focus on this task and report progress."},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Buffer new messages — write to session only AFTER the run completes.
|
||||
// This prevents concurrent runs from seeing each other's in-progress messages.
|
||||
// NOTE: pendingMsgs stores text + lightweight MediaRefs (not base64 images).
|
||||
@@ -435,6 +465,8 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error)
|
||||
var skillNudge70Sent, skillNudge90Sent bool
|
||||
var skillPostscriptSent bool
|
||||
|
||||
// Member progress nudge: remind dispatched members to report progress (every 10 iterations).
|
||||
|
||||
// Inject retry hook so channels can update placeholder on LLM retries.
|
||||
ctx = providers.WithRetryHook(ctx, func(attempt, maxAttempts int, err error) {
|
||||
emitRun(AgentEvent{
|
||||
@@ -492,6 +524,27 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error)
|
||||
}
|
||||
}
|
||||
|
||||
// Member progress nudge: remind to report progress every 10 iterations.
|
||||
// Suggests percent based on iteration ratio — model can adjust but has a baseline.
|
||||
if req.TeamTaskID != "" && memberTaskSubject != "" && iteration > 0 && iteration%10 == 0 {
|
||||
var nudge string
|
||||
if maxIter > 0 {
|
||||
suggestedPct := iteration * 100 / maxIter
|
||||
nudge = fmt.Sprintf(
|
||||
"[System] You are at iteration %d/%d (~%d%% of budget) working on task #%d: %q. "+
|
||||
"Report your progress now: team_tasks(action=\"progress\", percent=%d, text=\"what you've accomplished so far\"). "+
|
||||
"Adjust percent based on actual work completed.",
|
||||
iteration, maxIter, suggestedPct, memberTaskNumber, memberTaskSubject, suggestedPct)
|
||||
} else {
|
||||
nudge = fmt.Sprintf(
|
||||
"[System] You are at iteration %d working on task #%d: %q. "+
|
||||
"Report your progress now: team_tasks(action=\"progress\", percent=50, text=\"what you've accomplished so far\"). "+
|
||||
"Adjust percent based on actual work completed.",
|
||||
iteration, memberTaskNumber, memberTaskSubject)
|
||||
}
|
||||
messages = append(messages, providers.Message{Role: "user", Content: nudge})
|
||||
}
|
||||
|
||||
// Emit activity event: thinking phase
|
||||
emitRun(AgentEvent{
|
||||
Type: protocol.AgentEventActivity,
|
||||
|
||||
@@ -134,8 +134,13 @@ func buildTeamMD(team *store.TeamData, members []store.TeamMemberData, selfID uu
|
||||
if selfRole == store.TeamRoleReviewer {
|
||||
sb.WriteString("You are a **reviewer**. When evaluating, respond with **APPROVED** or **REJECTED: <feedback>**.\n\n")
|
||||
}
|
||||
sb.WriteString("As a member, just do the assigned work. Task completion is automatic.\n")
|
||||
sb.WriteString("For long-running tasks, use `team_tasks(action=\"progress\", percent=50, text=\"status update\")` to report progress. The task_id is auto-resolved from your assigned task — you don't need to specify it.\n")
|
||||
sb.WriteString("As a member, focus entirely on your assigned task.\n\n")
|
||||
sb.WriteString("Rules:\n")
|
||||
sb.WriteString("- Stay on task — do not deviate from the assignment\n")
|
||||
sb.WriteString("- Your final response becomes the task result — make it clear, complete, and actionable\n")
|
||||
sb.WriteString("- For long tasks, report progress: `team_tasks(action=\"progress\", percent=50, text=\"status\")`\n")
|
||||
sb.WriteString("- The task_id is auto-resolved — you don't need to specify it\n")
|
||||
sb.WriteString("- Task completion is automatic when your run finishes\n")
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
|
||||
@@ -305,14 +305,23 @@ func (t *TeamTasksTool) executeProgress(ctx context.Context, args map[string]any
|
||||
return ErrorResult("failed to update progress: " + err.Error())
|
||||
}
|
||||
|
||||
ownerKey := ""
|
||||
if task.OwnerAgentID != nil {
|
||||
ownerKey = t.manager.agentKeyFromID(ctx, *task.OwnerAgentID)
|
||||
}
|
||||
t.manager.broadcastTeamEvent(protocol.EventTeamTaskProgress, protocol.TeamTaskEventPayload{
|
||||
TeamID: team.ID.String(),
|
||||
TaskID: taskID.String(),
|
||||
Status: store.TeamTaskStatusInProgress,
|
||||
UserID: store.UserIDFromContext(ctx),
|
||||
Channel: ToolChannelFromCtx(ctx),
|
||||
ChatID: ToolChatIDFromCtx(ctx),
|
||||
Timestamp: time.Now().UTC().Format("2006-01-02T15:04:05Z"),
|
||||
TeamID: team.ID.String(),
|
||||
TaskID: taskID.String(),
|
||||
TaskNumber: task.TaskNumber,
|
||||
Subject: task.Subject,
|
||||
Status: store.TeamTaskStatusInProgress,
|
||||
OwnerAgentKey: ownerKey,
|
||||
ProgressPercent: percent,
|
||||
ProgressStep: step,
|
||||
UserID: store.UserIDFromContext(ctx),
|
||||
Channel: ToolChannelFromCtx(ctx),
|
||||
ChatID: ToolChatIDFromCtx(ctx),
|
||||
Timestamp: time.Now().UTC().Format("2006-01-02T15:04:05Z"),
|
||||
})
|
||||
|
||||
return SilentResult(fmt.Sprintf("Progress updated: %d%% %s", percent, step))
|
||||
|
||||
@@ -74,8 +74,8 @@ func (t *TeamTasksTool) Parameters() map[string]any {
|
||||
"description": "Require user approval before claim (for create, default false)",
|
||||
},
|
||||
"percent": map[string]any{
|
||||
"type": "number",
|
||||
"description": "Progress 0-100 (for progress)",
|
||||
"type": "integer",
|
||||
"description": "Progress percentage 0-100 (for progress action)",
|
||||
},
|
||||
"file_id": map[string]any{
|
||||
"type": "string",
|
||||
|
||||
@@ -80,6 +80,14 @@ export function TaskList({
|
||||
{task.task_type && task.task_type !== "general" && (
|
||||
<Badge variant="outline" className="mt-0.5 text-[10px]">{task.task_type}</Badge>
|
||||
)}
|
||||
{isTeamV2 && task.progress_percent != null && task.progress_percent > 0 && (
|
||||
<div className="mt-1 flex items-center gap-1.5">
|
||||
<div className="h-1.5 flex-1 rounded-full bg-muted">
|
||||
<div className="h-full rounded-full bg-primary transition-all" style={{ width: `${task.progress_percent}%` }} />
|
||||
</div>
|
||||
<span className="text-[10px] text-muted-foreground">{task.progress_percent}%</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-1">
|
||||
<Badge variant={taskStatusBadgeVariant(task.status)}>{task.status.replace(/_/g, " ")}</Badge>
|
||||
|
||||
Reference in new issue
Block a user