@@ -15,6 +15,7 @@ limitations under the License.
1515******************************************************************************/
1616
1717using BotSharp . Abstraction . Infrastructures . Enums ;
18+ using BotSharp . Abstraction . MLTasks ;
1819using BotSharp . Abstraction . Routing . Models ;
1920using BotSharp . Abstraction . Routing . Reasoning ;
2021using BotSharp . Abstraction . Templating ;
@@ -61,14 +62,15 @@ public async Task<FunctionCallFromLlm> GetNextInstruction(Agent router, string m
6162 MessageId = messageId
6263 }
6364 } ;
64- var response = await completion . GetChatCompletions ( router , dialogs ) ;
6565
66- // Due to format drift, LLMs may complete with finishReason=function_call (instruction in FunctionArgs)
67- // or finishReason=stop (instruction serialized as JSON in Content).
68- // Use FunctionArgs ?? Content to be compatible with both cases.
69- var inst = ( response . FunctionArgs ?? response . Content ) . JsonContent < FunctionCallFromLlm > ( ) ;
70- var routingCtx = _services . GetRequiredService < IRoutingContext > ( ) ;
71- _logger . LogInformation ( $ "[OneStepForwardReasoner] ConversationId: { routingCtx . ConversationId } , MessageId: { messageId } , Next instruction: { response . FunctionArgs ?? response . Content } ") ;
66+ // Force tool_choice=required so the LLM always returns the instruction as a function call,
67+ // eliminating format drift where the LLM completes with finishReason=stop and returns
68+ // free text or JSON in Content instead of a structured function call.
69+ var response = await GetChatCompletionsWithScopedState ( completion , router , dialogs , "tool_choice" , "required" ) ;
70+
71+ var inst = response . FunctionArgs ? . JsonContent < FunctionCallFromLlm > ( ) ;
72+ _logger . LogInformation ( "[OneStepForwardReasoner] ConversationId: {ConversationId}, MessageId: {MessageId}, Next instruction: {Instruction}" ,
73+ _services . GetRequiredService < IRoutingContext > ( ) . ConversationId , messageId , response . FunctionArgs ) ;
7274
7375 // Fix LLM malformed response
7476 await ReasonerHelper . FixMalformedResponse ( _services , inst ) ;
@@ -107,6 +109,30 @@ public async Task<bool> AgentExecuted(Agent router, FunctionCallFromLlm inst, Ro
107109 return true ;
108110 }
109111
112+ /// <summary>
113+ /// Runs chat completion with a scoped conversation state that is set before the call
114+ /// and guaranteed to be removed afterwards, even if the completion throws.
115+ /// </summary>
116+ private async Task < RoleDialogModel > GetChatCompletionsWithScopedState (
117+ IChatCompletion completion ,
118+ Agent agent ,
119+ List < RoleDialogModel > dialogs ,
120+ string stateKey ,
121+ string stateValue )
122+ {
123+ var states = _services . GetRequiredService < IConversationStateService > ( ) ;
124+ states . SetState ( stateKey , stateValue , source : StateSource . Application ) ;
125+
126+ try
127+ {
128+ return await completion . GetChatCompletions ( agent , dialogs ) ;
129+ }
130+ finally
131+ {
132+ states . RemoveState ( stateKey ) ;
133+ }
134+ }
135+
110136 private string GetNextStepPrompt ( Agent router )
111137 {
112138 var template = router . Templates . First ( x => x . Name == "reasoner.one-step-forward" ) . Content ;
0 commit comments