Skip to content

Conversation

@adenchen123
Copy link
Contributor

@adenchen123 adenchen123 commented Jan 28, 2026

User description

Introduce a retry mechanism in RoutingService.InvokeAgent to handle cases where the FunctionName is null or whitespace. Add IsRetry property to InvokeAgentOptions to prevent infinite retry loops. If a retry still results in a missing FunctionName, log an error and return a user-facing error message. Set IsRetry default to false. This improves robustness and user feedback for invalid function call requests.


PR Type

Bug fix, Enhancement


Description

  • Add retry mechanism for missing FunctionName in agent invocation

  • Introduce IsRetry property to InvokeAgentOptions preventing infinite loops

  • Log warnings/errors and return user-facing message on persistent failures

  • Improve robustness of function call request handling


Diagram Walkthrough

flowchart LR
  A["InvokeAgent called"] --> B{"FunctionName null or whitespace?"}
  B -->|Yes, first attempt| C["Set IsRetry=true"]
  C --> D["Retry InvokeAgent"]
  D --> B
  B -->|Yes, after retry| E["Log error"]
  E --> F["Set user-facing error message"]
  F --> G["Return true"]
  B -->|No| H["Proceed to InvokeFunction"]
Loading

File Walkthrough

Relevant files
Enhancement
InvokeOptions.cs
Add IsRetry property to InvokeAgentOptions                             

src/Infrastructure/BotSharp.Abstraction/Routing/Models/InvokeOptions.cs

  • Add IsRetry boolean property to InvokeAgentOptions class
  • Initialize IsRetry to false in Default() factory method
+3/-1     
Bug fix
RoutingService.InvokeAgent.cs
Implement retry logic for missing FunctionName handling   

src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs

  • Call new HandleEmptyFunctionNameRetry method after setting message
    properties
  • Add HandleEmptyFunctionNameRetry private method to handle
    null/whitespace FunctionName
  • Implement retry logic with IsRetry flag to prevent infinite loops
  • Log warnings on first retry attempt and errors on persistent failures
  • Return user-facing error message when FunctionName remains empty after
    retry
+45/-0   

Introduce a retry mechanism in RoutingService.InvokeAgent to handle cases where the FunctionName is null or whitespace. Add IsRetry property to InvokeAgentOptions to prevent infinite retry loops. If a retry still results in a missing FunctionName, log an error and return a user-facing error message. Set IsRetry default to false. This improves robustness and user feedback for invalid function call requests.
@qodo-code-review
Copy link

qodo-code-review bot commented Jan 28, 2026

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
🟢
No security concerns identified No security vulnerabilities detected by AI analysis. Human verification advised for critical code.
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

🔴
Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status:
Unstructured log messages: The new _logger.LogWarning/_logger.LogError calls use interpolated strings instead of
structured logging fields, reducing log parsability and audit usefulness.

Referred Code
    _logger.LogWarning($"Function name is empty, retrying InvokeAgent for agent {agentId}");
    options ??= InvokeAgentOptions.Default();
    options.IsRetry = true;
    var retryResult = await InvokeAgent(agentId, dialogs, options);
    return retryResult;
}
else
{
    // Already retried once, avoid infinite loop
    _logger.LogError($"Function name is still empty after retry for agent {agentId}, stopping to avoid infinite loop");

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status:
Missing audit context: The newly added warning/error logs for agent invocation retries include agentId but do not
include an explicit user identifier or a clear outcome context required for audit
reconstruction.

Referred Code
    _logger.LogWarning($"Function name is empty, retrying InvokeAgent for agent {agentId}");
    options ??= InvokeAgentOptions.Default();
    options.IsRetry = true;
    var retryResult = await InvokeAgent(agentId, dialogs, options);
    return retryResult;
}
else
{
    // Already retried once, avoid infinite loop
    _logger.LogError($"Function name is still empty after retry for agent {agentId}, stopping to avoid infinite loop");
    message.StopCompletion = true;
    message.Content = "I received a function call request but the function name is missing. Please try again.";
    message.Role = AgentRole.Assistant;
    dialogs.Add(message);
    Context.AddDialogs([message]);
    return true;

Learn more about managing compliance generic rules or creating your own custom rules

  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@qodo-code-review
Copy link

qodo-code-review bot commented Jan 28, 2026

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
High-level
Retry logic should guide the LLM
Suggestion Impact:The commit changed the same retry area but did not implement guided-retry prompting; instead it removed the empty-FunctionName retry helper and its invocation entirely, eliminating the existing "retry once with same dialogs" behavior.

code diff:

-
-            // Handle case when FunctionName is null or whitespace - retry once
-            var retryResult = await HandleEmptyFunctionNameRetry(agentId, message, dialogs, options);
-            if (retryResult.HasValue)
-            {
-                return retryResult.Value;
-            }
 
             await InvokeFunction(message, dialogs, options);
         }
@@ -147,43 +140,5 @@
 
         return true;
     }
-
-    /// <summary>
-    /// Handles the case when FunctionName is null or whitespace by retrying once.
-    /// Returns null if FunctionName is valid (no action needed), 
-    /// otherwise returns the result of handling (retry result or true if error was set).
-    /// </summary>
-    private async Task<bool?> HandleEmptyFunctionNameRetry(
-        string agentId,
-        RoleDialogModel message,
-        List<RoleDialogModel> dialogs,
-        InvokeAgentOptions? options)
-    {
-        if (string.IsNullOrWhiteSpace(message.FunctionName))
-        {
-            if (!(options?.IsRetry ?? false))
-            {
-                // Retry once by recursively calling InvokeAgent
-                _logger.LogWarning($"Function name is empty, retrying InvokeAgent for agent {agentId}");
-                options ??= InvokeAgentOptions.Default();
-                options.IsRetry = true;
-                var retryResult = await InvokeAgent(agentId, dialogs, options);
-                return retryResult;
-            }
-            else
-            {
-                // Already retried once, avoid infinite loop
-                _logger.LogError($"Function name is still empty after retry for agent {agentId}, stopping to avoid infinite loop");
-                message.StopCompletion = true;
-                message.Content = "I received a function call request but the function name is missing. Please try again.";
-                message.Role = AgentRole.Assistant;
-                dialogs.Add(message);
-                Context.AddDialogs([message]);
-                return true;
-            }
-        }
-
-        return null;
-    }

The current retry logic resubmits the same dialog history, which is unreliable.
It is suggested to modify the prompt on retry by adding a system message that
instructs the LLM to correct its previous invalid output.

Examples:

src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs [162-172]
        if (string.IsNullOrWhiteSpace(message.FunctionName))
        {
            if (!(options?.IsRetry ?? false))
            {
                // Retry once by recursively calling InvokeAgent
                _logger.LogWarning($"Function name is empty, retrying InvokeAgent for agent {agentId}");
                options ??= InvokeAgentOptions.Default();
                options.IsRetry = true;
                var retryResult = await InvokeAgent(agentId, dialogs, options);
                return retryResult;

 ... (clipped 1 lines)

Solution Walkthrough:

Before:

private async Task<bool?> HandleEmptyFunctionNameRetry(...)
{
    if (string.IsNullOrWhiteSpace(message.FunctionName))
    {
        if (!options.IsRetry)
        {
            // Retry once by recursively calling InvokeAgent
            _logger.LogWarning("Function name is empty, retrying...");
            options.IsRetry = true;
            // The exact same 'dialogs' are passed again.
            var retryResult = await InvokeAgent(agentId, dialogs, options);
            return retryResult;
        }
        else
        {
            // ... handle failure after retry
        }
    }
    return null;
}

After:

private async Task<bool?> HandleEmptyFunctionNameRetry(...)
{
    if (string.IsNullOrWhiteSpace(message.FunctionName))
    {
        if (!options.IsRetry)
        {
            _logger.LogWarning("Function name is empty, retrying with guidance...");
            options.IsRetry = true;
            
            // Add a corrective message to guide the LLM on retry.
            var retryDialogs = new List<RoleDialogModel>(dialogs);
            retryDialogs.Add(new RoleDialogModel(AgentRole.System, 
                "Your previous response was invalid because the function name was missing. Please provide a valid function call."));

            var retryResult = await InvokeAgent(agentId, retryDialogs, options);
            return retryResult;
        }
        else
        {
            // ... handle failure after retry
        }
    }
    return null;
}
Suggestion importance[1-10]: 9

__

Why: This suggestion correctly identifies a key weakness in the retry implementation and proposes a significantly more robust design by actively guiding the LLM to correct its mistake, rather than passively hoping for a different outcome.

High
Possible issue
Prevent duplicate dialog message entry
Suggestion Impact:The commit removed the entire retry/error-handling helper method that contained the duplicate-adding code path (including dialogs.Add(message) and Context.AddDialogs([message])), thereby eliminating the duplication issue indirectly.

code diff:

-
-    /// <summary>
-    /// Handles the case when FunctionName is null or whitespace by retrying once.
-    /// Returns null if FunctionName is valid (no action needed), 
-    /// otherwise returns the result of handling (retry result or true if error was set).
-    /// </summary>
-    private async Task<bool?> HandleEmptyFunctionNameRetry(
-        string agentId,
-        RoleDialogModel message,
-        List<RoleDialogModel> dialogs,
-        InvokeAgentOptions? options)
-    {
-        if (string.IsNullOrWhiteSpace(message.FunctionName))
-        {
-            if (!(options?.IsRetry ?? false))
-            {
-                // Retry once by recursively calling InvokeAgent
-                _logger.LogWarning($"Function name is empty, retrying InvokeAgent for agent {agentId}");
-                options ??= InvokeAgentOptions.Default();
-                options.IsRetry = true;
-                var retryResult = await InvokeAgent(agentId, dialogs, options);
-                return retryResult;
-            }
-            else
-            {
-                // Already retried once, avoid infinite loop
-                _logger.LogError($"Function name is still empty after retry for agent {agentId}, stopping to avoid infinite loop");
-                message.StopCompletion = true;
-                message.Content = "I received a function call request but the function name is missing. Please try again.";
-                message.Role = AgentRole.Assistant;
-                dialogs.Add(message);
-                Context.AddDialogs([message]);
-                return true;
-            }
-        }
-
-        return null;
-    }

Remove the redundant dialogs.Add(message) and Context.AddDialogs([message])
calls to prevent adding a duplicate message to the conversation history.

src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs [177-182]

 message.StopCompletion = true;
 message.Content = "I received a function call request but the function name is missing. Please try again.";
 message.Role = AgentRole.Assistant;
-dialogs.Add(message);
-Context.AddDialogs([message]);
 return true;

[Suggestion processed]

Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a bug where a modified message is re-added to the dialogs list, causing duplication. Removing the redundant dialogs.Add(message) and Context.AddDialogs([message]) calls fixes this issue.

Medium
Avoid mutating the input options
Suggestion Impact:The commit removed the entire retry helper method that was mutating the incoming `options` object (`options ??= ...; options.IsRetry = true`) and also removed the call site, eliminating the side effect rather than refactoring it to use a new options object.

code diff:

-
-            // Handle case when FunctionName is null or whitespace - retry once
-            var retryResult = await HandleEmptyFunctionNameRetry(agentId, message, dialogs, options);
-            if (retryResult.HasValue)
-            {
-                return retryResult.Value;
-            }
 
             await InvokeFunction(message, dialogs, options);
         }
@@ -147,43 +140,5 @@
 
         return true;
     }
-
-    /// <summary>
-    /// Handles the case when FunctionName is null or whitespace by retrying once.
-    /// Returns null if FunctionName is valid (no action needed), 
-    /// otherwise returns the result of handling (retry result or true if error was set).
-    /// </summary>
-    private async Task<bool?> HandleEmptyFunctionNameRetry(
-        string agentId,
-        RoleDialogModel message,
-        List<RoleDialogModel> dialogs,
-        InvokeAgentOptions? options)
-    {
-        if (string.IsNullOrWhiteSpace(message.FunctionName))
-        {
-            if (!(options?.IsRetry ?? false))
-            {
-                // Retry once by recursively calling InvokeAgent
-                _logger.LogWarning($"Function name is empty, retrying InvokeAgent for agent {agentId}");
-                options ??= InvokeAgentOptions.Default();
-                options.IsRetry = true;
-                var retryResult = await InvokeAgent(agentId, dialogs, options);
-                return retryResult;
-            }
-            else
-            {
-                // Already retried once, avoid infinite loop
-                _logger.LogError($"Function name is still empty after retry for agent {agentId}, stopping to avoid infinite loop");
-                message.StopCompletion = true;
-                message.Content = "I received a function call request but the function name is missing. Please try again.";
-                message.Role = AgentRole.Assistant;
-                dialogs.Add(message);
-                Context.AddDialogs([message]);
-                return true;
-            }
-        }
-
-        return null;
-    }

To avoid side effects, create a new InvokeAgentOptions object for the retry call
instead of mutating the existing options object.

src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs [168-171]

-options ??= InvokeAgentOptions.Default();
-options.IsRetry = true;
-var retryResult = await InvokeAgent(agentId, dialogs, options);
+var retryOptions = new InvokeAgentOptions
+{
+    From = options?.From ?? InvokeSource.Manual,
+    UseStream = options?.UseStream ?? false,
+    IsRetry = true
+};
+var retryResult = await InvokeAgent(agentId, dialogs, retryOptions);
 return retryResult;

[Suggestion processed]

Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies that mutating the input options object is a side effect that can lead to bugs if the object is reused, and proposes a robust solution by creating a new options object for the retry call.

Medium
  • Update

@JackJiang1234
Copy link
Contributor

reviewed

@adenchen123 adenchen123 changed the title Add retry for missing FunctionName in agent invocation logging infomation when LLM response function call Jan 29, 2026
@yileicn
Copy link
Member

yileicn commented Jan 29, 2026

reviewed

@yileicn yileicn merged commit dcf185d into SciSharp:master Jan 29, 2026
3 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants