Skip to content

.NET: [Bug]: TodoProvider injects a synthetic user message between assistant tool_calls and tool results, causing OpenAI 400 invalid_request_error #6953

Description

@soul-soft

Description

Version

  • Microsoft.Agents.AI.Harness: 1.13.0-preview.260703.1
  • Microsoft.Agents.AI.OpenAI: 1.13.0

Summary

When using HarnessAgent with the default TodoProvider enabled and an OpenAI chat client underneath, a simple function/tool call can fail with:

HTTP 400 (invalid_request_error: invalid_request_error)
An assistant message with 'tool_calls' must be followed by tool messages responding to each 'tool_call_id'.

The issue appears to be that TodoProvider injects a synthetic user message on every invocation, including the internal continuation call inside the function-invocation loop. That injected user message ends
up between:

  • the previous assistant message containing tool_calls
  • and the current tool message containing FunctionResultContent

This produces an invalid message sequence for OpenAI.

Minimal repro

var agent = scope.ServiceProvider.GetRequiredService();
var session = await agent.CreateSessionAsync();

AgentResponse response = await agent.RunAsync("获取当前路径", session);

while (true)
{
List approvalRequests = response.Messages
.SelectMany(static x => x.Contents)
.OfType()
.ToList();

  if (approvalRequests.Count == 0)
  {
      break;
  }

  List<ChatMessage> approvalMessages = approvalRequests
      .Select(static request =>
          new ChatMessage(ChatRole.User, [request.CreateResponse(approved: true)]))
      .ToList();

  response = await agent.RunAsync(approvalMessages, session);

}

This also reproduces without approval handling if the first model call returns a tool call and the framework performs the internal follow-up call to send the tool result back to the model.

Observed behavior

If I print the actual messages sent to the underlying OpenAI client, I see two requests.

First request:

[
{
"role": "user",
"contents": [
{
"$type": "text",
"text": "获取当前路径"
}
]
},
{
"role": "user",
"contents": [
{
"$type": "text",
"text": "### Current todo list\n- none yet"
}
],
"additionalProperties": {
"_attribution": {
"sourceType": {
"value": "AIContextProvider"
},
"sourceId": "Microsoft.Agents.AI.TodoProvider"
}
}
}
]

Second request:

[
{
"role": "user",
"contents": [
{
"$type": "text",
"text": "获取当前路径"
}
],
"additionalProperties": {
"_attribution": {
"sourceType": {
"value": "ChatHistory"
},
"sourceId": "Microsoft.Agents.AI.InMemoryChatHistoryProvider"
}
}
},
{
"role": "user",
"contents": [
{
"$type": "text",
"text": "### Current todo list\n- none yet"
}
],
"additionalProperties": {
"_attribution": {
"sourceType": {
"value": "ChatHistory"
},
"sourceId": "Microsoft.Agents.AI.InMemoryChatHistoryProvider"
}
}
},
{
"createdAt": "2026-07-07T10:15:00+00:00",
"role": "assistant",
"contents": [
{
"$type": "text",
"text": ""
},
{
"$type": "reasoning",
"text": "The user is asking "获取当前路径" which means "get the current path/directory". This is a simple question that doesn't require complex steps. Let me just run a shell command to show the
current working directory."
},
{
"$type": "functionCall",
"name": "run_shell",
"arguments": {
"command": "Get-Location | Select-Object -ExpandProperty Path"
},
"informationalOnly": true,
"callId": "call_00_qwP1EnxwrANWfcXYyKvn8275"
}
],
"messageId": "8972ba95-fa5a-43ae-8621-0cc301402483",
"additionalProperties": {
"_attribution": {
"sourceType": {
"value": "ChatHistory"
},
"sourceId": "Microsoft.Agents.AI.InMemoryChatHistoryProvider"
}
}
},
{
"role": "user",
"contents": [
{
"$type": "text",
"text": "### Current todo list\n- none yet"
}
],
"additionalProperties": {
"_attribution": {
"sourceType": {
"value": "AIContextProvider"
},
"sourceId": "Microsoft.Agents.AI.TodoProvider"
}
}
},
{
"role": "tool",
"contents": [
{
"$type": "functionResult",
"result": "C:\Users\DELL\.deepseek\r\nexit_code: 0",
"callId": "call_00_qwP1EnxwrANWfcXYyKvn8275"
}
]
}
]

OpenAI rejects this because the assistant tool call is followed by a synthetic user message before the matching tool result.

Exception

System.ClientModel.ClientResultException: HTTP 400 (invalid_request_error: invalid_request_error)

An assistant message with 'tool_calls' must be followed by tool messages responding to each 'tool_call_id'. (insufficient tool messages following tool_calls message)

Why I think this is a framework bug

The behavior seems to come from the interaction of these pieces:

  • TodoProvider injects a synthetic user message on every InvokingAsync
  • ChatClientAgent invokes AIContextProviders on every invocation, including internal function-calling continuations
  • ChatHistoryProvider persists the first round’s synthetic todo message and assistant function call
  • on the second internal call, chat history is loaded, then TodoProvider injects another synthetic user message, then the tool result is appended

This creates the invalid sequence:

assistant(tool_call)
user(todo)
tool(function_result)

Expected behavior

One of the following should happen:

  1. TodoProvider should not inject synthetic user messages during internal function-calling continuation calls.
  2. More generally, AIContextProvider-injected messages should not be inserted between an assistant message with tool calls and its matching tool results.
  3. Alternatively, TodoProvider could provide this state via instructions instead of a synthetic user message in these continuation scenarios.

Workaround

Disabling TodoProvider avoids the issue.

Question

Is this the intended behavior for TodoProvider during internal function-invocation loop iterations, or should providers like this be suppressed for continuation/tool-result turns?

Code Sample

Error Messages / Stack Traces

Package Versions

Microsoft.Agents.AI.OpenAI: 1.13.0 Microsoft.Agents.AI.Harness: 1.13.0-preview.260703.1

.NET Version

.net 10

Additional Context

No response

Activity

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

Metadata

Metadata

Labels

.NETUsage: [Issues, PRs], Target: .Netlikely-fixed

Type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions