fix(team): reject clarifying worker results (#610)

This commit is contained in:
matevip 2026-08-20 23:09:22 -04:00
parent 1eabbb68ac
commit 770198ba69
2 changed files with 70 additions and 1 deletions

View File

@ -57,6 +57,26 @@ public class TeamDispatchService {
private static final Pattern GENERATED_FILE_MARKDOWN_LINK = Pattern.compile( private static final Pattern GENERATED_FILE_MARKDOWN_LINK = Pattern.compile(
"\\[([^\\]\\r\\n]{1,200})]\\(((?:https?://[^/\\s)\\]]+)?/api/v1/files/generated/[A-Za-z0-9-]+)\\)"); "\\[([^\\]\\r\\n]{1,200})]\\(((?:https?://[^/\\s)\\]]+)?/api/v1/files/generated/[A-Za-z0-9-]+)\\)");
private static final Set<String> CLARIFICATION_CUES = Set.of(
"您希望",
"你希望",
"是否继续",
"请问",
"能否",
"可以提供",
"请提供",
"需要您",
"需要你",
"我应该",
"如何处理",
"what would you like",
"how should i",
"could you provide",
"please provide",
"do you want me",
"should i",
"would you like");
/** One JDK 21 virtual thread per member-agent run. */ /** One JDK 21 virtual thread per member-agent run. */
private static final ExecutorService DISPATCH_EXECUTOR = private static final ExecutorService DISPATCH_EXECUTOR =
Executors.newVirtualThreadPerTaskExecutor(); Executors.newVirtualThreadPerTaskExecutor();
@ -337,9 +357,37 @@ public class TeamDispatchService {
&& taskService.listDeliverables(task).isEmpty()) { && taskService.listDeliverables(task).isEmpty()) {
return "required deliverable was not attached"; return "required deliverable was not attached";
} }
if (looksLikeClarificationQuestion(reply)) {
return "member asked for clarification instead of producing a result";
}
return null; return null;
} }
private boolean looksLikeClarificationQuestion(String reply) {
if (reply == null) {
return false;
}
String normalized = reply.strip().replaceAll("\\s+", " ");
if (normalized.isBlank() || normalized.length() > 800) {
return false;
}
String lower = normalized.toLowerCase();
boolean hasCue = CLARIFICATION_CUES.stream().anyMatch(lower::contains);
if (!hasCue) {
return false;
}
return normalized.endsWith("?")
|| normalized.endsWith("")
|| lower.contains("what would you like")
|| lower.contains("how should i")
|| lower.contains("could you provide")
|| lower.contains("please provide")
|| normalized.contains("请问")
|| normalized.contains("是否继续")
|| normalized.contains("如何处理")
|| normalized.contains("请提供");
}
private boolean attachGeneratedFileDeliverable(TeamTaskEntity task, String reply) { private boolean attachGeneratedFileDeliverable(TeamTaskEntity task, String reply) {
if (!requiresDeliverable(task) || reply == null || reply.isBlank()) { if (!requiresDeliverable(task) || reply == null || reply.isBlank()) {
return false; return false;
@ -393,7 +441,8 @@ public class TeamDispatchService {
- Execute this task now. Your final reply becomes the task result reported to the team lead, so end with a complete, self-contained summary of what you produced. - Execute this task now. Your final reply becomes the task result reported to the team lead, so end with a complete, self-contained summary of what you produced.
- Report milestones with team_tasks(action="progress", taskId=%s, percent=..., step=...). - Report milestones with team_tasks(action="progress", taskId=%s, percent=..., step=...).
- If the output is a document, spreadsheet or presentation, generate a real file (renderDocx / renderXlsx / renderPptx or the docx/pptx/xlsx skills) and register it with team_tasks(action="attach", taskId=%s, name="<file name>", url=<the download link the render tool returned>). Keep the result a summary do not paste file contents. - If the output is a document, spreadsheet or presentation, generate a real file (renderDocx / renderXlsx / renderPptx or the docx/pptx/xlsx skills) and register it with team_tasks(action="attach", taskId=%s, name="<file name>", url=<the download link the render tool returned>). Keep the result a summary do not paste file contents.
- If you are missing an input you cannot obtain yourself, call team_tasks(action="comment", taskId=%s, type="blocker", text="what you need") and stop. - If scope is ambiguous but you can make a reasonable assumption, state the assumption and continue.
- If you are missing an input you cannot obtain yourself, call team_tasks(action="comment", taskId=%s, type="blocker", text="what you need") and stop. Do not ask the lead or user for clarification in your final reply.
""".formatted(task.getId(), task.getId(), task.getId())); """.formatted(task.getId(), task.getId(), task.getId()));
return sb.toString(); return sb.toString();
} }

View File

@ -173,6 +173,26 @@ class TeamDispatchServiceTest {
verify(eventChannel).publishTaskEvent(any(), eq("team_task_retrying"), any()); verify(eventChannel).publishTaskEvent(any(), eq("team_task_retrying"), any());
} }
@Test
@DisplayName("a clarifying question is requeued instead of being reported as completed")
void settleClarifyingQuestionRequeues() {
TeamTaskEntity running = task(1L, MEMBER_A);
running.setStatus(TeamTaskStatus.IN_PROGRESS);
running.setDispatchCount(1);
when(taskService.getTask(1L)).thenReturn(running);
when(taskService.requeueUnusableResult(1L,
"member asked for clarification instead of producing a result"))
.thenReturn(true);
service.settleOutcome(running, "您希望我如何处理?");
verify(taskService).requeueUnusableResult(1L,
"member asked for clarification instead of producing a result");
verify(taskService, never()).completeTask(any(), any(), anyString());
verify(announceService, never()).announceTaskSettled(any());
verify(eventChannel).publishTaskEvent(any(), eq("team_task_retrying"), any());
}
@Test @Test
@DisplayName("an unusable third result fails instead of bypassing the circuit breaker") @DisplayName("an unusable third result fails instead of bypassing the circuit breaker")
void settleFallbackFailsAfterDispatchBudget() { void settleFallbackFailsAfterDispatchBudget() {