Skip to content

Commit b65f78e

Browse files
committed
Add Microsoft Agent Framework adapter
1 parent 4c0d5d6 commit b65f78e

File tree

12 files changed

+696
-6
lines changed

12 files changed

+696
-6
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<Description>.NET Microsoft Agent Framework adapter for CodexSharpSDK, providing AIAgent registration helpers.</Description>
4+
<PackageId>ManagedCode.CodexSharpSDK.Extensions.AgentFramework</PackageId>
5+
<RootNamespace>ManagedCode.CodexSharpSDK.Extensions.AgentFramework</RootNamespace>
6+
<AssemblyName>ManagedCode.CodexSharpSDK.Extensions.AgentFramework</AssemblyName>
7+
<PackageTags>codex;openai;sdk;ai;agent;cli;microsoft-agent-framework;aiagent</PackageTags>
8+
<GenerateDocumentationFile>true</GenerateDocumentationFile>
9+
<NoWarn>$(NoWarn);CS1591</NoWarn>
10+
</PropertyGroup>
11+
12+
<ItemGroup>
13+
<PackageReference Include="DotNet.ReproducibleBuilds">
14+
<PrivateAssets>all</PrivateAssets>
15+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
16+
</PackageReference>
17+
<PackageReference Include="Microsoft.Agents.AI" />
18+
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
19+
<PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="all" />
20+
</ItemGroup>
21+
22+
<ItemGroup>
23+
<ProjectReference Include="..\CodexSharpSDK.Extensions.AI\CodexSharpSDK.Extensions.AI.csproj" />
24+
</ItemGroup>
25+
</Project>
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
using ManagedCode.CodexSharpSDK.Extensions.AI;
2+
using ManagedCode.CodexSharpSDK.Extensions.AI.Extensions;
3+
using Microsoft.Agents.AI;
4+
using Microsoft.Extensions.AI;
5+
using Microsoft.Extensions.DependencyInjection;
6+
using Microsoft.Extensions.Logging;
7+
8+
namespace ManagedCode.CodexSharpSDK.Extensions.AgentFramework.Extensions;
9+
10+
public static class CodexAgentServiceCollectionExtensions
11+
{
12+
public static IServiceCollection AddCodexAIAgent(
13+
this IServiceCollection services,
14+
Action<CodexChatClientOptions>? configureChatClient = null,
15+
Action<ChatClientAgentOptions>? configureAgent = null)
16+
{
17+
ArgumentNullException.ThrowIfNull(services);
18+
19+
services.AddCodexChatClient(configureChatClient);
20+
services.AddSingleton<AIAgent>(serviceProvider => CreateAgent(serviceProvider, serviceKey: null, configureAgent));
21+
return services;
22+
}
23+
24+
public static IServiceCollection AddKeyedCodexAIAgent(
25+
this IServiceCollection services,
26+
object serviceKey,
27+
Action<CodexChatClientOptions>? configureChatClient = null,
28+
Action<ChatClientAgentOptions>? configureAgent = null)
29+
{
30+
ArgumentNullException.ThrowIfNull(services);
31+
ArgumentNullException.ThrowIfNull(serviceKey);
32+
33+
services.AddKeyedCodexChatClient(serviceKey, configureChatClient);
34+
services.AddKeyedSingleton<AIAgent>(
35+
serviceKey,
36+
(serviceProvider, key) => CreateAgent(serviceProvider, key, configureAgent));
37+
return services;
38+
}
39+
40+
private static ChatClientAgent CreateAgent(
41+
IServiceProvider serviceProvider,
42+
object? serviceKey,
43+
Action<ChatClientAgentOptions>? configureAgent)
44+
{
45+
ArgumentNullException.ThrowIfNull(serviceProvider);
46+
47+
var options = new ChatClientAgentOptions();
48+
configureAgent?.Invoke(options);
49+
50+
var loggerFactory = serviceProvider.GetService<ILoggerFactory>();
51+
var chatClient = serviceKey is null
52+
? serviceProvider.GetRequiredService<IChatClient>()
53+
: serviceProvider.GetRequiredKeyedService<IChatClient>(serviceKey);
54+
55+
return chatClient.AsAIAgent(options, loggerFactory, serviceProvider);
56+
}
57+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
using System.Runtime.CompilerServices;
2+
3+
[assembly: InternalsVisibleTo("ManagedCode.CodexSharpSDK.Tests")]
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
using ManagedCode.CodexSharpSDK.Extensions.AgentFramework.Extensions;
2+
using Microsoft.Agents.AI;
3+
using Microsoft.Extensions.AI;
4+
using Microsoft.Extensions.DependencyInjection;
5+
6+
namespace ManagedCode.CodexSharpSDK.Tests.AgentFramework;
7+
8+
public class CodexAgentServiceCollectionExtensionsTests
9+
{
10+
private const string AgentDescription = "Agent description";
11+
private const string AgentInstructions = "You are a coding assistant.";
12+
private const string AgentName = "codex-agent";
13+
private const string ConfiguredDefaultModel = "configured-default-model";
14+
private const string KeyedServiceName = "codex-agent";
15+
16+
[Test]
17+
public async Task AddCodexAIAgent_ThrowsForNullServices()
18+
{
19+
ServiceCollection? services = null;
20+
21+
var action = () => services!.AddCodexAIAgent();
22+
23+
var exception = await Assert.That(action).ThrowsException();
24+
await Assert.That(exception).IsTypeOf<ArgumentNullException>();
25+
await Assert.That(((ArgumentNullException)exception!).ParamName).IsEqualTo("services");
26+
}
27+
28+
[Test]
29+
public async Task AddCodexAIAgent_RegistersAIAgentAndChatClient()
30+
{
31+
var services = new ServiceCollection();
32+
services.AddCodexAIAgent();
33+
34+
var provider = services.BuildServiceProvider();
35+
var agent = provider.GetService<AIAgent>();
36+
var chatClient = provider.GetService<IChatClient>();
37+
var resolvedAgentChatClient = agent?.GetService(typeof(IChatClient)) as IChatClient;
38+
var metadata = resolvedAgentChatClient?.GetService(typeof(ChatClientMetadata)) as ChatClientMetadata;
39+
40+
await Assert.That(agent).IsNotNull();
41+
await Assert.That(chatClient).IsNotNull();
42+
await Assert.That(resolvedAgentChatClient).IsNotNull();
43+
await Assert.That(metadata).IsNotNull();
44+
await Assert.That(metadata!.ProviderName).IsEqualTo("CodexCLI");
45+
}
46+
47+
[Test]
48+
public async Task AddCodexAIAgent_WithConfiguration_AppliesAgentOptions()
49+
{
50+
var services = new ServiceCollection();
51+
services.AddCodexAIAgent(
52+
configureChatClient: options => options.DefaultModel = ConfiguredDefaultModel,
53+
configureAgent: options =>
54+
{
55+
options.Name = AgentName;
56+
options.Description = AgentDescription;
57+
options.ChatOptions = new ChatOptions
58+
{
59+
Instructions = AgentInstructions,
60+
};
61+
});
62+
63+
var provider = services.BuildServiceProvider();
64+
var agent = provider.GetRequiredService<AIAgent>();
65+
var chatClient = agent.GetService<IChatClient>();
66+
var metadata = chatClient?.GetService(typeof(ChatClientMetadata)) as ChatClientMetadata;
67+
var agentOptions = agent.GetService<ChatClientAgentOptions>();
68+
69+
await Assert.That(agent.Name).IsEqualTo(AgentName);
70+
await Assert.That(agent.Description).IsEqualTo(AgentDescription);
71+
await Assert.That(agentOptions).IsNotNull();
72+
await Assert.That(agentOptions!.ChatOptions).IsNotNull();
73+
await Assert.That(agentOptions.ChatOptions!.Instructions).IsEqualTo(AgentInstructions);
74+
await Assert.That(metadata).IsNotNull();
75+
await Assert.That(metadata!.DefaultModelId).IsEqualTo(ConfiguredDefaultModel);
76+
}
77+
78+
[Test]
79+
public async Task AddKeyedCodexAIAgent_RegistersKeyedAgent()
80+
{
81+
var services = new ServiceCollection();
82+
services.AddKeyedCodexAIAgent(KeyedServiceName);
83+
84+
var provider = services.BuildServiceProvider();
85+
var agent = provider.GetKeyedService<AIAgent>(KeyedServiceName);
86+
var chatClient = provider.GetKeyedService<IChatClient>(KeyedServiceName);
87+
var resolvedAgentChatClient = agent?.GetService(typeof(IChatClient)) as IChatClient;
88+
var metadata = resolvedAgentChatClient?.GetService(typeof(ChatClientMetadata)) as ChatClientMetadata;
89+
90+
await Assert.That(agent).IsNotNull();
91+
await Assert.That(chatClient).IsNotNull();
92+
await Assert.That(resolvedAgentChatClient).IsNotNull();
93+
await Assert.That(metadata).IsNotNull();
94+
await Assert.That(metadata!.ProviderName).IsEqualTo("CodexCLI");
95+
}
96+
97+
[Test]
98+
public async Task AddKeyedCodexAIAgent_ThrowsForNullServiceKey()
99+
{
100+
var services = new ServiceCollection();
101+
102+
var action = () => services.AddKeyedCodexAIAgent(serviceKey: null!);
103+
104+
var exception = await Assert.That(action).ThrowsException();
105+
await Assert.That(exception).IsTypeOf<ArgumentNullException>();
106+
await Assert.That(((ArgumentNullException)exception!).ParamName).IsEqualTo("serviceKey");
107+
}
108+
109+
[Test]
110+
public async Task AddKeyedCodexAIAgent_WithConfiguration_AppliesKeyedAgentOptions()
111+
{
112+
var services = new ServiceCollection();
113+
services.AddKeyedCodexAIAgent(
114+
KeyedServiceName,
115+
configureChatClient: options => options.DefaultModel = ConfiguredDefaultModel,
116+
configureAgent: options =>
117+
{
118+
options.Name = AgentName;
119+
options.ChatOptions = new ChatOptions
120+
{
121+
Instructions = AgentInstructions,
122+
};
123+
});
124+
125+
var provider = services.BuildServiceProvider();
126+
var agent = provider.GetRequiredKeyedService<AIAgent>(KeyedServiceName);
127+
var chatClient = agent.GetService<IChatClient>();
128+
var metadata = chatClient?.GetService(typeof(ChatClientMetadata)) as ChatClientMetadata;
129+
var agentOptions = agent.GetService<ChatClientAgentOptions>();
130+
131+
await Assert.That(agent.Name).IsEqualTo(AgentName);
132+
await Assert.That(agentOptions).IsNotNull();
133+
await Assert.That(agentOptions!.ChatOptions).IsNotNull();
134+
await Assert.That(agentOptions.ChatOptions!.Instructions).IsEqualTo(AgentInstructions);
135+
await Assert.That(metadata).IsNotNull();
136+
await Assert.That(metadata!.DefaultModelId).IsEqualTo(ConfiguredDefaultModel);
137+
}
138+
}

CodexSharpSDK.Tests/CodexSharpSDK.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
</ItemGroup>
2121

2222
<ItemGroup>
23+
<ProjectReference Include="..\CodexSharpSDK.Extensions.AgentFramework\CodexSharpSDK.Extensions.AgentFramework.csproj" />
2324
<ProjectReference Include="..\CodexSharpSDK.Extensions.AI\CodexSharpSDK.Extensions.AI.csproj" />
2425
<ProjectReference Include="..\CodexSharpSDK\CodexSharpSDK.csproj" />
2526
</ItemGroup>

Directory.Packages.props

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
</PropertyGroup>
55
<ItemGroup>
66
<PackageVersion Include="DotNet.ReproducibleBuilds" Version="2.0.2" />
7+
<PackageVersion Include="Microsoft.Agents.AI" Version="1.0.0-rc4" />
78
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="10.3.0" />
89
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.3" />
910
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.3" />
@@ -12,4 +13,4 @@
1213
<PackageVersion Include="TUnit" Version="1.18.21" />
1314
<PackageVersion Include="coverlet.collector" Version="8.0.0" />
1415
</ItemGroup>
15-
</Project>
16+
</Project>

ManagedCode.CodexSharpSDK.slnx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<Solution>
22
<Project Path="CodexSharpSDK/CodexSharpSDK.csproj" />
33
<Project Path="CodexSharpSDK.Extensions.AI/CodexSharpSDK.Extensions.AI.csproj" />
4+
<Project Path="CodexSharpSDK.Extensions.AgentFramework/CodexSharpSDK.Extensions.AgentFramework.csproj" />
45
<Project Path="CodexSharpSDK.Tests/CodexSharpSDK.Tests.csproj" />
56
</Solution>

README.md

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,83 @@ foreach (var content in response.Messages.SelectMany(m => m.Contents))
388388

389389
See [docs/Features/meai-integration.md](https://github.com/managedcode/CodexSharpSDK/blob/main/docs/Features/meai-integration.md) and [ADR 003](https://github.com/managedcode/CodexSharpSDK/blob/main/docs/ADR/003-microsoft-extensions-ai-integration.md) for full details.
390390

391+
## Microsoft Agent Framework Integration
392+
393+
An optional adapter package lets you use CodexSharpSDK with Microsoft Agent Framework `AIAgent`.
394+
395+
```bash
396+
dotnet add package ManagedCode.CodexSharpSDK.Extensions.AgentFramework
397+
```
398+
399+
### Basic usage
400+
401+
```csharp
402+
using ManagedCode.CodexSharpSDK.Extensions.AI;
403+
using Microsoft.Agents.AI;
404+
using Microsoft.Extensions.AI;
405+
406+
IChatClient chatClient = new CodexChatClient();
407+
408+
AIAgent agent = chatClient.AsAIAgent(
409+
name: "CodexAssistant",
410+
instructions: "You are a helpful coding assistant.");
411+
412+
AgentResponse response = await agent.RunAsync("Summarize the repository");
413+
Console.WriteLine(response);
414+
```
415+
416+
### DI registration
417+
418+
```csharp
419+
using ManagedCode.CodexSharpSDK.Extensions.AgentFramework.Extensions;
420+
using Microsoft.Agents.AI;
421+
using Microsoft.Extensions.AI;
422+
423+
builder.Services.AddCodexAIAgent(
424+
configureAgent: options =>
425+
{
426+
options.Name = "CodexAssistant";
427+
options.ChatOptions = new ChatOptions
428+
{
429+
Instructions = "You are a helpful coding assistant."
430+
};
431+
});
432+
433+
app.MapGet("/agent", async (AIAgent agent) =>
434+
{
435+
var response = await agent.RunAsync("Summarize the repository");
436+
return response.ToString();
437+
});
438+
```
439+
440+
### Keyed DI registration
441+
442+
```csharp
443+
using ManagedCode.CodexSharpSDK.Extensions.AgentFramework.Extensions;
444+
using Microsoft.Agents.AI;
445+
using Microsoft.Extensions.AI;
446+
using Microsoft.Extensions.DependencyInjection;
447+
448+
var services = new ServiceCollection();
449+
services.AddKeyedCodexAIAgent(
450+
"codex-main",
451+
configureAgent: options =>
452+
{
453+
options.Name = "CodexAssistant";
454+
options.ChatOptions = new ChatOptions
455+
{
456+
Instructions = "You are a helpful coding assistant."
457+
};
458+
});
459+
460+
using var provider = services.BuildServiceProvider();
461+
var keyedAgent = provider.GetRequiredKeyedService<AIAgent>("codex-main");
462+
```
463+
464+
This package builds on the existing `IChatClient` adapter, so the canonical MAF path remains `IChatClient.AsAIAgent(...)`; the new package adds a supported Codex-specific package boundary and DI convenience methods.
465+
466+
See [docs/Features/agent-framework-integration.md](https://github.com/managedcode/CodexSharpSDK/blob/main/docs/Features/agent-framework-integration.md) and [ADR 004](https://github.com/managedcode/CodexSharpSDK/blob/main/docs/ADR/004-microsoft-agent-framework-integration.md) for full details.
467+
391468
## Build and Test
392469

393470
```bash

0 commit comments

Comments
 (0)