@@ -129,6 +129,23 @@ interface FetchMetadata {
129129 userHappinessScore : number | undefined ;
130130}
131131
132+ namespace FetchResult {
133+ export class Lines {
134+ constructor (
135+ readonly linesStream : AsyncIterable < string > ,
136+ readonly getFetchFailure : ( ) => NoNextEditReason | undefined ,
137+ readonly getResponseSoFar : ( ) => string ,
138+ readonly fetchRequestStopWatch : StopWatch ,
139+ ) { }
140+ }
141+ export class ModelNotFound { public static INSTANCE = new ModelNotFound ( ) ; }
142+ export class Error {
143+ constructor ( readonly reason : NoNextEditReason ) { }
144+ }
145+
146+ export type t = Lines | ModelNotFound | Error ;
147+ }
148+
132149export class XtabProvider implements IStatelessNextEditProvider {
133150
134151 public static readonly ID = XTabProviderId ;
@@ -700,23 +717,27 @@ export class XtabProvider implements IStatelessNextEditProvider {
700717 }
701718 }
702719
703- private async * _streamEditsImpl (
704- request : StatelessNextEditRequest ,
705- editStreamCtx : EditStreamContext ,
706- responseOpts : ResponseOpts ,
720+ /**
721+ * Initiates the HTTP fetch, sets up the streaming pipeline, and returns either
722+ * a clean line stream (with cursor-tag removal and latency logging applied)
723+ * or an error / retry signal.
724+ *
725+ * This method encapsulates all fetch infrastructure so that downstream response
726+ * format handlers only need an `AsyncIterable<string>` line stream.
727+ */
728+ private async _performFetch (
729+ endpoint : IChatEndpoint ,
730+ messages : Raw . ChatMessage [ ] ,
731+ prediction : Prediction | undefined ,
732+ requestId : string ,
707733 fetchMetadata : FetchMetadata ,
708- retryState : RetryState . t ,
709- delaySession : DelaySession ,
710- tracing : RequestTracingContext ,
711- cancellationToken : CancellationToken ,
712- fetchCts : CancellationTokenSource ,
734+ shouldRemoveCursorTagFromResponse : boolean ,
735+ editWindow : OffsetRange ,
736+ documentBeforeEdits : StringText ,
713737 fetchCancellationToken : CancellationToken ,
714- ) : EditStreaming {
738+ tracing : RequestTracingContext ,
739+ ) : Promise < FetchResult . t > {
715740 const { tracer, logContext, telemetry } = tracing ;
716- const { endpoint, messages, clippedTaggedCurrentDoc, editWindowInfo, promptPieces, prediction, originalEditWindow } = editStreamCtx ;
717- const { editWindow, editWindowLines, cursorOriginalLinesOffset, editWindowLineRange } = editWindowInfo ;
718-
719- const targetDocument = request . getActiveDocument ( ) . id ;
720741
721742 const useFetcher = this . configService . getExperimentBasedConfig ( ConfigKey . NextEditSuggestionsFetcher , this . expService ) || undefined ;
722743
@@ -732,7 +753,7 @@ export class XtabProvider implements IStatelessNextEditProvider {
732753
733754 const firstTokenReceived = new DeferredPromise < void > ( ) ;
734755
735- logContext . setHeaderRequestId ( request . headerRequestId ) ;
756+ logContext . setHeaderRequestId ( requestId ) ;
736757
737758 telemetry . setFetchStartedAt ( ) ;
738759 logContext . setFetchStartTime ( ) ;
@@ -765,7 +786,7 @@ export class XtabProvider implements IStatelessNextEditProvider {
765786 } satisfies OptionalChatRequestParams ,
766787 userInitiatedRequest : undefined ,
767788 telemetryProperties : {
768- requestId : request . headerRequestId ,
789+ requestId,
769790 } ,
770791 useFetcher,
771792 customMetadata : {
@@ -785,13 +806,13 @@ export class XtabProvider implements IStatelessNextEditProvider {
785806 ! this . forceUseDefaultModel // if we haven't already forced using the default model; otherwise, this could cause an infinite loop
786807 ) {
787808 this . forceUseDefaultModel = true ;
788- return yield * this . doGetNextEdit ( request , delaySession , tracing , cancellationToken , retryState ) ; // use the same retry state
809+ return FetchResult . ModelNotFound . INSTANCE ;
789810 }
790811 // diff-patch based model returns no choices if it has no edits to suggest
791812 if ( fetchRes . type === ChatFetchResponseType . Unknown && fetchRes . reason === RESPONSE_CONTAINED_NO_CHOICES ) {
792- return new NoNextEditReason . NoSuggestions ( request . documentBeforeEdits , editWindow ) ;
813+ return new FetchResult . Error ( new NoNextEditReason . NoSuggestions ( documentBeforeEdits , editWindow ) ) ;
793814 }
794- return mapChatFetcherErrorToNoNextEditReason ( fetchRes ) ;
815+ return new FetchResult . Error ( mapChatFetcherErrorToNoNextEditReason ( fetchRes ) ) ;
795816 }
796817
797818 fetchResultPromise
@@ -829,15 +850,53 @@ export class XtabProvider implements IStatelessNextEditProvider {
829850 const trace = `Line ${ i ++ } emitted with latency ${ fetchRequestStopWatch . elapsed ( ) } ms` ;
830851 tracer . trace ( trace ) ;
831852
832- yield responseOpts . shouldRemoveCursorTagFromResponse
853+ yield shouldRemoveCursorTagFromResponse
833854 ? v . replaceAll ( PromptTags . CURSOR , '' )
834855 : v ;
835856 }
836857 } ) ( ) ;
837858
859+ return new FetchResult . Lines ( linesStream , getFetchFailure , ( ) => responseSoFar , fetchRequestStopWatch ) ;
860+ }
861+
862+ private async * _streamEditsImpl (
863+ request : StatelessNextEditRequest ,
864+ editStreamCtx : EditStreamContext ,
865+ responseOpts : ResponseOpts ,
866+ fetchMetadata : FetchMetadata ,
867+ retryState : RetryState . t ,
868+ delaySession : DelaySession ,
869+ tracing : RequestTracingContext ,
870+ cancellationToken : CancellationToken ,
871+ fetchCts : CancellationTokenSource ,
872+ fetchCancellationToken : CancellationToken ,
873+ ) : EditStreaming {
874+ const { tracer, logContext, telemetry } = tracing ;
875+ const { endpoint, messages, clippedTaggedCurrentDoc, editWindowInfo, promptPieces, prediction, originalEditWindow } = editStreamCtx ;
876+ const { editWindow, editWindowLines, cursorOriginalLinesOffset, editWindowLineRange } = editWindowInfo ;
877+
878+ const targetDocument = request . getActiveDocument ( ) . id ;
879+
880+ // Phase 1: Fetch lifecycle — initiate HTTP request and produce a clean line stream
881+ const fetchResult = await this . _performFetch (
882+ endpoint , messages , prediction , request . headerRequestId ,
883+ fetchMetadata , responseOpts . shouldRemoveCursorTagFromResponse ,
884+ editWindow , request . documentBeforeEdits ,
885+ fetchCancellationToken , tracing ,
886+ ) ;
887+
888+ if ( fetchResult instanceof FetchResult . ModelNotFound ) {
889+ return yield * this . doGetNextEdit ( request , delaySession , tracing , cancellationToken , retryState ) ;
890+ }
891+ if ( fetchResult instanceof FetchResult . Error ) {
892+ return fetchResult . reason ;
893+ }
894+
895+ const { linesStream, getFetchFailure, getResponseSoFar, fetchRequestStopWatch } = fetchResult ;
896+
897+ // Phase 2: Dispatch to the appropriate response format handler
838898 const isFromCursorJump = retryState instanceof RetryState . Retrying && retryState . reason === 'cursorJump' ;
839899
840- // Dispatch to the appropriate response format handler
841900 let parseResult : ResponseParseResult . t ;
842901
843902 switch ( responseOpts . responseFormat ) {
@@ -1019,7 +1078,7 @@ export class XtabProvider implements IStatelessNextEditProvider {
10191078 }
10201079 }
10211080
1022- logContext . setResponse ( responseSoFar ) ;
1081+ logContext . setResponse ( getResponseSoFar ( ) ) ;
10231082
10241083 for ( const singleLineEdit of singleLineEdits ) {
10251084 tracer . trace ( `extracting edit #${ i } : ${ singleLineEdit . toString ( ) } ` ) ;
0 commit comments