diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStateService.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStateService.cs index 50bfa530..144edb71 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStateService.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStateService.cs @@ -377,6 +377,17 @@ public class ConversationStateService : IConversationStateService && !string.IsNullOrEmpty(_curStates[name].Values.Last().Data); } + /// + /// Argument names that must never be written as conversation state. They are BotSharp's own + /// LLM-routing keys (read back by CompletionProvider.GetProviderAndModel): a tool whose + /// arguments happen to use one of these names would otherwise silently retarget the model for + /// the next round. A board tool's provider argument, for example, replaced the resolved + /// LLM provider with "forgejo", so the re-invocation after the tool call resolved no completion + /// provider and threw a NullReferenceException in RoutingService.InvokeAgent. + /// + private static readonly HashSet ReservedLlmStateKeys = + new(StringComparer.OrdinalIgnoreCase) { "provider", "model", "model_id" }; + public void SaveStateByArgs(JsonDocument args) { if (args == null) @@ -388,6 +399,9 @@ public class ConversationStateService : IConversationStateService { foreach (JsonProperty property in root.EnumerateObject()) { + // Never let a function argument shadow the LLM routing state. + if (ReservedLlmStateKeys.Contains(property.Name)) continue; + var propertyValue = property.Value; var stateValue = propertyValue.ToString(); if (!string.IsNullOrEmpty(stateValue))