Skip to content

Commit b298113

Browse files
alliscodeCopilot
andauthored
.NET - Fix missing id on function_call_output in Foundry Hosting (microsoft#6246)
* Fix missing id on function_call_output in Foundry Hosting The Foundry storage layer was rejecting responses with "ID cannot be null or empty (Parameter 'id')" because function_call_output items emitted by OutputConverter had no id on the wire. OutputItemFunctionToolCallOutput's public ctor only sets CallId and Output; Id is read-only and only the SDK's internal ctor populates it. OutputItemBuilder<T>.ApplyAutoStamps fills ResponseId and AgentReference but not Id, so the itemId passed to AddOutputItem<T>(itemId) was used only for event sequencing and the serialized item went out with id=null. Switch to stream.OutputItemFunctionCallOutput(callId, output), the SDK convenience method that uses the internal ctor and stamps the id. Add a regression test asserting the added/done events carry a non-empty matching Id. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * ci: free disk space and relocate NuGet cache on ubuntu runners The ubuntu-latest dotnet-build/test jobs were hitting No space left on device because the runner image only ships ~14 GB free on /. The full multi-TFM build plus the dotnet pack + console-app install-check exhausts that easily. Add a reusable composite action .github/actions/free-runner-disk-space that runs on Linux runners only and: * removes pre-installed toolchains we never use here (Android SDK, GHC/Haskell, CodeQL, PyPy, Ruby, Go, boost, vcpkg, etc.), prunes docker images, and disables swap (reclaims ~25-30 GB on /) * relocates the NuGet package cache to /mnt/nuget via NUGET_PACKAGES env, since /mnt has ~75 GB free on hosted runners Wire the action into the four ubuntu-touching jobs in dotnet-build-and-test.yml (dotnet-build, dotnet-test, dotnet-foundry-hosted-it, dotnet-test-functions). The action self-guards with runner.os == 'Linux' so the matrix legs that run on windows are unaffected. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: alliscode <25218250+alliscode@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 8091d05 commit b298113

4 files changed

Lines changed: 116 additions & 6 deletions

File tree

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
name: Free runner disk space
2+
description: |
3+
Reclaims disk space on GitHub-hosted Ubuntu runners by removing
4+
pre-installed toolchains we do not use (Android SDK, GHC/Haskell,
5+
CodeQL bundle), Docker images, and swap. Also relocates the
6+
NuGet package cache to /mnt (which has ~75 GB free vs ~14 GB
7+
on /). No-op on non-Linux runners.
8+
9+
runs:
10+
using: composite
11+
steps:
12+
- name: Free disk space (Linux only)
13+
if: runner.os == 'Linux'
14+
shell: bash
15+
run: |
16+
set -euo pipefail
17+
echo "::group::Disk usage before cleanup"
18+
df -h /
19+
echo "::endgroup::"
20+
21+
# Remove pre-installed toolchains we never use on this repo's
22+
# dotnet/python jobs. These reclaim ~25-30 GB on ubuntu-latest.
23+
sudo rm -rf \
24+
/usr/local/lib/android \
25+
/usr/share/dotnet/sdk/NuGetFallbackFolder \
26+
/opt/ghc \
27+
/usr/local/.ghcup \
28+
/opt/hostedtoolcache/CodeQL \
29+
/opt/hostedtoolcache/PyPy \
30+
/opt/hostedtoolcache/Ruby \
31+
/opt/hostedtoolcache/go \
32+
/usr/local/share/boost \
33+
/usr/local/share/powershell \
34+
/usr/local/share/chromium \
35+
/usr/local/share/vcpkg \
36+
/usr/local/lib/heroku \
37+
"${AGENT_TOOLSDIRECTORY:-/opt/hostedtoolcache}/PyPy" \
38+
"${AGENT_TOOLSDIRECTORY:-/opt/hostedtoolcache}/Ruby" \
39+
"${AGENT_TOOLSDIRECTORY:-/opt/hostedtoolcache}/go" || true
40+
41+
# Drop docker images shipped on the runner; jobs that need
42+
# docker pull what they need fresh.
43+
if command -v docker >/dev/null 2>&1; then
44+
sudo docker image prune --all --force >/dev/null 2>&1 || true
45+
fi
46+
47+
# Disable swap to free its backing file.
48+
sudo swapoff -a || true
49+
sudo rm -f /mnt/swapfile /swapfile || true
50+
51+
echo "::group::Disk usage after cleanup"
52+
df -h /
53+
echo "::endgroup::"
54+
55+
- name: Relocate NuGet package cache to /mnt (Linux only)
56+
if: runner.os == 'Linux'
57+
shell: bash
58+
run: |
59+
set -euo pipefail
60+
sudo mkdir -p /mnt/nuget
61+
sudo chown -R "$USER":"$USER" /mnt/nuget
62+
echo "NUGET_PACKAGES=/mnt/nuget" >> "$GITHUB_ENV"
63+
echo "Relocated NuGet package cache to /mnt/nuget"
64+
df -h /mnt || true

.github/workflows/dotnet-build-and-test.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,9 @@ jobs:
121121
python
122122
declarative-agents
123123
124+
- name: Free runner disk space
125+
uses: ./.github/actions/free-runner-disk-space
126+
124127
- name: Setup dotnet
125128
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
126129
with:
@@ -191,6 +194,9 @@ jobs:
191194
python
192195
declarative-agents
193196
197+
- name: Free runner disk space
198+
uses: ./.github/actions/free-runner-disk-space
199+
194200
# Start Cosmos DB Emulator for all integration tests and only for unit tests when CosmosDB changes happened)
195201
- name: Start Azure Cosmos DB Emulator
196202
if: ${{ runner.os == 'Windows' && (needs.paths-filter.outputs.cosmosDbChanges == 'true' || (github.event_name != 'pull_request' && matrix.integration-tests)) }}
@@ -365,6 +371,9 @@ jobs:
365371
dotnet
366372
python
367373
374+
- name: Free runner disk space
375+
uses: ./.github/actions/free-runner-disk-space
376+
368377
- name: Setup dotnet
369378
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
370379
with:
@@ -452,6 +461,9 @@ jobs:
452461
python
453462
declarative-agents
454463
464+
- name: Free runner disk space
465+
uses: ./.github/actions/free-runner-disk-space
466+
455467
- name: Setup dotnet
456468
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
457469
with:

dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/OutputConverter.cs

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -281,14 +281,19 @@ public static async IAsyncEnumerable<ResponseStreamEvent> ConvertUpdatesToEvents
281281

282282
var outputText = EncodeFunctionResultAsJsonStringPayload(functionResult.Result);
283283

284-
var itemId = GenerateItemId("fc");
285-
var outputItem = new OutputItemFunctionToolCallOutput(
284+
// Use the SDK's convenience method so the OutputItemFunctionToolCallOutput
285+
// is constructed with a populated Id. The public OutputItemFunctionToolCallOutput
286+
// ctor only sets CallId/Output (Id is read-only), and AddOutputItem<T>+EmitAdded
287+
// does not auto-stamp Id — only ResponseId/AgentReference. Without this, the
288+
// serialized item arrives at the Foundry storage layer with id=null and is
289+
// rejected with "ID cannot be null or empty (Parameter 'id')".
290+
foreach (var evt in stream.OutputItemFunctionCallOutput(
286291
functionResult.CallId,
287-
BinaryData.FromString(outputText));
292+
BinaryData.FromString(outputText)))
293+
{
294+
yield return evt;
295+
}
288296

289-
var outputBuilder = stream.AddOutputItem<OutputItemFunctionToolCallOutput>(itemId);
290-
yield return outputBuilder.EmitAdded(outputItem);
291-
yield return outputBuilder.EmitDone(outputItem);
292297
break;
293298
}
294299

dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/OutputConverterTests.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -704,6 +704,35 @@ public async Task ConvertUpdatesToEventsAsync_FunctionResultJsonElementArrayPayl
704704
Assert.Equal("[{\"id\":1}]", inner);
705705
}
706706

707+
// K-06e: Regression — the OutputItemFunctionToolCallOutput must have a populated Id
708+
// and a matching wire id on the added/done events. The Foundry storage layer extracts
709+
// a partition id from this field and throws "ID cannot be null or empty (Parameter 'id')"
710+
// when it is missing.
711+
[Fact]
712+
public async Task ConvertUpdatesToEventsAsync_FunctionResult_OutputItemHasIdAsync()
713+
{
714+
var (stream, _) = CreateTestStream();
715+
var update = new AgentResponseUpdate { Contents = [new FunctionResultContent("call_1", "sunny")] };
716+
717+
var events = new List<ResponseStreamEvent>();
718+
await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(new[] { update }), stream))
719+
{
720+
events.Add(evt);
721+
}
722+
723+
var added = Assert.Single(events.OfType<ResponseOutputItemAddedEvent>());
724+
var done = Assert.Single(events.OfType<ResponseOutputItemDoneEvent>());
725+
726+
var addedOutput = Assert.IsType<OutputItemFunctionToolCallOutput>(added.Item);
727+
var doneOutput = Assert.IsType<OutputItemFunctionToolCallOutput>(done.Item);
728+
729+
Assert.False(string.IsNullOrEmpty(addedOutput.Id));
730+
Assert.False(string.IsNullOrEmpty(doneOutput.Id));
731+
Assert.Equal(addedOutput.Id, doneOutput.Id);
732+
Assert.Equal("call_1", addedOutput.CallId);
733+
Assert.Equal("call_1", doneOutput.CallId);
734+
}
735+
707736
// L-01
708737
[Fact]
709738
public async Task ConvertUpdatesToEventsAsync_ExecutorInvokedEvent_EmitsWorkflowActionItemAsync()

0 commit comments

Comments
 (0)