diff --git a/cmd/gateway.go b/cmd/gateway.go index b6a3ef8d..4278288c 100644 --- a/cmd/gateway.go +++ b/cmd/gateway.go @@ -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 { diff --git a/internal/agent/loop.go b/internal/agent/loop.go index 58e3797f..f51a22d2 100644 --- a/internal/agent/loop.go +++ b/internal/agent/loop.go @@ -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, diff --git a/internal/agent/resolver_helpers.go b/internal/agent/resolver_helpers.go index b8199b01..1bb1f983 100644 --- a/internal/agent/resolver_helpers.go +++ b/internal/agent/resolver_helpers.go @@ -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: **.\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() diff --git a/internal/tools/team_tasks_mutations.go b/internal/tools/team_tasks_mutations.go index 8909f0e6..862cd478 100644 --- a/internal/tools/team_tasks_mutations.go +++ b/internal/tools/team_tasks_mutations.go @@ -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)) diff --git a/internal/tools/team_tasks_tool.go b/internal/tools/team_tasks_tool.go index 4abacc1d..f6464186 100644 --- a/internal/tools/team_tasks_tool.go +++ b/internal/tools/team_tasks_tool.go @@ -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", diff --git a/ui/web/src/pages/teams/task-sections/task-list.tsx b/ui/web/src/pages/teams/task-sections/task-list.tsx index 32c065c9..47c38796 100644 --- a/ui/web/src/pages/teams/task-sections/task-list.tsx +++ b/ui/web/src/pages/teams/task-sections/task-list.tsx @@ -80,6 +80,14 @@ export function TaskList({ {task.task_type && task.task_type !== "general" && ( {task.task_type} )} + {isTeamV2 && task.progress_percent != null && task.progress_percent > 0 && ( +
+
+
+
+ {task.progress_percent}% +
+ )}
{task.status.replace(/_/g, " ")}