WickedAILabs.AgentFramework
1.2.0
See the version list below for details.
dotnet add package WickedAILabs.AgentFramework --version 1.2.0
NuGet\Install-Package WickedAILabs.AgentFramework -Version 1.2.0
<PackageReference Include="WickedAILabs.AgentFramework" Version="1.2.0" />
<PackageVersion Include="WickedAILabs.AgentFramework" Version="1.2.0" />
<PackageReference Include="WickedAILabs.AgentFramework" />
paket add WickedAILabs.AgentFramework --version 1.2.0
#r "nuget: WickedAILabs.AgentFramework, 1.2.0"
#:package WickedAILabs.AgentFramework@1.2.0
#addin nuget:?package=WickedAILabs.AgentFramework&version=1.2.0
#tool nuget:?package=WickedAILabs.AgentFramework&version=1.2.0
WickedAILabs.AgentFramework
A .NET 10 scaffolding framework for building agent applications on Microsoft Agent Framework (MAF) 1.1. Consuming agent projects reference the framework directly via project reference — the framework owns all infrastructure (DI wiring, provider selection, runner dispatch, tool discovery, middleware, sessions, telemetry) so consuming projects contain only domain logic.
Status: 1.0 (April 2026)
Install
Stable releases ship to NuGet.org (no authentication required). Prereleases and day-of-tag drops ship to GitHub Packages under the WickedAILabs org (auth required).
From NuGet.org (recommended for stable releases)
<PackageReference Include="WickedAILabs.AgentFramework" Version="1.2.0" />
Target net10.0 and wire DI as shown in the Quickstart below. No nuget.config needed — nuget.org is the default feed.
From GitHub Packages (for prereleases)
Prereleases (e.g. 1.2.0-beta.1) are not pushed to NuGet.org. To consume them, authenticate to the WickedAILabs GitHub Packages feed:
Create a GitHub Personal Access Token with the
read:packagesscope (classic PAT) or equivalent fine-grained permission. Export it asGITHUB_USER+GITHUB_TOKENin the shell runningdotnet restore.Add a
nuget.configto your consuming repo root (alongside the.sln/.slnx):<?xml version="1.0" encoding="utf-8"?> <configuration> <packageSources> <clear /> <add key="nuget.org" value="https://api.nuget.org/v3/index.json" /> <add key="wickedailabs" value="https://nuget.pkg.github.com/WickedAILabs/index.json" /> </packageSources> <packageSourceCredentials> <wickedailabs> <add key="Username" value="%GITHUB_USER%" /> <add key="ClearTextPassword" value="%GITHUB_TOKEN%" /> </wickedailabs> </packageSourceCredentials> </configuration>Reference the prerelease version explicitly:
<PackageReference Include="WickedAILabs.AgentFramework" Version="1.2.0-beta.1" />
GitHub Packages requires authentication even for public repos — that's a GitHub Packages behaviour, not a framework misconfiguration. If your consuming project itself runs in GitHub Actions, use the ambient
GITHUB_TOKEN(withpackages: readpermission) instead of a PAT.
Design goal
An agent project file should never import MAF types, never call AddHttpClient / Configure<T> / IOptions, and never reference framework internals. Attributes + convention scanning do all the wiring. See spec/spec.md for the full contract.
Quickstart
1. Configure the provider
appsettings.json:
{
"AgentFramework": {
"Provider": {
"Type": "openai",
"DeploymentName": "gpt-4o-mini"
},
"Agents": {
"MyAgent": {
"Runner": "simple",
"Instructions": "You are a helpful assistant.",
"Tools": [ "Echo" ]
}
}
}
}
Set the API key via environment variable:
export AgentFramework__Provider__ApiKey=sk-...
2. Write an agent
[Agent]
[AgentInstructions("You are a helpful assistant.")]
[UsesTools("Echo")]
public sealed class MyAgent : IAgent
{
[Inject] IEchoService Echo { get; set; } = null!;
public Task<string> RunAsync(string message, AgentContext ctx, CancellationToken ct)
=> AgentAI.AskAsync(this, message, ct);
}
3. Write a tool
public sealed class EchoTools
{
[AgentTool("Echoes the input verbatim.")]
public string Echo([Description("Text to echo.")] string text) => text;
}
4. Write a service
[Service(As = typeof(IEchoService))]
public sealed class EchoService : IEchoService { /* ... */ }
5. Wire DI + call the agent
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddAgentFramework(builder.Configuration);
using var host = builder.Build();
var agents = host.Services.GetRequiredService<IAgentClient>();
var result = await agents.RunAsync("my-agent", "hello");
Console.WriteLine(result.Success ? result.Response : $"FAIL: {result.ErrorCode}");
Solution layout
WickedAILabs.AgentFramework/
├── src/
│ ├── AgentFramework.Framework/ ← scaffolding (Abstractions/ + Internal/)
│ ├── AgentFramework.Testing/ ← AgentTestBase<T> + FakeKernel
│ └── Shared/AgentFramework.SharedTools/ ← cross-project tool library
├── Projects/
│ └── Smoke.Agents/ ← smoke-test consumer
├── tests/
│ └── AgentFramework.Framework.Tests/ ← xUnit + FluentAssertions (36 tests)
├── spec/
│ └── spec.md ← authoritative design
└── CHANGELOG.md
Runners
| Runner | When | Behaviour |
|---|---|---|
simple |
default | Single AIAgent.RunAsync with resolved tools |
hitl |
any approval-required tool | Approval gate via IApprovalHandler before tool execution |
workflow |
ordered executor graph | WorkflowBuilder + InProcessExecution.RunStreamingAsync |
The spec §7 capability matrix determines which providers support HITL — enforced at startup.
Orchestrator pattern (multi-agent routing)
Multi-agent handoff is not a first-class runner, but the orchestrator pattern composes cleanly from existing primitives: a tool class injects IAgentClient, exposes one [AgentTool] per specialist, and the orchestrator agent's LLM picks which to invoke based on tool descriptions. The framework's scoped tool registration and singleton IAgentClient are all that's needed — no framework changes, no new attributes.
public sealed class RoutingTools
{
private readonly IAgentClient _agents;
public RoutingTools(IAgentClient agents) => _agents = agents;
[AgentTool("Ask the weather specialist for forecast / climate questions.")]
public async Task<string> AskWeather(
[Description("The weather question.")] string question,
CancellationToken ct)
{
var r = await _agents.RunAsync("weather-advisor", question, ct: ct);
return r.Success ? r.Response : $"weather-advisor failed: {r.ErrorCode}";
}
[AgentTool("Ask the finance specialist for market / stock questions.")]
public async Task<string> AskFinance(
[Description("The finance question.")] string question,
CancellationToken ct)
{
var r = await _agents.RunAsync("finance-advisor", question, ct: ct);
return r.Success ? r.Response : $"finance-advisor failed: {r.ErrorCode}";
}
}
[Agent]
[AgentInstructions("You are a router. Delegate each request to the best specialist via the routing tools. Never answer from your own knowledge.")]
[UsesTools("AskWeather", "AskFinance")]
public sealed class OrchestratorAgent : IAgent
{
public Task<string> RunAsync(string message, AgentContext ctx, CancellationToken ct)
=> AgentAI.AskAsync(this, message, ct);
}
A working example lives at Projects/Smoke.Agents/Agents/OrchestratorAgent.cs + Projects/Smoke.Agents/Tools/RoutingTools.cs — it routes to simple-echo and hitl-calculator.
When to use vs. alternatives
| Pattern | Best for |
|---|---|
| Orchestrator-as-agent (above) | Open-ended input where the LLM should pick the specialist |
workflow runner |
Deterministic sequence of specialists (no LLM routing overhead) |
| Single agent with all tools | When specialists are really just tools, not independent agents |
Gotchas
- Recursion: every
_agents.RunAsync(...)re-enters the full middleware pipeline. Two orchestrators routing to each other will loop forever — don't register cycles. A depth-tracking middleware is a reasonable add if you expect complex graphs. - Session forwarding: the nested call gets a fresh
AgentContextunless you passconfigure: b => b.WithSessionId(ctx.SessionId)to share history across orchestrator + specialists. - Approval bubbling: if a routed specialist uses
hitl, approval prompts happen inside the nested call. The orchestrator's LLM sees only the final tool result — usually what you want. - Telemetry: each specialist invocation emits its own
agent.run:{name}span, nested under the orchestrator's span in OTel traces.
Providers
Supported AgentFramework:Provider:Type values:
| Type | Package | Status |
|---|---|---|
foundry |
Microsoft.Agents.AI.AzureAI 1.0.0-rc5 |
Prerelease |
azure-openai |
Microsoft.Agents.AI.OpenAI 1.1.0 |
Stable |
openai |
Microsoft.Agents.AI.OpenAI 1.1.0 |
Stable |
anthropic |
Microsoft.Agents.AI.Anthropic 1.1.0-rc1 |
Prerelease — stub |
ollama |
(not released) | Not available |
github-copilot |
(not released) | Not available |
copilot-studio |
Microsoft.Agents.AI.CopilotStudio preview |
Stub |
custom |
Consumer-registered IChatClient |
Stable |
For azure-openai with token auth (no API key), set:
"AgentFramework:Provider:UseDefaultAzureCredential": true
— without this explicit opt-in, a missing API key fails fast rather than silently falling back to developer-workstation credentials.
Telemetry
The framework emits structured logs, distributed traces, and metrics via built-in .NET abstractions. Wire OpenTelemetry:
using AgentFramework.Abstractions;
builder.Services.AddOpenTelemetry()
.WithTracing(t => t.AddSource(AgentFrameworkTelemetry.ActivitySourceName))
.WithMetrics(m => m.AddMeter(AgentFrameworkTelemetry.MeterName));
Metrics
| Instrument | Type | Tags |
|---|---|---|
agentframework.requests |
counter | name, runner, success |
agentframework.request.duration |
histogram | name, runner, success |
agentframework.approvals |
counter | tool, decision |
agentframework.session.loads |
counter | hit (bool) |
agentframework.tools.invocations |
counter | tool |
Traces
Single ActivitySource "AgentFramework" emits one span per request: agent.run:{AgentName} with tags agent.name, agent.user_id (SHA-256 fingerprint), agent.runner, agent.success, agent.error_code.
Logs
Structured ILogger events (all via LoggerMessage source-gen) with event IDs 1001–3201. UserIDs logged as fingerprints at Information; raw values gated on Debug. Error messages scrubbed through LogScrubber.Scrub to strip credential-shaped substrings.
Testing
public class MyAgentTests : AgentTestBase<MyAgent>
{
[Fact]
public async Task Returns_expected_response()
{
Kernel.WillReturn("fake response");
var result = await Client.RunAsync("my-agent", "input");
result.Success.Should().BeTrue();
}
}
AgentTestBase<TAgent> wires DI with a fake IChatClient backed by FakeKernel.WillReturn(...). No MAF, no HTTP, no provider config needed.
Security posture
- All LLM-generated tool arguments are untrusted.
ApprovalRequest.SafeSummary()strips control chars + truncates before display. - Credential-shaped substrings in error messages are redacted via
LogScrubber.Scrub. agent.user_idOTel tag and Info-level log field are SHA-256 fingerprints, not raw IDs.- Session storage key binds
(TenantId, UserId, SessionId)via per-component hashing — separator-injection resistant. - Approval-required tools cannot appear under a non-HITL runner (startup fail).
- Framework contract types (
IChatClient,IAgent,IApprovalHandler, etc.) cannot be registered via[Service(As=...)]scanning. - Assembly scanning executes type initializers — only pass trusted first-party assemblies. See XML docs on
AddAgentFramework.
Build + test
dotnet build
dotnet test
dotnet run --project Projects/Smoke.Agents
Target framework net10.0. Central package management via Directory.Packages.props.
Release process (maintainers)
Cutting a new package release is tag-driven — no manual version bumps in csproj files.
Ensure
mainis green and the work you want to release is merged.Add a new
## [X.Y.Z] — YYYY-MM-DDentry toCHANGELOG.mdsummarising the changes. Commit onmain(via PR).Tag the commit that should be released and push the tag:
git checkout main && git pull git tag v1.2.0 # annotated or lightweight — MinVer reads both git push origin v1.2.0The
Releaseworkflow (.github/workflows/release.yml) fires on the tag push. It will:- Check out with
fetch-depth: 0(MinVer requires full git history). dotnet restore→dotnet build -c Release→dotnet packthe framework project only.- Push the
.nupkg+.snupkgto GitHub Packages using the ambientGITHUB_TOKEN(no secrets configuration required). - If the tag is a stable release (no
-suffix), also push to NuGet.org using theNUGET_API_KEYsecret. Prerelease tags (e.g.v1.2.0-beta.1) stay private on GitHub Packages. - Upload the artifacts to the workflow run for auditing.
- Check out with
Verify at:
- GitHub Packages:
https://github.com/orgs/WickedAILabs/packages - NuGet.org (stable only):
https://www.nuget.org/packages/WickedAILabs.AgentFramework(direct URL works immediately; search indexing can take up to an hour)
- GitHub Packages:
One-time NuGet.org setup
The NUGET_API_KEY secret must exist before the first stable tag is pushed to publish publicly.
- Sign in to https://www.nuget.org (GitHub or Microsoft SSO).
- Create an API key at https://www.nuget.org/account/apikeys:
- Key name:
github-actions-wickedailabs-agentframework - Select scopes: Push new packages and versions
- Select packages: Glob
WickedAILabs.*(narrow enough to limit blast radius, broad enough to cover future packages). - Expires: 365 days. Set a calendar reminder to rotate.
- Key name:
- In this repo: Settings → Secrets and variables → Actions → New repository secret:
- Name:
NUGET_API_KEY - Value: the key from step 2
- Name:
- (Optional, one-time) Reserve the
WickedAILabs.*ID prefix on NuGet.org: https://learn.microsoft.com/en-us/nuget/nuget-org/id-prefix-reservation — prevents typo-squatting and adds a verified-owner badge. Takes a few business days.
Versioning rules (MinVer)
- Tag
vX.Y.Zexactly on a commit → package versionX.Y.Z(stable → both feeds). - Tag
vX.Y.Z-beta.N→ package versionX.Y.Z-beta.N(prerelease → GitHub Packages only). Ncommits after the latest tag, without a newer tag → package versionX.Y.(Z+1)-alpha.0.N(dev prerelease; patch bumps by default).- Consumers pinning a stable version (
Version="1.2.0") will not pick up prereleases unless they opt in (Version="1.2.0-*"or similar).
Retrying a failed release
If the push step fails (e.g. transient feed error), re-run the workflow via Actions → Release → Run workflow (workflow_dispatch) against the same tag. --skip-duplicate on dotnet nuget push means re-runs on an already-published version are no-ops, not failures.
Reverting a release
- GitHub Packages: allows deleting specific versions via the org's Packages page. Fine for prereleases and mistakes.
- NuGet.org: immutable — versions cannot be deleted, only unlisted (hidden from search, still restorable by exact version). Double-check metadata and dependencies before pushing a stable tag.
In either case: fix the underlying issue on main, add a new CHANGELOG entry, and cut a new patch tag (e.g. v1.2.1). Never re-use a deleted/unlisted version number — consumer caches will serve stale content.
Spec
All architectural rules are drawn from spec/spec.md (v1.0). Any design change belongs in the spec first.
License
GPL-3.0-only. Note that GPL-3.0 is a "viral" license — any project that redistributes a derived work must also be GPL-3.0. This may affect commercial adoption; consumers should review the license before depending on the package.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Azure.AI.OpenAI (>= 2.9.0-beta.1)
- Azure.Identity (>= 1.21.0)
- Microsoft.Agents.AI (>= 1.1.0)
- Microsoft.Agents.AI.Abstractions (>= 1.1.0)
- Microsoft.Agents.AI.Anthropic (>= 1.1.0-rc1)
- Microsoft.Agents.AI.AzureAI (>= 1.0.0-rc5)
- Microsoft.Agents.AI.CopilotStudio (>= 1.1.0-preview.260410.1)
- Microsoft.Agents.AI.OpenAI (>= 1.1.0)
- Microsoft.Agents.AI.Workflows (>= 1.1.0)
- Microsoft.Extensions.AI.OpenAI (>= 10.5.0)
- Microsoft.Extensions.Http.Resilience (>= 10.5.0)
- OpenTelemetry.Extensions.Hosting (>= 1.15.2)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.