convergence.ts
1,351 bytes
| 1 | /** |
|---|---|
| 2 | * The early-stop decision. |
| 3 | * |
| 4 | * Pure and side-effect free so it can be exhaustively unit-tested. The |
| 5 | * orchestrator calls this after each round with the convergence score returned |
| 6 | * by the assessor model and the current health of the council. |
| 7 | */ |
| 8 | |
| 9 | export type StopReason = 'converged' | 'max_rounds' | 'insufficient_models' | 'budget' | 'gavel' | 'continue'; |
| 10 | |
| 11 | export interface StopDecision { |
| 12 | stop: boolean; |
| 13 | reason: StopReason; |
| 14 | } |
| 15 | |
| 16 | export interface StopParams { |
| 17 | /** Convergence score for the round just completed (0-100). */ |
| 18 | score: number; |
| 19 | /** Threshold at/above which we consider the council converged. */ |
| 20 | threshold: number; |
| 21 | /** 1-based index of the round just completed. */ |
| 22 | round: number; |
| 23 | /** Configured maximum number of critique→revision rounds. */ |
| 24 | maxRounds: number; |
| 25 | /** Number of models still healthy enough to continue. */ |
| 26 | activeCount: number; |
| 27 | } |
| 28 | |
| 29 | export function shouldStop({ |
| 30 | score, |
| 31 | threshold, |
| 32 | round, |
| 33 | maxRounds, |
| 34 | activeCount, |
| 35 | }: StopParams): StopDecision { |
| 36 | // A debate needs at least two voices; below that there is nothing to debate. |
| 37 | if (activeCount < 2) return { stop: true, reason: 'insufficient_models' }; |
| 38 | if (score >= threshold) return { stop: true, reason: 'converged' }; |
| 39 | if (round >= maxRounds) return { stop: true, reason: 'max_rounds' }; |
| 40 | return { stop: false, reason: 'continue' }; |
| 41 | } |
| 42 | |