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:
viettranx committed 2026-03-17 12:43:09 +07:00
1 parent 9a4a5ef2c1
commit 97cacfe68b
6 files changed
+92 -13

No files matched your search

+5 -1
View File
@@ -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
View File
@@ -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,
+7 -2
View File
@@ -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()
+16 -7
View File
@@ -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))
+2 -2
View File
@@ -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>