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

  1. Feed each middleware invocation a homogeneous stream: either all ToolCallMessageUpdate chunks or non-tool chunks, not interleaved.
  2. Buffer by type: route ToolCallMessageUpdate chunks to one accumulator and other messages through the normal path (yield return).
  3. 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.
  4. 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

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


AI-assisted analysis of microsoft/autogen@027ecf0a37 (2026-08-15). Data as JSON: /api/errors/67f4d7d2589b2bb6. Report an issue: GitHub.