microsoft/autogen · error · InvalidOperationException
The first message is ToolCallMessage, but the update message
Error message
The first message is ToolCallMessage, but the update message is not ToolCallMessageUpdate
What it means
FunctionCallMiddleware's streaming merge loop maintains a single mergedFunctionCallMessage across chunks. When a chunk is a ToolCallMessageUpdate and functionMap exists, it either creates the first ToolCallMessage or calls Update on the existing one; the else branch throws InvalidOperationException('The first message is ToolCallMessage, but the update message is not ToolCallMessageUpdate'). Despite the wording, it fires when a merge was already started with tool-call updates and a later chunk in the same stream is not a ToolCallMessageUpdate, so the established ToolCallMessage can no longer be updated.
Source
Thrown at dotnet/src/AutoGen.Core/Middleware/FunctionCallMiddleware.cs:119
var combinedFunctions = this.functions?.Concat(options.Functions ?? []) ?? options.Functions;
options.Functions = combinedFunctions?.ToArray();
IMessage? mergedFunctionCallMessage = default;
await foreach (var message in agent.GenerateStreamingReplyAsync(context.Messages, options, cancellationToken))
{
if (message is ToolCallMessageUpdate toolCallMessageUpdate && this.functionMap != null)
{
if (mergedFunctionCallMessage is null)
{
mergedFunctionCallMessage = new ToolCallMessage(toolCallMessageUpdate);
}
else if (mergedFunctionCallMessage is ToolCallMessage toolCall)
{
toolCall.Update(toolCallMessageUpdate);
}
else
{
throw new InvalidOperationException("The first message is ToolCallMessage, but the update message is not ToolCallMessageUpdate");
}
}
else if (message is ToolCallMessage toolCallMessage1)
{
mergedFunctionCallMessage = toolCallMessage1;
}
else
{
yield return message;
}
}
if (mergedFunctionCallMessage is ToolCallMessage toolCallMsg)
{
yield return await this.InvokeToolCallMessagesAfterInvokingAgentAsync(toolCallMsg, agent);
}
}
View on GitHub (pinned to 027ecf0a37)
Solutions
- Feed each middleware invocation a homogeneous stream: either all ToolCallMessageUpdate chunks or non-tool chunks, not interleaved.
- Buffer by type: route ToolCallMessageUpdate chunks to one accumulator and other messages through the normal path (yield return).
- Check the stream before merging: if a ToolCallMessage update is followed by a non-update message, close/emit the merged tool-call message first, then handle the next message separately.
- Update to a newer AutoGen version if you hit this from an SDK stream, and report the event sequence upstream.
Example fix
// before
var stream = new IMessage[] { toolCallUpdate1, textMessageUpdate }; // mixed kinds
await foreach (var m in middleware.ProcessAsync(agent, stream)) { ... } // throws
// after
// emit tool-call updates as one batch, then the other messages
var toolUpdates = stream.OfType<ToolCallMessageUpdate>().ToList();
var others = stream.Where(m => m is not ToolCallMessageUpdate);
var merged = ProcessMerged(toolUpdates.Concat(others)); Defensive patterns
Strategy: validation
Validate before calling
// ensure a homogeneous chunk stream before feeding the middleware
bool startsWithToolUpdates = messages.FirstOrDefault() is ToolCallMessageUpdate;
bool homogeneous = !startsWithToolUpdates || messages.All(m => m is ToolCallMessageUpdate or ToolCallMessage);
if (!homogeneous) { splitAndEmitSeparately(messages); } Type guard
static bool IsHomogeneousStream(IEnumerable<IMessage> stream)
{
var hasUpdates = stream.Any(m => m is ToolCallMessageUpdate);
var hasOthers = stream.Any(m => m is not (ToolCallMessageUpdate or ToolCallMessage));
return !(hasUpdates && hasOthers);
} Try / catch
try { await foreach (var m in middlewareStream) { } }
catch (InvalidOperationException ex) when (ex.Message.Contains("not ToolCallMessageUpdate"))
{
// replay: batch tool-call updates first, then forward remaining messages
} Prevention
- Never interleave tool-call update chunks with other message kinds in one middleware pass.
- Split mixed streams by type and process each group through its own invocation.
- Capture the exact event sequence when reporting this to maintainers.
When it happens
Trigger: A stream that begins with ToolCallMessageUpdate chunks (partial function arguments) followed by a plain message or TextMessageUpdate chunk while mergedFunctionCallMessage is already a ToolCallMessage; interleaving content chunks inside a function-call stream; heterogeneous streaming events fed into one middleware invocation.
Common situations: Models emitting mixed content+tool-call streams; custom streaming code that forwards every event type through FunctionCallMiddleware in one batch; test harnesses replaying recorded streams whose event order mixes update kinds.
Related errors
- FunctionMap is not available
- The message is not a valid message
- From mismatch
- The recent update is not a ToolCallMessage
- Invalid streaming message type {reply.GetType().Name}
AI-assisted analysis of microsoft/autogen@027ecf0a37 (2026-08-15).
Data as JSON: /api/errors/67f4d7d2589b2bb6.
Report an issue: GitHub.