WickedAILabs.AgentFramework
1.3.0
dotnet add package WickedAILabs.AgentFramework --version 1.3.0
NuGet\Install-Package WickedAILabs.AgentFramework -Version 1.3.0
<PackageReference Include="WickedAILabs.AgentFramework" Version="1.3.0" />
<PackageVersion Include="WickedAILabs.AgentFramework" Version="1.3.0" />
<PackageReference Include="WickedAILabs.AgentFramework" />
paket add WickedAILabs.AgentFramework --version 1.3.0
#r "nuget: WickedAILabs.AgentFramework, 1.3.0"
#:package WickedAILabs.AgentFramework@1.3.0
#addin nuget:?package=WickedAILabs.AgentFramework&version=1.3.0
#tool nuget:?package=WickedAILabs.AgentFramework&version=1.3.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}");
Prompts from files (optional)
For anything longer than a one-liner, point the agent at a file checked into source control instead of inlining a long string:
[Agent]
[AgentInstructionsFile("prompts/my-agent.md")]
public sealed class MyAgent : IAgent { /* ... */ }
The path is resolved relative to AppContext.BaseDirectory. Missing files fail fast at startup. Precedence (high → low) is:
AgentFramework:Agents:{Key}:Instructions(config literal)AgentFramework:Agents:{Key}:InstructionsFile(config path)[AgentInstructions("...")](attribute literal)[AgentInstructionsFile("...")](attribute path)
Structured output (optional)
When you want a typed result instead of a string:
public sealed record VarianceReport(string Department, double VariancePercent, string Status);
public Task<VarianceReport> RunAsync(string message, AgentContext ctx, CancellationToken ct)
=> AgentAI.AskForAsync<VarianceReport>(this, message, ct);
The framework derives a JSON schema from T, asks the provider to respond in that shape, and deserializes the result. Tools are not forwarded on the structured-output path — use AskAsync if you need tool dispatch.
Remote tools via MCP (Model Context Protocol)
Bind one or more MCP servers to an agent. Their tools are merged into the agent's [AgentTool] list automatically:
[Agent]
[UsesMcpServer("budget-reports", Url = "https://mcp.example/mcp", Transport = McpTransport.StreamableHttp)]
public sealed class AnalystAgent : IAgent { /* ... */ }
Or leave URL/transport out of the attribute and configure per environment:
{
"AgentFramework": {
"Mcp": {
"budget-reports": { "Url": "https://mcp.example/mcp", "Transport": "StreamableHttp" }
}
}
}
Supported transports: AutoDetect (default — probe then fall back to SSE), Sse, StreamableHttp. Stdio is not supported in v1.3. The framework opens one connection per unique binding and caches it for the process lifetime.
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 (56 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)
- ModelContextProtocol.Core (>= 1.2.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.