Skip to content

.NET: Fix: OutputConverter.FunctionResultContent creates OutputItemFunction…#6247

Open
enhendrickson wants to merge 3 commits into
microsoft:mainfrom
provisions-group:fix/outputconverter-null-id-persistence
Open

.NET: Fix: OutputConverter.FunctionResultContent creates OutputItemFunction…#6247
enhendrickson wants to merge 3 commits into
microsoft:mainfrom
provisions-group:fix/outputconverter-null-id-persistence

Conversation

@enhendrickson
Copy link
Copy Markdown

Summary

When a Foundry-hosted agent invokes a tool, the OutputConverter.FunctionResultContent case creates OutputItemFunctionToolCallOutput items with a null Id field. This causes the Responses API storage layer to reject the persistence request with HTTP 500, followed by a 409 on retry (confirming partial resource creation). While Azure.AI.AgentServer.Responses v1.0.0-beta.5 gracefully catches this and emits a response.failed event instead of breaking the SSE stream, the root cause remains unfixed.

Root Cause

In OutputConverter.cs (~line 450-460), the FunctionResultContent case:

case FunctionResultContent functionResult:
{
    var outputText = EncodeFunctionResultAsJsonStringPayload(functionResult.Result);
    var itemId = GenerateItemId("fc");  // WRONG: "fc" is function_call prefix, not function_call_output
    var outputItem = new OutputItemFunctionToolCallOutput(
        functionResult.CallId,
        BinaryData.FromString(outputText));  // Id property stays null (get-only)
    var outputBuilder = stream.AddOutputItem<OutputItemFunctionToolCallOutput>(itemId);
    yield return outputBuilder.EmitAdded(outputItem);
    yield return outputBuilder.EmitDone(outputItem);
    break;
}

Problem: The 2-arg constructor OutputItemFunctionToolCallOutput(callId, output) doesn't set the Id property (it's get-only). When the item is serialized for persistence (using ModelReaderWriterOptions.Json), the id field is written as null, which violates the Responses API schema and causes HTTP 500.

Why it happens: The 2-arg public constructor is for deserialization. The internal 9-arg constructor (used by the SDK's convenience method) has the proper id setup:

internal OutputItemFunctionToolCallOutput(
    string id,
    string callId,
    BinaryData output,
    ... other fields ...)

Reproduction

  1. Deploy a Foundry hosted agent with Foundry.Hosting 1.8.0-preview
  2. Register any tool via AddFoundryToolboxes() or AsOpenAIResponseTool()
  3. Invoke the agent with a prompt that triggers the tool
  4. Observe in logs:
    • Initial POST /storage/responses returns HTTP 500
    • Azure.Core auto-retry with same request-id returns HTTP 409 (resource already exists)
    • SSE stream terminates (beta.4) or emits response.failed event (beta.5)

Smoke test evidence (from this session):

17:15:13.534927401Z  POST /storage/responses ... HTTP 500
17:15:13.535XXX...Z  Azure.Core retry with request-id d08ad4aa-17aa-42e5-9ae6-03b1ebb857c3
17:15:13.536XXX...Z  HTTP 409 Conflict (resource already exists)
17:15:13.537XXX...Z  Persistence failed before terminal event for response caresp_...

Expected Behavior

Tool invocations should persist cleanly and the agent should return results to the client without storage errors.

Current Workaround (beta.5)

Upgrade to Azure.AI.AgentServer.Responses v1.0.0-beta.5, which contains ResponseOrchestrator.CreateStreamingAsync exception handling that catches persistence failures and emits response.failed with code "storage_error". The SSE stream completes gracefully instead of terminating abruptly.

<PackageReference Include="Azure.AI.AgentServer.Responses" Version="1.0.0-beta.5" />

Client-side mitigation: listen for response.failed events with code="storage_error" and display a user-friendly message.

Suggested Fix

Replace the manual 2-arg constructor usage with the SDK's convenience method, which handles id generation correctly:

case FunctionResultContent functionResult:
{
    var outputText = EncodeFunctionResultAsJsonStringPayload(functionResult.Result);
    foreach (var evt in stream.OutputItemFunctionCallOutput(
        functionResult.CallId,
        BinaryData.FromString(outputText)))
    {
        yield return evt;
    }
    break;
}

The convenience method uses IdGenerator.NewFunctionCallOutputItemId() internally, which generates proper "fco_{partitionKey}{entropy}" formatted ids.

Code Location

  • File: dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/OutputConverter.cs
  • Class: OutputConverter
  • Method: (various overloads of the iterator that yield from FunctionResultContent case)
  • Lines: ~450-460 (exact line number may vary with version)

Environment

  • Microsoft.Agents.AI.Foundry.Hosting: 1.8.0-preview.260528.1
  • Azure.AI.AgentServer.Responses: 1.0.0-beta.4 (buggy) → 1.0.0-beta.5 (graceful error)
  • Azure.AI.Projects: 2.1.0-beta.1
  • Foundry Hosted Agent: /responses protocol (streaming SSE)

Related

I have submitted a ticket for this: #6245

…ToolCallOutput with null id

- Root cause: 2-arg constructor leaves Id property null (get-only)
- Solution: Use ResponseEventStream.OutputItemFunctionCallOutput convenience method
- Impact: Prevents HTTP 500 on function_call_output persistence
- Ref: issue microsoft/agent-framework (filed separately)
Copilot AI review requested due to automatic review settings June 1, 2026 17:58
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Refactors function tool call output emission to use a new helper method OutputItemFunctionCallOutput on the stream, replacing inline construction and emission of added/done events.

Changes:

  • Replaces manual OutputItemFunctionToolCallOutput construction and AddOutputItem/EmitAdded/EmitDone calls with a single helper method that yields the events.

@moonbox3 moonbox3 added the .NET label Jun 1, 2026
@github-actions github-actions Bot changed the title Fix: OutputConverter.FunctionResultContent creates OutputItemFunction… .NET: Fix: OutputConverter.FunctionResultContent creates OutputItemFunction… Jun 1, 2026
Copy link
Copy Markdown
Contributor

@moonbox3 moonbox3 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a unit test we should be adding/updating related to this change?

…have non-null Id

Adds a unit test that verifies the OutputItemFunctionToolCallOutput emitted by
the FunctionResultContent case has a non-null, non-empty Id with the correct
'fco_' prefix.

Regression guard for issue microsoft#6245: the old code used the 2-arg constructor
(OutputItemFunctionToolCallOutput(callId, output)), which leaves the Id property
null (get-only). A null id causes the Responses API storage layer to reject the
function_call_output with HTTP 500. The fix uses the convenience method
ResponseEventStream.OutputItemFunctionCallOutput(), which calls
IdGenerator.NewFunctionCallOutputItemId() internally to produce a proper id.
@enhendrickson
Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree company="Provisions Group"

@enhendrickson
Copy link
Copy Markdown
Author

Is there a unit test we should be adding/updating related to this change?

good question, I just amended/pushed the one that was there @moonbox3

Resolves merge conflicts by accepting origin/main versions of:
- OutputConverter.cs: keeps the explanatory documentation added upstream
- OutputConverterTests.cs: uses the more comprehensive K-06e regression test from main
@enhendrickson
Copy link
Copy Markdown
Author

I also accepted someone else's changes that came in via MC - I'm guessing it was an agent coded solution to my bug, but either way, it was G2G... this PR is now ready to merge with both the fixed bug and updated test

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants