Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,19 @@ private async Task<RoleDialogModel> InnerCreateResponse(Agent agent, List<RoleDi
var response = await responsesClient.CreateResponseAsync(options);
var value = response.Value;

var functionCall = value.OutputItems.OfType<FunctionCallResponseItem>().FirstOrDefault();
// Every call the model asked for, not only the first. It routinely asks for several
// independent ones at once, and keeping one made it re-ask for the rest next turn.
var functionCalls = value.OutputItems.OfType<FunctionCallResponseItem>().ToList();
var toolCalls = ToLlmToolCalls(functionCalls);
var functionCall = functionCalls.FirstOrDefault();
var reasoningItem = value.OutputItems.OfType<ReasoningResponseItem>().FirstOrDefault();
var text = value.GetOutputText() ?? string.Empty;
var thinkingText = reasoningItem?.GetSummaryText();

RoleDialogModel responseMessage;
if (functionCall != null)
{
_logger.LogInformation($"Action: {nameof(InnerCreateResponse)}, Agent: {agent.Name}, ToolCall: {functionCall.FunctionName}");
_logger.LogInformation($"Action: {nameof(InnerCreateResponse)}, Agent: {agent.Name}, ToolCalls: {string.Join(",", toolCalls.Select(x => x.FunctionName))}");

responseMessage = new RoleDialogModel(AgentRole.Function, text)
{
Expand All @@ -42,6 +46,7 @@ private async Task<RoleDialogModel> InnerCreateResponse(Agent agent, List<RoleDi
ToolCallId = functionCall.CallId,
FunctionName = functionCall.FunctionName.NormalizeFunctionName(),
FunctionArgs = functionCall.FunctionArguments?.ToString(),
ToolCalls = toolCalls,

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. Parallel calls remain unhandled 🐞 Bug ≡ Correctness

Although the provider populates ToolCalls, RoutingService.InvokeAgent copies only the legacy
first-call fields and invokes one function, so every later requested call is discarded in the
standard execution path. The next Responses API request consequently contains only that first call
and output, allowing the model to request the omitted calls again.
Agent Prompt
## Issue description
The Responses API provider now returns every requested tool call in `RoleDialogModel.ToolCalls`, but the standard routing pipeline copies and executes only the legacy first-call fields. Update the routing and conversation-history flow to execute each call, preserve each call ID and arguments, and send every corresponding result back to the model while retaining single-call compatibility.

## Issue Context
The same correction must cover non-streaming and streaming Responses API results. Ensure each tool result remains associated with its provider call ID and that completion resumes only after all requested calls have produced results.

## Fix Focus Areas
- src/Plugins/BotSharp.Plugin.OpenAI/Providers/Chat/ChatCompletionProvider.Response.cs[28-49]
- src/Plugins/BotSharp.Plugin.OpenAI/Providers/Chat/ChatCompletionProvider.Response.cs[526-537]
- src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs[51-65]
- src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs[89-130]

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

RenderedInstruction = string.Join("\r\n", renderedInstructions)
};
}
Expand Down Expand Up @@ -113,15 +118,19 @@ private async Task<bool> InnerCreateResponseAsync(Agent agent,
var response = await responsesClient.CreateResponseAsync(options);
var value = response.Value;

var functionCall = value.OutputItems.OfType<FunctionCallResponseItem>().FirstOrDefault();
// Every call the model asked for, not only the first. It routinely asks for several
// independent ones at once, and keeping one made it re-ask for the rest next turn.
var functionCalls = value.OutputItems.OfType<FunctionCallResponseItem>().ToList();
var toolCalls = ToLlmToolCalls(functionCalls);
var functionCall = functionCalls.FirstOrDefault();
var reasoningItem = value.OutputItems.OfType<ReasoningResponseItem>().FirstOrDefault();
var text = value.GetOutputText() ?? string.Empty;
var thinkingText = reasoningItem?.GetSummaryText();

RoleDialogModel responseMessage;
if (functionCall != null)
{
_logger.LogInformation($"Action: {nameof(InnerCreateResponseAsync)}, Agent: {agent.Name}, ToolCall: {functionCall.FunctionName}");
_logger.LogInformation($"Action: {nameof(InnerCreateResponseAsync)}, Agent: {agent.Name}, ToolCalls: {string.Join(",", toolCalls.Select(x => x.FunctionName))}");

responseMessage = new RoleDialogModel(AgentRole.Function, text)
{
Expand All @@ -130,6 +139,7 @@ private async Task<bool> InnerCreateResponseAsync(Agent agent,
ToolCallId = functionCall.CallId,
FunctionName = functionCall.FunctionName.NormalizeFunctionName(),
FunctionArgs = functionCall.FunctionArguments?.ToString(),
ToolCalls = toolCalls,
RenderedInstruction = string.Join("\r\n", renderedInstructions)
};
}
Expand Down Expand Up @@ -221,7 +231,7 @@ private async Task<RoleDialogModel> InnerCreateResponseStreamingAsync(Agent agen

using var textStream = new RealtimeTextStream();
using var thinkingStream = new RealtimeTextStream();
FunctionCallResponseItem? functionCall = null;
var functionCalls = new List<FunctionCallResponseItem>();
ResponseResult? finalResult = null;
ResponseTokenUsage? tokenUsage = null;

Expand Down Expand Up @@ -312,7 +322,7 @@ private async Task<RoleDialogModel> InnerCreateResponseStreamingAsync(Agent agen
{
if (itemDone.Item is FunctionCallResponseItem fc)
{
functionCall = fc;
functionCalls.Add(fc);
#if DEBUG
_logger.LogDebug($"Tool Call (id: {fc.CallId}) => {fc.FunctionName}({fc.FunctionArguments})");
#endif
Expand Down Expand Up @@ -342,9 +352,12 @@ private async Task<RoleDialogModel> InnerCreateResponseStreamingAsync(Agent agen
var allText = textStream.GetText();
var thinkingText = thinkingStream.GetText();

var toolCalls = ToLlmToolCalls(functionCalls);
var functionCall = functionCalls.FirstOrDefault();

if (functionCall != null)
{
_logger.LogInformation($"Action: {nameof(InnerCreateResponseStreamingAsync)}, Agent: {agent.Name}, ToolCall: {functionCall.FunctionName}");
_logger.LogInformation($"Action: {nameof(InnerCreateResponseStreamingAsync)}, Agent: {agent.Name}, ToolCalls: {string.Join(",", toolCalls.Select(x => x.FunctionName))}");

responseMessage = new RoleDialogModel(AgentRole.Function, allText)
{
Expand All @@ -353,6 +366,7 @@ private async Task<RoleDialogModel> InnerCreateResponseStreamingAsync(Agent agen
ToolCallId = functionCall.CallId,
FunctionName = functionCall.FunctionName.NormalizeFunctionName(),
FunctionArgs = functionCall.FunctionArguments?.ToString(),
ToolCalls = toolCalls,
RenderedInstruction = string.Join("\r\n", renderedInstructions)
};
}
Expand Down Expand Up @@ -822,4 +836,9 @@ private void SetResponseFormat(CreateResponseOptions options, AgentLlmConfig? ll
} : null;
}
#endregion

private static List<LlmToolCall> ToLlmToolCalls(IEnumerable<FunctionCallResponseItem>? functionCalls)
=> (functionCalls ?? [])
.Select(x => new LlmToolCall(x.CallId, x.FunctionName, x.FunctionArguments?.ToString()))
.ToList();
}
Loading