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:
- TodoProvider should not inject synthetic user messages during internal function-calling continuation calls.
- More generally, AIContextProvider-injected messages should not be inserted between an assistant message with tool calls and its matching tool results.
- 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
Description
Version
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:
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();
}
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:
This creates the invalid sequence:
assistant(tool_call)
user(todo)
tool(function_result)
Expected behavior
One of the following should happen:
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