fix(agent): prevent team V2 leads from manually creating tasks before spawn

Strengthen TEAM.md prompt with WRONG/CORRECT examples and NEVER wording
to stop models from calling `team_tasks create` before `spawn` in V2.

Prompt changes:
- V2 leads: explicit WRONG/CORRECT pattern, NEVER create before spawn
- V1 leads: separate workflow with manual create→spawn instructions
- team_tasks summary: de-emphasize "create", highlight auto-creation
- spawn team_task_id: clarify "omit to auto-create (recommended)"

Backend guards:
- Reject spawn with in-progress team_task_id (prevents reuse)
- Log warning on early claim race instead of silently ignoring
This commit is contained in:
viettranx committed 2026-03-14 14:23:15 +07:00
1 parent 8ad425f5f8
commit 08823c057e
4 files changed
+41 -12

No files matched your search

+23 -9
View File
@@ -142,18 +142,24 @@ func buildTeamMD(team *store.TeamData, members []store.TeamMemberData, selfID uu
}
}
// Workflow guidance
// Workflow guidance — version-aware to match backend behavior.
// V2: spawn auto-creates tasks, leads should NOT manually create.
// V1: leads must create tasks first, then spawn with team_task_id.
sb.WriteString("\n## Workflow\n\n")
if selfRole == store.TeamRoleLead {
sb.WriteString("**ONE delegation = ONE spawn call.** System auto-creates a tracking task per delegation.\n")
sb.WriteString("The `label` parameter sets the task title (keep it short). Tasks auto-complete when delegation finishes.\n\n")
sb.WriteString("Rules:\n")
sb.WriteString("- Call all spawns first, then briefly tell the user what you delegated\n")
sb.WriteString("- Do NOT add confirmations (\"Done!\", \"Got it!\") — just state what was assigned\n")
sb.WriteString("- Parallel results arrive in a single combined notification — do NOT present partial results\n")
sb.WriteString("- For dependency chains: `team_tasks` create with blocked_by, then `spawn` with team_task_id\n")
if isV2 {
sb.WriteString("**NEVER use `team_tasks create` before spawning.** The system handles task creation automatically.\n\n")
sb.WriteString("WRONG: `team_tasks(action=\"create\", ...)` → `spawn(team_task_id=...)`\n")
sb.WriteString("CORRECT: `spawn(agent=\"...\", task=\"...\", label=\"short title\")`\n\n")
sb.WriteString("Just call `spawn` — the system auto-creates one tracking task per delegation.\n")
sb.WriteString("The `label` parameter sets the auto-created task title. Tasks auto-complete when delegation finishes.\n\n")
sb.WriteString("Rules:\n")
sb.WriteString("- Each spawn call creates its own task — never pass the same `team_task_id` to multiple spawns\n")
sb.WriteString("- Call all spawns first, then briefly tell the user what you delegated\n")
sb.WriteString("- Do NOT add confirmations (\"Done!\", \"Got it!\") — just state what was assigned\n")
sb.WriteString("- Parallel results arrive in a single combined notification — do NOT present partial results\n")
sb.WriteString("- `team_tasks create` is ONLY for dependency chains (tasks with `blocked_by`), then `spawn` with that `team_task_id`\n")
sb.WriteString("\n## Orchestration Patterns\n\n")
sb.WriteString("- **Sequential**: A finishes → review → delegate to B with A's output\n")
sb.WriteString("- **Iterative**: A drafts → B reviews → A revises with feedback\n")
@@ -164,6 +170,14 @@ func buildTeamMD(team *store.TeamData, members []store.TeamMemberData, selfID uu
sb.WriteString("\n## Follow-up Reminders\n\n")
sb.WriteString("When waiting for user reply: create+claim task, then `await_reply` with text=<reminder>.\n")
sb.WriteString("System auto-sends reminders. Call `clear_followup` when user replies.\n")
} else {
sb.WriteString("Create a task with `team_tasks` first, then `spawn` with that `team_task_id`.\n")
sb.WriteString("Tasks auto-complete when delegation finishes.\n\n")
sb.WriteString("Rules:\n")
sb.WriteString("- Each task should have exactly one spawn (1:1 mapping)\n")
sb.WriteString("- Call all spawns first, then briefly tell the user what you delegated\n")
sb.WriteString("- Do NOT add confirmations (\"Done!\", \"Got it!\") — just state what was assigned\n")
sb.WriteString("- Parallel results arrive in a single combined notification — do NOT present partial results\n")
}
sb.WriteString("\nFor simple questions about team composition, answer directly from the member list above.\n")
+1 -1
View File
@@ -85,7 +85,7 @@ var coreToolSummaries = map[string]string{
"handoff": "Transfer conversation to another agent (ONLY when user explicitly asks to switch agents — NOT for task delegation)",
"evaluate_loop": "Run a generate→evaluate→revise loop between two agents for quality-critical tasks",
"delegate_search": "Search for agents by expertise to find the right delegation target",
"team_tasks": "Manage team task board (list, create, complete, cancel tasks)",
"team_tasks": "Team task board — track progress, manage dependencies (spawn auto-creates delegation tasks)",
"team_message": "Send messages to teammates (progress updates, questions)",
"workspace_write": "Write files to the team shared workspace (visible to all team members)",
"workspace_read": "Read, list, delete, pin, tag files in the team shared workspace",
+16 -1
View File
@@ -116,6 +116,18 @@ func (dm *DelegateManager) prepareDelegation(ctx context.Context, opts DelegateO
opts.TeamTaskID, teamTask.Status, ownerLabel)
}
// Guard: reject in-progress tasks — prevent multiple delegations on the same task.
// This catches the pattern where an LLM reuses a team_task_id across parallel spawns.
if teamTask.Status == store.TeamTaskStatusInProgress {
ownerLabel := "another delegation"
if teamTask.OwnerAgentKey != "" {
ownerLabel = teamTask.OwnerAgentKey
}
return nil, nil, fmt.Errorf(
"team_task_id %s is already in progress (claimed by %q). Each spawn needs its own task — omit team_task_id to auto-create.",
opts.TeamTaskID, ownerLabel)
}
if team != nil {
if teamTask.TeamID != team.ID {
return nil, nil, fmt.Errorf("team_task_id does not belong to your team")
@@ -141,7 +153,10 @@ func (dm *DelegateManager) prepareDelegation(ctx context.Context, opts DelegateO
// tasks that are already running. The ClaimTask in autoCompleteTeamTask()
// will harmlessly fail (WHERE status='pending' won't match).
if team != nil && IsTeamV2(team) {
_ = dm.teamStore.ClaimTask(ctx, opts.TeamTaskID, targetAgent.ID, teamTask.TeamID)
if err := dm.teamStore.ClaimTask(ctx, opts.TeamTaskID, targetAgent.ID, teamTask.TeamID); err != nil {
slog.Warn("delegate: task claim race — another delegation may have claimed this task",
"task_id", opts.TeamTaskID, "target", opts.TargetAgentKey, "error", err)
}
}
}
+1 -1
View File
@@ -90,7 +90,7 @@ func (t *SpawnTool) Parameters() map[string]any {
}
props["team_task_id"] = map[string]any{
"type": "string",
"description": "Team task ID to auto-complete when task finishes (for team workflows)",
"description": "Omit to auto-create a task (recommended). Only set for dependency chains with blocked_by tasks.",
}
props["estimated_duration"] = map[string]any{
"type": "number",