WickedAILabs.AgentFramework 1.3.0

dotnet add package WickedAILabs.AgentFramework --version 1.3.0
                    
NuGet\Install-Package WickedAILabs.AgentFramework -Version 1.3.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="WickedAILabs.AgentFramework" Version="1.3.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="WickedAILabs.AgentFramework" Version="1.3.0" />
                    
Directory.Packages.props
<PackageReference Include="WickedAILabs.AgentFramework" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add WickedAILabs.AgentFramework --version 1.3.0
                    
#r "nuget: WickedAILabs.AgentFramework, 1.3.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package WickedAILabs.AgentFramework@1.3.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=WickedAILabs.AgentFramework&version=1.3.0
                    
Install as a Cake Addin
#tool nuget:?package=WickedAILabs.AgentFramework&version=1.3.0
                    
Install as a Cake Tool

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).

<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:

  1. Create a GitHub Personal Access Token with the read:packages scope (classic PAT) or equivalent fine-grained permission. Export it as GITHUB_USER + GITHUB_TOKEN in the shell running dotnet restore.

  2. Add a nuget.config to 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>
    
  3. 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 (with packages: read permission) 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:

  1. AgentFramework:Agents:{Key}:Instructions (config literal)
  2. AgentFramework:Agents:{Key}:InstructionsFile (config path)
  3. [AgentInstructions("...")] (attribute literal)
  4. [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 AgentContext unless you pass configure: 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_id OTel 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.

  1. Ensure main is green and the work you want to release is merged.

  2. Add a new ## [X.Y.Z] — YYYY-MM-DD entry to CHANGELOG.md summarising the changes. Commit on main (via PR).

  3. 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.0
    
  4. The Release workflow (.github/workflows/release.yml) fires on the tag push. It will:

    • Check out with fetch-depth: 0 (MinVer requires full git history).
    • dotnet restoredotnet build -c Releasedotnet pack the framework project only.
    • Push the .nupkg + .snupkg to GitHub Packages using the ambient GITHUB_TOKEN (no secrets configuration required).
    • If the tag is a stable release (no - suffix), also push to NuGet.org using the NUGET_API_KEY secret. Prerelease tags (e.g. v1.2.0-beta.1) stay private on GitHub Packages.
    • Upload the artifacts to the workflow run for auditing.
  5. 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)

One-time NuGet.org setup

The NUGET_API_KEY secret must exist before the first stable tag is pushed to publish publicly.

  1. Sign in to https://www.nuget.org (GitHub or Microsoft SSO).
  2. 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.
  3. In this repo: Settings → Secrets and variables → Actions → New repository secret:
    • Name: NUGET_API_KEY
    • Value: the key from step 2
  4. (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.Z exactly on a commit → package version X.Y.Z (stable → both feeds).
  • Tag vX.Y.Z-beta.N → package version X.Y.Z-beta.N (prerelease → GitHub Packages only).
  • N commits after the latest tag, without a newer tag → package version X.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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.3.0 72 4/19/2026
1.2.0 70 4/18/2026