Skip to content

Commit 3464dc2

Browse files
committed
Fix lifecycle and image cleanup leaks
1 parent 076b2f1 commit 3464dc2

19 files changed

Lines changed: 264 additions & 59 deletions

.github/workflows/release.yml

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ jobs:
1616

1717
outputs:
1818
version: ${{ steps.version.outputs.version }}
19-
agent_framework_version: ${{ steps.version.outputs.agent_framework_version }}
2019

2120
steps:
2221
- name: Checkout
@@ -48,24 +47,16 @@ jobs:
4847
echo "Failed to resolve package version from Directory.Build.props"
4948
exit 1
5049
fi
51-
AGENT_FRAMEWORK_VERSION="$VERSION-rc4"
5250
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
53-
echo "agent_framework_version=$AGENT_FRAMEWORK_VERSION" >> "$GITHUB_OUTPUT"
5451
echo "Version from Directory.Build.props: $VERSION"
55-
echo "Agent Framework package version: $AGENT_FRAMEWORK_VERSION"
5652
5753
- name: Validate semantic version
5854
run: |
5955
VERSION="${{ steps.version.outputs.version }}"
60-
AGENT_FRAMEWORK_VERSION="${{ steps.version.outputs.agent_framework_version }}"
6156
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$ ]]; then
6257
echo "Version '$VERSION' is not a valid semantic version."
6358
exit 1
6459
fi
65-
if [[ ! "$AGENT_FRAMEWORK_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$ ]]; then
66-
echo "Agent Framework version '$AGENT_FRAMEWORK_VERSION' is not a valid semantic version."
67-
exit 1
68-
fi
6960
7061
- name: Restore dependencies
7162
run: dotnet restore ManagedCode.GeminiSharpSDK.slnx
@@ -87,11 +78,10 @@ jobs:
8778
- name: Validate packed artifacts
8879
run: |
8980
VERSION="${{ steps.version.outputs.version }}"
90-
AGENT_FRAMEWORK_VERSION="${{ steps.version.outputs.agent_framework_version }}"
9181
find ./artifacts -maxdepth 1 -name '*.nupkg' | sort
9282
test -f "./artifacts/ManagedCode.GeminiSharpSDK.$VERSION.nupkg"
9383
test -f "./artifacts/ManagedCode.GeminiSharpSDK.Extensions.AI.$VERSION.nupkg"
94-
test -f "./artifacts/ManagedCode.GeminiSharpSDK.Extensions.AgentFramework.$AGENT_FRAMEWORK_VERSION.nupkg"
84+
test -f "./artifacts/ManagedCode.GeminiSharpSDK.Extensions.AgentFramework.$VERSION.nupkg"
9585
9686
- name: Upload artifacts
9787
uses: actions/upload-artifact@v7.0.0
@@ -109,7 +99,6 @@ jobs:
10999
outputs:
110100
published: ${{ steps.publish.outputs.published }}
111101
version: ${{ needs.build.outputs.version }}
112-
agent_framework_version: ${{ needs.build.outputs.agent_framework_version }}
113102

114103
steps:
115104
- name: Checkout
@@ -217,7 +206,6 @@ jobs:
217206
id: release_notes
218207
run: |
219208
VERSION="${{ needs.publish-nuget.outputs.version }}"
220-
AGENT_FRAMEWORK_VERSION="${{ needs.publish-nuget.outputs.agent_framework_version }}"
221209
CURRENT_TAG="v$VERSION"
222210
PREVIOUS_TAG="${{ steps.prev_tag.outputs.previous_tag }}"
223211

Directory.Packages.props

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@
44
</PropertyGroup>
55
<ItemGroup>
66
<PackageVersion Include="DotNet.ReproducibleBuilds" Version="2.0.2" />
7-
<PackageVersion Include="Microsoft.Agents.AI" Version="1.0.0-rc4" />
8-
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="10.3.0" />
9-
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.3" />
10-
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.3" />
11-
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.3" />
12-
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="10.0.103" />
13-
<PackageVersion Include="TUnit" Version="1.18.21" />
14-
<PackageVersion Include="coverlet.collector" Version="8.0.0" />
7+
<PackageVersion Include="Microsoft.Agents.AI" Version="1.0.0" />
8+
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="10.4.1" />
9+
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.5" />
10+
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.5" />
11+
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.5" />
12+
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="10.0.201" />
13+
<PackageVersion Include="TUnit" Version="1.30.0" />
14+
<PackageVersion Include="coverlet.collector" Version="8.0.1" />
1515
</ItemGroup>
1616
</Project>

GeminiSharpSDK.Extensions.AI/Extensions/GeminiServiceCollectionExtensions.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ public static IServiceCollection AddGeminiChatClient(
1313

1414
var options = new GeminiChatClientOptions();
1515
configure?.Invoke(options);
16-
services.AddSingleton<IChatClient>(new GeminiChatClient(options));
16+
services.AddSingleton<IChatClient>(_ => new GeminiChatClient(options));
1717
return services;
1818
}
1919

@@ -27,7 +27,7 @@ public static IServiceCollection AddKeyedGeminiChatClient(
2727

2828
var options = new GeminiChatClientOptions();
2929
configure?.Invoke(options);
30-
services.AddKeyedSingleton<IChatClient>(serviceKey, new GeminiChatClient(options));
30+
services.AddKeyedSingleton<IChatClient>(serviceKey, (_, _) => new GeminiChatClient(options));
3131
return services;
3232
}
3333
}

GeminiSharpSDK.Extensions.AI/Internal/StreamingEventMapper.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ internal static async IAsyncEnumerable<ChatResponseUpdate> ToUpdates(
154154
InputTokenCount = tc.Usage.InputTokens,
155155
OutputTokenCount = tc.Usage.OutputTokens,
156156
TotalTokenCount = tc.Usage.InputTokens + tc.Usage.OutputTokens,
157+
CachedInputTokenCount = tc.Usage.CachedInputTokens > 0 ? tc.Usage.CachedInputTokens : null,
157158
}),
158159
],
159160
};
@@ -172,6 +173,7 @@ internal static async IAsyncEnumerable<ChatResponseUpdate> ToUpdates(
172173
InputTokenCount = resultEvent.Usage.InputTokens,
173174
OutputTokenCount = resultEvent.Usage.OutputTokens,
174175
TotalTokenCount = resultEvent.Usage.InputTokens + resultEvent.Usage.OutputTokens,
176+
CachedInputTokenCount = resultEvent.Usage.CachedInputTokens > 0 ? resultEvent.Usage.CachedInputTokens : null,
175177
}),
176178
],
177179
};

GeminiSharpSDK.Tests/MEAI/StreamingEventMapperTests.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,28 @@ public async Task ToUpdates_FullSequence_MapsCurrentEvents()
5353
await Assert.That(updates.Count).IsGreaterThanOrEqualTo(4);
5454
}
5555

56+
[Test]
57+
public async Task ToUpdates_ResultUsage_MapsCachedInputTokens()
58+
{
59+
var events = ToAsyncEnumerable(new ResultEvent("success", new Usage(10, 4, 5), null));
60+
61+
var updates = await CollectUpdates(events);
62+
var usageContent = updates[0].Contents.OfType<UsageContent>().Single();
63+
64+
await Assert.That(usageContent.Details.CachedInputTokenCount).IsEqualTo(4);
65+
}
66+
67+
[Test]
68+
public async Task ToUpdates_TurnCompletedUsage_MapsCachedInputTokens()
69+
{
70+
var events = ToAsyncEnumerable(new TurnCompletedEvent(new Usage(12, 6, 3)));
71+
72+
var updates = await CollectUpdates(events);
73+
var usageContent = updates[0].Contents.OfType<UsageContent>().Single();
74+
75+
await Assert.That(usageContent.Details.CachedInputTokenCount).IsEqualTo(6);
76+
}
77+
5678
private static async IAsyncEnumerable<ThreadEvent> ToAsyncEnumerable(params ThreadEvent[] events)
5779
{
5880
foreach (var evt in events)

GeminiSharpSDK.Tests/Unit/GeminiCliLocatorTests.cs

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,62 @@ public async Task TryResolvePathExecutable_Unix_ResolvesGeminiBinary()
9393
}
9494
}
9595

96+
[Test]
97+
public async Task TryResolveNpmInstalledBinary_ResolvesPrimaryVendoredBinary()
98+
{
99+
var targetTriple = GeminiCliLocator.GetCurrentTargetTriple();
100+
if (targetTriple is null)
101+
{
102+
return;
103+
}
104+
105+
var sandboxDirectory = CreateSandboxDirectory();
106+
107+
try
108+
{
109+
var binaryPath = CreateVendoredBinaryPath(sandboxDirectory, targetTriple, nested: false);
110+
Directory.CreateDirectory(Path.GetDirectoryName(binaryPath)!);
111+
await File.WriteAllTextAsync(binaryPath, "binary");
112+
113+
var resolved = GeminiCliLocator.TryResolveNpmInstalledBinary([sandboxDirectory], targetTriple, OperatingSystem.IsWindows(), out var executablePath);
114+
115+
await Assert.That(resolved).IsTrue();
116+
await Assert.That(executablePath).IsEqualTo(binaryPath);
117+
}
118+
finally
119+
{
120+
Directory.Delete(sandboxDirectory, recursive: true);
121+
}
122+
}
123+
124+
[Test]
125+
public async Task TryResolveNpmInstalledBinary_ResolvesNestedVendoredBinary()
126+
{
127+
var targetTriple = GeminiCliLocator.GetCurrentTargetTriple();
128+
if (targetTriple is null)
129+
{
130+
return;
131+
}
132+
133+
var sandboxDirectory = CreateSandboxDirectory();
134+
135+
try
136+
{
137+
var binaryPath = CreateVendoredBinaryPath(sandboxDirectory, targetTriple, nested: true);
138+
Directory.CreateDirectory(Path.GetDirectoryName(binaryPath)!);
139+
await File.WriteAllTextAsync(binaryPath, "binary");
140+
141+
var resolved = GeminiCliLocator.TryResolveNpmInstalledBinary([sandboxDirectory], targetTriple, OperatingSystem.IsWindows(), out var executablePath);
142+
143+
await Assert.That(resolved).IsTrue();
144+
await Assert.That(executablePath).IsEqualTo(binaryPath);
145+
}
146+
finally
147+
{
148+
Directory.Delete(sandboxDirectory, recursive: true);
149+
}
150+
}
151+
96152
private static string CreateSandboxDirectory()
97153
{
98154
var sandboxDirectory = Path.Combine(
@@ -103,4 +159,48 @@ private static string CreateSandboxDirectory()
103159
Directory.CreateDirectory(sandboxDirectory);
104160
return sandboxDirectory;
105161
}
162+
163+
private static string CreateVendoredBinaryPath(string sandboxDirectory, string targetTriple, bool nested)
164+
{
165+
var packageDirectory = GetPackageDirectory(targetTriple);
166+
var executableName = OperatingSystem.IsWindows()
167+
? GeminiCliLocator.GeminiWindowsExecutableName
168+
: GeminiCliLocator.GeminiExecutableName;
169+
170+
var segments = new List<string>
171+
{
172+
sandboxDirectory,
173+
"node_modules",
174+
"@google",
175+
};
176+
177+
if (nested)
178+
{
179+
segments.Add("gemini");
180+
segments.Add("node_modules");
181+
segments.Add("@google");
182+
}
183+
184+
segments.Add(packageDirectory);
185+
segments.Add("vendor");
186+
segments.Add(targetTriple);
187+
segments.Add("gemini");
188+
segments.Add(executableName);
189+
190+
return Path.Combine([.. segments]);
191+
}
192+
193+
private static string GetPackageDirectory(string targetTriple)
194+
{
195+
return targetTriple switch
196+
{
197+
"x86_64-unknown-linux-musl" => "gemini-cli-linux-x64",
198+
"aarch64-unknown-linux-musl" => "gemini-cli-linux-arm64",
199+
"x86_64-apple-darwin" => "gemini-cli-darwin-x64",
200+
"aarch64-apple-darwin" => "gemini-cli-darwin-arm64",
201+
"x86_64-pc-windows-msvc" => "gemini-cli-win32-x64",
202+
"aarch64-pc-windows-msvc" => "gemini-cli-win32-arm64",
203+
_ => throw new InvalidOperationException($"Unsupported test target triple: {targetTriple}"),
204+
};
205+
}
106206
}

GeminiSharpSDK.Tests/Unit/GeminiModelsTests.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,6 @@ private static string ResolveBundledModelSlugsFilePath()
9090
{
9191
Path.Combine(repositoryRoot, "submodules", "google-gemini-cli", "packages", "core", "src", "config", BundledModelsTsFileName),
9292
Path.Combine(repositoryRoot, "submodules", "google-gemini-cli", "gemini-rs", "core", BundledModelsFileName),
93-
Path.Combine(repositoryRoot, "submodules", "google-gemini-cli", "codex-rs", "core", BundledModelsFileName),
9493
};
9594

9695
foreach (var candidate in candidates)

GeminiSharpSDK.Tests/Unit/StructuredOutputSchemaTests.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,21 @@ public async Task Object_ThrowsForUnsupportedPropertySelector()
6363
var exception = await Assert.That(action).ThrowsException();
6464
await Assert.That(exception).IsTypeOf<ArgumentException>();
6565
}
66+
67+
[Test]
68+
public async Task Object_RequiredPropertyNames_PreserveControlCharacters()
69+
{
70+
const string propertyName = "status\nwith\ttabs\u0001";
71+
72+
var schema = StructuredOutputSchema.Map(
73+
new Dictionary<string, StructuredOutputSchema>
74+
{
75+
[propertyName] = StructuredOutputSchema.PlainText(),
76+
},
77+
required: [propertyName]);
78+
79+
var json = schema.ToJsonObject();
80+
81+
await Assert.That(json["required"]![0]!.GetValue<string>()).IsEqualTo(propertyName);
82+
}
6683
}

GeminiSharpSDK.Tests/Unit/ThreadEventParserTests.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,25 @@ public async Task Parse_ParsesResultUsageFromCurrentStatsShape()
4444
await Assert.That(parsed.Usage.OutputTokens).IsEqualTo(5);
4545
}
4646

47+
[Test]
48+
public async Task Parse_ParsesResultUsageFromCachedInputTokensStatsShape()
49+
{
50+
var parsed = (ResultEvent)ThreadEventParser.Parse(
51+
"{\"type\":\"result\",\"status\":\"success\",\"stats\":{\"input_tokens\":10,\"cached_input_tokens\":7,\"output_tokens\":5}}");
52+
53+
await Assert.That(parsed.Usage).IsNotNull();
54+
await Assert.That(parsed.Usage!.CachedInputTokens).IsEqualTo(7);
55+
}
56+
57+
[Test]
58+
public async Task Parse_ParsesTurnCompletedUsageFromLegacyCachedShape()
59+
{
60+
var parsed = (TurnCompletedEvent)ThreadEventParser.Parse(
61+
"{\"type\":\"turn.completed\",\"usage\":{\"input_tokens\":10,\"cached\":2,\"output_tokens\":5}}");
62+
63+
await Assert.That(parsed.Usage.CachedInputTokens).IsEqualTo(2);
64+
}
65+
4766
[Test]
4867
public async Task Parse_ParsesToolResultError()
4968
{

GeminiSharpSDK/GeminiSharpSDK.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
3-
<Description>.NET SDK for the OpenAI Gemini CLI with streaming thread events.</Description>
3+
<Description>.NET SDK for the Google Gemini CLI with streaming thread events.</Description>
44
<PackageId>ManagedCode.GeminiSharpSDK</PackageId>
55
<RootNamespace>ManagedCode.GeminiSharpSDK</RootNamespace>
66
<AssemblyName>ManagedCode.GeminiSharpSDK</AssemblyName>

0 commit comments

Comments
 (0)