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

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