Skip to content

fix router response to user#1381

Draft
iceljc wants to merge 1 commit into
SciSharp:masterfrom
iceljc:bugfix/fix-router-response-to-user
Draft

fix router response to user#1381
iceljc wants to merge 1 commit into
SciSharp:masterfrom
iceljc:bugfix/fix-router-response-to-user

Conversation

@iceljc

@iceljc iceljc commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

No description provided.

@qodo-code-review

Copy link
Copy Markdown
Contributor

PR Summary by Qodo

Fix router replies by propagating function name/args through reasoners

🐞 Bug fix 🕐 20-40 Minutes

Grey Divider

AI Description

• Preserve LLM-provided tool/function name and raw arguments when building routing instructions.
• Prefer raw function_args over re-serialized Arguments to avoid malformed router responses.
• Prevent assistant streaming/completion when routing explicitly requests stop completion.
Diagram

graph TD
  A{{"LLM chat completion"}} --> B["Reasoners"] --> C["FunctionCallFromLlm"] --> D["InstructExecutor"] --> E["Role dialogs"]
  C --> F["RoutingArgs"]
  subgraph Legend
    direction LR
    _ext{{"External"}} ~~~ _svc["Service"] ~~~ _mdl["Model"]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Centralize completion→instruction mapping in a shared helper
  • ➕ Removes repeated enrichment logic across HF/Naive/OneStepForward reasoners
  • ➕ Reduces risk of future reasoners drifting in behavior
  • ➕ Makes precedence rules (raw FunctionArgs vs serialized Arguments) explicit in one place
  • ➖ Requires a small refactor and potentially wider code touch
  • ➖ May be harder to keep reasoner-specific parsing nuances isolated
2. Represent FunctionArgs as structured JSON (JsonElement) end-to-end
  • ➕ Avoids raw-string vs re-serialized JSON inconsistencies
  • ➕ Enables validation and safer manipulation of tool arguments
  • ➖ Bigger model/API change across routing components
  • ➖ Requires careful compatibility handling for existing integrations expecting strings

Recommendation: The PR’s approach (prefer the raw function_args if provided, otherwise fall back to serialized Arguments) is the right minimal fix to preserve the LLM/tool-call intent and avoid router response corruption. Consider follow-up refactoring to centralize the enrichment logic to prevent future reasoner divergence.

Files changed (5) +74 / -6

Bug fix (5) +74 / -6
RoutingArgs.csAdd function_args to routing model +3/-0

Add function_args to routing model

• Extends RoutingArgs with a new JSON-serialized field (function_args) to carry raw function argument payloads. This enables preserving the original tool-call arguments rather than reconstructing them later.

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

HFReasoner.csPropagate function name/args from completion into routing instruction +21/-2

Propagate function name/args from completion into routing instruction

• After parsing the completion into FunctionCallFromLlm, the reasoner now overwrites Function and FunctionArgs with the completion-provided values when present. When building the planner message, it prefers the raw FunctionArgs string over re-serializing the Arguments object.

src/Infrastructure/BotSharp.Core/Routing/Reasoning/HFReasoner.cs

InstructExecutor.csPrefer raw FunctionArgs and honor StopCompletion on routing response +7/-1

Prefer raw FunctionArgs and honor StopCompletion on routing response

• Sets message.FunctionArgs to the raw inst.FunctionArgs when provided instead of always serializing the whole instruction. Also avoids the fallback completion path when StopCompletion is requested and propagates StopCompletion onto the returned response dialog.

src/Infrastructure/BotSharp.Core/Routing/Reasoning/InstructExecutor.cs

NaiveReasoner.csEnrich parsed instruction with completion function metadata +20/-1

Enrich parsed instruction with completion function metadata

• Copies FunctionName and FunctionArgs from the completion response into the parsed instruction when present. Uses the same precedence when populating message.FunctionArgs (raw FunctionArgs first, otherwise serialize Arguments).

src/Infrastructure/BotSharp.Core/Routing/Reasoning/NaiveReasoner.cs

OneStepForwardReasoner.csPreserve tool call function name/args in one-step routing flow +23/-2

Preserve tool call function name/args in one-step routing flow

• After parsing FunctionArgs into FunctionCallFromLlm, the reasoner now populates Function and FunctionArgs from the completion response to prevent format drift. Message construction now prefers raw FunctionArgs, falling back to serialized Arguments if absent.

src/Infrastructure/BotSharp.Core/Routing/Reasoning/OneStepForwardReasoner.cs

ⓘ You are approaching your monthly quota for Qodo. Upgrade your plan

@iceljc
iceljc marked this pull request as draft July 16, 2026 22:10
@qodo-code-review

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (1) 📜 Skill insights (0)

Grey Divider


Action required

1. FunctionArgs used without validation 📘 Rule violation ☼ Reliability
Description
The PR propagates raw LLM-provided response.FunctionArgs into message.FunctionArgs without
validating it as JSON or providing a safe fallback. Downstream callbacks immediately
JsonSerializer.Deserialize(...) message.FunctionArgs without guards, so malformed/empty
FunctionArgs can throw and fail routing/user responses.
Code

src/Infrastructure/BotSharp.Core/Routing/Reasoning/InstructExecutor.cs[R33-37]

        message.FunctionArgs = JsonSerializer.Serialize(inst);
+        if (!string.IsNullOrEmpty(inst.FunctionArgs))
+        {
+            message.FunctionArgs = inst.FunctionArgs;
+        }
Evidence
PR Compliance ID 2 requires explicit validation and safe failure at integration boundaries. The
updated reasoners copy response.FunctionArgs (external/LLM boundary) into inst.FunctionArgs, and
InstructExecutor then overwrites message.FunctionArgs with this raw value; routing functions
later deserialize message.FunctionArgs directly, which can throw on malformed JSON.

src/Infrastructure/BotSharp.Core/Routing/Reasoning/HFReasoner.cs[56-67]
src/Infrastructure/BotSharp.Core/Routing/Reasoning/InstructExecutor.cs[33-37]
src/Infrastructure/BotSharp.Core/Routing/Functions/ResponseToUserFn.cs[21-27]
src/Infrastructure/BotSharp.Core/Routing/RouteToAgentFn.cs[22-25]
Best Practice: Learned patterns

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`inst.FunctionArgs` is treated as trusted JSON and copied into `message.FunctionArgs` without validation. Since routing callbacks deserialize `message.FunctionArgs` directly, malformed LLM output can throw and break the request instead of failing safely.

## Issue Context
- `inst.FunctionArgs` is assigned from `response.FunctionArgs` (LLM/provider boundary).
- `message.FunctionArgs` is then deserialized in multiple callbacks without try/catch or null/empty guards.

## Fix Focus Areas
- src/Infrastructure/BotSharp.Core/Routing/Reasoning/InstructExecutor.cs[33-37]
- src/Infrastructure/BotSharp.Core/Routing/Reasoning/HFReasoner.cs[56-67]
- src/Infrastructure/BotSharp.Core/Routing/Reasoning/NaiveReasoner.cs[60-71]
- src/Infrastructure/BotSharp.Core/Routing/Reasoning/OneStepForwardReasoner.cs[72-83]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. StopCompletion not honored 🐞 Bug ≡ Correctness
Description
In InstructExecutor.Execute, the invoked routing function runs on a cloned RoleDialogModel (msg),
but the new guard uses message.StopCompletion (which is not updated), so InvokeAgent can still run
even when the function intended to stop completion (e.g., response_to_user). This can produce an
unintended extra router/agent completion and wrong final output flow.
Code

src/Infrastructure/BotSharp.Core/Routing/Reasoning/InstructExecutor.cs[R64-67]

+        else if (!message.StopCompletion)
        {
            var state = _services.GetRequiredService<IConversationStateService>();
            var useStreamMsg = state.GetState("use_stream_message");
Evidence
The function execution mutates the cloned dialog message, but the routing guard and later flow
consult the original loop message, so stop flags from functions like response_to_user are ignored
and routing proceeds.

src/Infrastructure/BotSharp.Core/Routing/Reasoning/InstructExecutor.cs[39-43]
src/Infrastructure/BotSharp.Core/Routing/Reasoning/InstructExecutor.cs[64-74]
src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeFunction.cs[25-66]
src/Infrastructure/BotSharp.Core/Routing/Functions/ResponseToUserFn.cs[21-28]
src/Infrastructure/BotSharp.Core/Routing/RoutingService.InstructLoop.cs[46-80]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`InstructExecutor.Execute` invokes the routing function using a cloned dialog `msg`, but then checks `message.StopCompletion` to decide whether to call `InvokeAgent`. Because `RoutingService.InvokeFunction` copies the stop flag back to the *passed* message instance (the clone), `message.StopCompletion` remains unchanged and routing continues even after a stop-intended function (e.g. `response_to_user`).

### Issue Context
- The executor creates `msg = RoleDialogModel.From(message, role: Function)` and invokes the function on `msg`.
- `response_to_user` sets `StopCompletion = true`.
- `RoutingService.InvokeFunction` copies `StopCompletion` back to its `message` parameter (the clone), not the original `message` held by the loop.

### Fix Focus Areas
- src/Infrastructure/BotSharp.Core/Routing/Reasoning/InstructExecutor.cs[39-80]

### Expected fix
- After `InvokeFunction(...)`, copy relevant termination state back (at minimum `message.StopCompletion = msg.StopCompletion`), or change the guard to check `msg.StopCompletion`.
- If `StopCompletion` is true after function execution, append an assistant response to `dialogs` (e.g., `dialogs.Add(RoleDialogModel.From(msg, role: AgentRole.Assistant, content: msg.Content))`) and return that response instead of calling `InvokeAgent`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. StopCompletion overwritten 🐞 Bug ≡ Correctness
Description
In InstructExecutor.Execute, response.StopCompletion = message.StopCompletion can clear a
StopCompletion flag already set on the actual response (e.g., by the LLM provider for max-token
truncation), preventing InstructLoop from breaking. This can cause extra routing loops and
unintended additional LLM/function calls.
Code

src/Infrastructure/BotSharp.Core/Routing/Reasoning/InstructExecutor.cs[79]

+        response.StopCompletion = message.StopCompletion;
Evidence
The routing loop break condition relies on the response’s StopCompletion; overwriting it from an
unrelated object can erase a true stop signal and keep the loop running.

src/Infrastructure/BotSharp.Core/Routing/Reasoning/InstructExecutor.cs[76-80]
src/Infrastructure/BotSharp.Core/Routing/RoutingService.InstructLoop.cs[75-80]
src/Plugins/BotSharp.Plugin.OpenAI/Providers/Chat/ChatCompletionProvider.Response.cs[359-370]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`InstructExecutor.Execute` unconditionally overwrites `dialogs.Last().StopCompletion` with `message.StopCompletion`. This can clear a legitimate termination flag set on the response object itself, leading to extra iterations in `RoutingService.InstructLoop`.

### Issue Context
`InstructLoop` uses the returned `response.StopCompletion` to decide whether to break. Some providers set `StopCompletion = true` on assistant responses (e.g., max-token responses).

### Fix Focus Areas
- src/Infrastructure/BotSharp.Core/Routing/Reasoning/InstructExecutor.cs[76-80]

### Expected fix
- Do not overwrite `response.StopCompletion` from `message.StopCompletion`.
- If you need to propagate a stop condition from earlier stages, combine instead of replace (e.g., `response.StopCompletion = response.StopCompletion || message.StopCompletion`) *after* correctly propagating the stop flag from the function-executed message (see other finding).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ You are approaching your monthly quota for Qodo. Upgrade your plan

Qodo Logo

Comment on lines 33 to +37
message.FunctionArgs = JsonSerializer.Serialize(inst);
if (!string.IsNullOrEmpty(inst.FunctionArgs))
{
message.FunctionArgs = inst.FunctionArgs;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. functionargs used without validation 📘 Rule violation ☼ Reliability

The PR propagates raw LLM-provided response.FunctionArgs into message.FunctionArgs without
validating it as JSON or providing a safe fallback. Downstream callbacks immediately
JsonSerializer.Deserialize(...) message.FunctionArgs without guards, so malformed/empty
FunctionArgs can throw and fail routing/user responses.
Agent Prompt
## Issue description
`inst.FunctionArgs` is treated as trusted JSON and copied into `message.FunctionArgs` without validation. Since routing callbacks deserialize `message.FunctionArgs` directly, malformed LLM output can throw and break the request instead of failing safely.

## Issue Context
- `inst.FunctionArgs` is assigned from `response.FunctionArgs` (LLM/provider boundary).
- `message.FunctionArgs` is then deserialized in multiple callbacks without try/catch or null/empty guards.

## Fix Focus Areas
- src/Infrastructure/BotSharp.Core/Routing/Reasoning/InstructExecutor.cs[33-37]
- src/Infrastructure/BotSharp.Core/Routing/Reasoning/HFReasoner.cs[56-67]
- src/Infrastructure/BotSharp.Core/Routing/Reasoning/NaiveReasoner.cs[60-71]
- src/Infrastructure/BotSharp.Core/Routing/Reasoning/OneStepForwardReasoner.cs[72-83]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +64 to 67
else if (!message.StopCompletion)
{
var state = _services.GetRequiredService<IConversationStateService>();
var useStreamMsg = state.GetState("use_stream_message");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Stopcompletion not honored 🐞 Bug ≡ Correctness

In InstructExecutor.Execute, the invoked routing function runs on a cloned RoleDialogModel (msg),
but the new guard uses message.StopCompletion (which is not updated), so InvokeAgent can still run
even when the function intended to stop completion (e.g., response_to_user). This can produce an
unintended extra router/agent completion and wrong final output flow.
Agent Prompt
### Issue description
`InstructExecutor.Execute` invokes the routing function using a cloned dialog `msg`, but then checks `message.StopCompletion` to decide whether to call `InvokeAgent`. Because `RoutingService.InvokeFunction` copies the stop flag back to the *passed* message instance (the clone), `message.StopCompletion` remains unchanged and routing continues even after a stop-intended function (e.g. `response_to_user`).

### Issue Context
- The executor creates `msg = RoleDialogModel.From(message, role: Function)` and invokes the function on `msg`.
- `response_to_user` sets `StopCompletion = true`.
- `RoutingService.InvokeFunction` copies `StopCompletion` back to its `message` parameter (the clone), not the original `message` held by the loop.

### Fix Focus Areas
- src/Infrastructure/BotSharp.Core/Routing/Reasoning/InstructExecutor.cs[39-80]

### Expected fix
- After `InvokeFunction(...)`, copy relevant termination state back (at minimum `message.StopCompletion = msg.StopCompletion`), or change the guard to check `msg.StopCompletion`.
- If `StopCompletion` is true after function execution, append an assistant response to `dialogs` (e.g., `dialogs.Add(RoleDialogModel.From(msg, role: AgentRole.Assistant, content: msg.Content))`) and return that response instead of calling `InvokeAgent`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

var response = dialogs.Last();

response.Instruction = inst;
response.StopCompletion = message.StopCompletion;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

3. Stopcompletion overwritten 🐞 Bug ≡ Correctness

In InstructExecutor.Execute, response.StopCompletion = message.StopCompletion can clear a
StopCompletion flag already set on the actual response (e.g., by the LLM provider for max-token
truncation), preventing InstructLoop from breaking. This can cause extra routing loops and
unintended additional LLM/function calls.
Agent Prompt
### Issue description
`InstructExecutor.Execute` unconditionally overwrites `dialogs.Last().StopCompletion` with `message.StopCompletion`. This can clear a legitimate termination flag set on the response object itself, leading to extra iterations in `RoutingService.InstructLoop`.

### Issue Context
`InstructLoop` uses the returned `response.StopCompletion` to decide whether to break. Some providers set `StopCompletion = true` on assistant responses (e.g., max-token responses).

### Fix Focus Areas
- src/Infrastructure/BotSharp.Core/Routing/Reasoning/InstructExecutor.cs[76-80]

### Expected fix
- Do not overwrite `response.StopCompletion` from `message.StopCompletion`.
- If you need to propagate a stop condition from earlier stages, combine instead of replace (e.g., `response.StopCompletion = response.StopCompletion || message.StopCompletion`) *after* correctly propagating the stop flag from the function-executed message (see other finding).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant