Skip to content

Commit 21e47ac

Browse files
PureWeenCopilot
andcommitted
Fix reflect loop: enforce all-worker participation before completion
- Track dispatched workers across iterations in the reflect loop - Override [[GROUP_REFLECT_COMPLETE]] if not all workers have participated - Include RoutingContext in synthesis prompt so orchestrator remembers I&C pattern - Show worker participation status (missing workers) in synthesis prompt - Add DISPATCHER ONLY language to replan prompt for iteration 2+ - Pass RoutingContext to replan prompt for consistent routing awareness Fixes: orchestrator prematurely completing after only worker-1 (Implementer) runs, without waiting for worker-2 (Challenger) to review. Also fixes the loop stopping after Challenger finds issues instead of continuing to have Implementer fix them. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent c5b1972 commit 21e47ac

1 file changed

Lines changed: 72 additions & 8 deletions

File tree

PolyPilot/Services/CopilotService.Organization.cs

Lines changed: 72 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1681,6 +1681,7 @@ private async Task SendViaOrchestratorReflectAsync(string groupId, List<string>
16811681
}
16821682

16831683
var workerNames = members.Where(m => m != orchestratorName).ToList();
1684+
var dispatchedWorkers = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
16841685

16851686
try
16861687
{
@@ -1705,7 +1706,7 @@ private async Task SendViaOrchestratorReflectAsync(string groupId, List<string>
17051706
}
17061707
else
17071708
{
1708-
planPrompt = BuildReplanPrompt(reflectState.LastEvaluation ?? "Continue iterating.", workerNames, prompt);
1709+
planPrompt = BuildReplanPrompt(reflectState.LastEvaluation ?? "Continue iterating.", workerNames, prompt, group.RoutingContext);
17091710
}
17101711

17111712
var planResponse = await SendPromptAndWaitAsync(orchestratorName, planPrompt, ct);
@@ -1760,10 +1761,15 @@ private async Task SendViaOrchestratorReflectAsync(string groupId, List<string>
17601761
var workerTasks = assignments.Select(a => ExecuteWorkerAsync(a.WorkerName, a.Task, prompt, ct));
17611762
var results = await Task.WhenAll(workerTasks);
17621763

1764+
// Track which workers have been dispatched across all iterations
1765+
foreach (var a in assignments)
1766+
dispatchedWorkers.Add(a.WorkerName);
1767+
17631768
// Phase 4: Synthesize + Evaluate
17641769
InvokeOnUI(() => OnOrchestratorPhaseChanged?.Invoke(groupId, OrchestratorPhase.Synthesizing, iterDetail));
17651770

1766-
var synthEvalPrompt = BuildSynthesisWithEvalPrompt(prompt, results.ToList(), reflectState);
1771+
var synthEvalPrompt = BuildSynthesisWithEvalPrompt(prompt, results.ToList(), reflectState,
1772+
group.RoutingContext, dispatchedWorkers, workerNames);
17671773

17681774
// Use dedicated evaluator session if configured, otherwise orchestrator self-evaluates
17691775
string evaluatorName = reflectState.EvaluatorSessionName ?? orchestratorName;
@@ -1783,14 +1789,26 @@ private async Task SendViaOrchestratorReflectAsync(string groupId, List<string>
17831789
var evaluatorModel = GetEffectiveModel(evaluatorName);
17841790
var trend = reflectState.RecordEvaluation(reflectState.CurrentIteration, score, rationale, evaluatorModel);
17851791

1786-
// Check if evaluator says complete
1787-
if (evalResponse.Contains("[[GROUP_REFLECT_COMPLETE]]", StringComparison.OrdinalIgnoreCase) || score >= 0.9)
1792+
// Check if evaluator says complete — but only if all workers have participated
1793+
var allWorkersDispatched = workerNames.All(w => dispatchedWorkers.Contains(w));
1794+
if ((evalResponse.Contains("[[GROUP_REFLECT_COMPLETE]]", StringComparison.OrdinalIgnoreCase) || score >= 0.9)
1795+
&& allWorkersDispatched)
17881796
{
17891797
reflectState.GoalMet = true;
17901798
reflectState.IsActive = false;
17911799
AddOrchestratorSystemMessage(orchestratorName, $"✅ {reflectState.BuildCompletionSummary()} (score: {score:F1})");
17921800
break;
17931801
}
1802+
else if (!allWorkersDispatched)
1803+
{
1804+
var missing = workerNames.Where(w => !dispatchedWorkers.Contains(w)).ToList();
1805+
Debug($"Reflection: overriding completion — workers not yet dispatched: {string.Join(", ", missing)}");
1806+
reflectState.LastEvaluation = $"Not all workers have participated yet. Missing: {string.Join(", ", missing)}. " +
1807+
"Dispatch to the remaining workers before completing.";
1808+
AddOrchestratorSystemMessage(orchestratorName,
1809+
$"🔄 Overriding completion — {string.Join(", ", missing)} haven't participated yet.");
1810+
continue;
1811+
}
17941812

17951813
reflectState.LastEvaluation = rationale;
17961814
if (trend == Models.QualityTrend.Degrading)
@@ -1800,14 +1818,30 @@ private async Task SendViaOrchestratorReflectAsync(string groupId, List<string>
18001818
{
18011819
synthesisResponse = await SendPromptAndWaitAsync(orchestratorName, synthEvalPrompt, ct);
18021820

1803-
// Check completion sentinel
1804-
if (synthesisResponse.Contains("[[GROUP_REFLECT_COMPLETE]]", StringComparison.OrdinalIgnoreCase))
1821+
// Check completion sentinel — but only if all workers have participated
1822+
var allWorkersDispatched = workerNames.All(w => dispatchedWorkers.Contains(w));
1823+
if (synthesisResponse.Contains("[[GROUP_REFLECT_COMPLETE]]", StringComparison.OrdinalIgnoreCase)
1824+
&& allWorkersDispatched)
18051825
{
18061826
reflectState.GoalMet = true;
18071827
reflectState.IsActive = false;
18081828
AddOrchestratorSystemMessage(orchestratorName, $"✅ {reflectState.BuildCompletionSummary()}");
18091829
break;
18101830
}
1831+
else if (synthesisResponse.Contains("[[GROUP_REFLECT_COMPLETE]]", StringComparison.OrdinalIgnoreCase)
1832+
&& !allWorkersDispatched)
1833+
{
1834+
// Override premature completion — not all workers have participated
1835+
var missing = workerNames.Where(w => !dispatchedWorkers.Contains(w)).ToList();
1836+
Debug($"Reflection: overriding [[GROUP_REFLECT_COMPLETE]] — workers not yet dispatched: {string.Join(", ", missing)}");
1837+
reflectState.LastEvaluation = $"Not all workers have participated yet. Missing: {string.Join(", ", missing)}. " +
1838+
"Dispatch to the remaining workers before completing.";
1839+
AddOrchestratorSystemMessage(orchestratorName,
1840+
$"🔄 Overriding completion — {string.Join(", ", missing)} haven't participated yet.");
1841+
reflectState.RecordEvaluation(reflectState.CurrentIteration, 0.3,
1842+
reflectState.LastEvaluation, GetEffectiveModel(orchestratorName));
1843+
continue;
1844+
}
18111845

18121846
// Extract evaluation for next iteration
18131847
reflectState.LastEvaluation = ExtractIterationEvaluation(synthesisResponse);
@@ -1895,11 +1929,30 @@ private async Task SendViaOrchestratorReflectAsync(string groupId, List<string>
18951929
}
18961930
}
18971931

1898-
private string BuildSynthesisWithEvalPrompt(string originalPrompt, List<WorkerResult> results, ReflectionCycle state)
1932+
private string BuildSynthesisWithEvalPrompt(string originalPrompt, List<WorkerResult> results, ReflectionCycle state,
1933+
string? routingContext = null, HashSet<string>? dispatchedWorkers = null, List<string>? allWorkers = null)
18991934
{
19001935
var sb = new System.Text.StringBuilder();
19011936
sb.Append(BuildSynthesisPrompt(originalPrompt, results));
19021937
sb.AppendLine();
1938+
if (!string.IsNullOrEmpty(routingContext))
1939+
{
1940+
sb.AppendLine("## Work Routing (from team definition)");
1941+
sb.AppendLine(routingContext);
1942+
sb.AppendLine();
1943+
}
1944+
// Show which workers have/haven't participated
1945+
if (dispatchedWorkers != null && allWorkers != null)
1946+
{
1947+
var missing = allWorkers.Where(w => !dispatchedWorkers.Contains(w)).ToList();
1948+
if (missing.Count > 0)
1949+
{
1950+
sb.AppendLine("### ⚠️ Worker Participation");
1951+
sb.AppendLine($"The following workers have NOT yet been dispatched: **{string.Join(", ", missing)}**");
1952+
sb.AppendLine("You MUST include `[[NEEDS_ITERATION]]` and dispatch to them before marking complete.");
1953+
sb.AppendLine();
1954+
}
1955+
}
19031956
sb.AppendLine($"## Evaluation Check (Iteration {state.CurrentIteration}/{state.MaxIterations})");
19041957
sb.AppendLine($"**Goal:** {state.Goal}");
19051958
sb.AppendLine();
@@ -1931,20 +1984,31 @@ private string BuildSynthesisWithEvalPrompt(string originalPrompt, List<WorkerRe
19311984
return sb.ToString();
19321985
}
19331986

1934-
private string BuildReplanPrompt(string lastEvaluation, List<string> workerNames, string originalPrompt)
1987+
private string BuildReplanPrompt(string lastEvaluation, List<string> workerNames, string originalPrompt,
1988+
string? routingContext = null)
19351989
{
19361990
var sb = new System.Text.StringBuilder();
1991+
sb.AppendLine("You are a DISPATCHER ONLY. You do NOT have tools. You CANNOT write code, read files, or run commands yourself.");
1992+
sb.AppendLine("Your ONLY job is to write @worker/@end blocks that assign work to your workers.");
1993+
sb.AppendLine();
19371994
sb.AppendLine("## Previous Iteration Evaluation");
19381995
sb.AppendLine(lastEvaluation);
19391996
sb.AppendLine();
19401997
sb.AppendLine("## Original Request (context)");
19411998
sb.AppendLine(originalPrompt);
19421999
sb.AppendLine();
2000+
if (!string.IsNullOrEmpty(routingContext))
2001+
{
2002+
sb.AppendLine("## Work Routing (from team definition)");
2003+
sb.AppendLine(routingContext);
2004+
sb.AppendLine();
2005+
}
19432006
sb.AppendLine($"Available workers ({workerNames.Count}):");
19442007
foreach (var w in workerNames)
19452008
sb.AppendLine($" - '{w}' (model: {GetEffectiveModel(w)})");
19462009
sb.AppendLine();
19472010
sb.AppendLine("Assign refined tasks using `@worker:name` / `@end` blocks to address the gaps identified above.");
2011+
sb.AppendLine("You MUST produce at least one @worker block. NEVER attempt to do the work yourself.");
19482012
return sb.ToString();
19492013
}
19502014

0 commit comments

Comments
 (0)