Emissary.Testing 0.1.0-preview.4

This is a prerelease version of Emissary.Testing.
dotnet add package Emissary.Testing --version 0.1.0-preview.4
                    
NuGet\Install-Package Emissary.Testing -Version 0.1.0-preview.4
                    
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="Emissary.Testing" Version="0.1.0-preview.4" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Emissary.Testing" Version="0.1.0-preview.4" />
                    
Directory.Packages.props
<PackageReference Include="Emissary.Testing" />
                    
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 Emissary.Testing --version 0.1.0-preview.4
                    
#r "nuget: Emissary.Testing, 0.1.0-preview.4"
                    
#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 Emissary.Testing@0.1.0-preview.4
                    
#: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=Emissary.Testing&version=0.1.0-preview.4&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Emissary.Testing&version=0.1.0-preview.4&prerelease
                    
Install as a Cake Tool

<img src="assets/emissary-mark.svg" alt="" width="96" align="left" hspace="18" vspace="4">

Emissary

Production-grade Claude agents for .NET β€” compile-time verified, provably safe, Native AOT.

CI NuGet Docs License: MIT

πŸ“– Documentation β€” guides and full API reference.

Emissary is a Claude-native agent framework for modern C#. Where other frameworks discover agent mistakes in production, Emissary shifts correctness left: tool schemas and dispatchers are generated by the compiler, agent behavior is constrained by enforceable contracts, and every run is recordable, replayable, and assertable in CI β€” agents you can put in front of an auditor.

var options = new AgentOptions
{
    SystemPrompt = "You are a support agent. Verify identity before refunding.",
    Tools = { SupportTools.VerifyIdentityTool, SupportTools.RefundPaymentTool },
};
options.Rules.Require("refund_payment", prerequisite: "verify_identity");

var agent = new ClaudeAgent(options);
var result = await agent.RunAsync("Refund order A-1001, it arrived broken.");
Console.WriteLine(result.FinalText);

internal static partial class SupportTools
{
    /// <summary>Refunds a payment for an order.</summary>
    /// <param name="orderId">The order id.</param>
    /// <param name="amount">The amount to refund.</param>
    [ClaudeTool(Privileged = true)]
    public static string RefundPayment(string orderId, double amount) => /* ... */;
}

That [ClaudeTool] attribute is the whole integration: at compile time, a Roslyn source generator turns the method into a wire-ready tool β€” JSON Schema derived from the signature, descriptions harvested from the doc comments, and a strongly-typed dispatcher with zero reflection. If a parameter type can't be represented, or the description is missing, or a safety declaration is inconsistent, the build fails, not the production run.

Why Emissary

πŸ›  The compiler is the safety net. Tools, structured-output schemas, and argument binders are source-generated β€” no reflection anywhere in the pipeline. Analyzer diagnostics (EMS001–EMS012) catch undescribed tools and parameters, unrepresentable types, broken compensation targets, and safety attributes that would be silently ignored β€” in the IDE. The proof lives in CI: a full agent with tools, nested records, and strict schemas compiles to a 1.5 MB self-contained Native AOT binary on every commit.

Tools can be static or instance methods, so a tool reaches your DbContext or HttpClient straight from the container β€” the generated {Method}Tool is then an instance property bound to that object, with no service locator and no static state.

Those generated binders also validate what the model sends: a wrong-typed argument or an invented enum value comes back as a precise, model-visible error ("argument 'left' must be a whole number … but the value was the string "one"", "must be one of: Celsius, Fahrenheit"), so the model corrects itself on the next turn instead of the run dying on an exception.

πŸ”’ Provable behavior, not prompt engineering. The answer to "how do you guarantee the agent can't do X?" is enforced machinery, all model-visible so Claude self-corrects:

  • Tool contracts β€” Rules.Require("refund_payment", "verify_identity"), Terminal("close_ticket"), Limit("send_email", 3). Violating calls never execute, and a contract naming a tool the agent doesn't have throws at construction β€” a typo must not leave a privileged tool quietly unguarded.
  • Taint tracking β€” output from Untrusted tools (web pages, documents, email) taints the run; Privileged tools are then blocked. Information-flow control against prompt injection, demonstrated in a replayable sample where an injected instruction provably fails.
  • RBAC β€” [AuthorizeTool("payments")] tools are filtered before prompt construction: unauthorized callers' models never even see the schema. Deny by default.
  • Shadow mode β€” run the whole agent with privileged effects intercepted into a reviewable plan of effects instead of executed.
  • Human-in-the-loop gates β€” a gated call durably suspends the run (state serializes to JSON, survives restarts via SQLite); a webhook approval resumes it minutes or days later with contracts and taint state intact.
  • Compensation sagas β€” [ClaudeTool(CompensatedBy = nameof(CancelReservation))] lets CompensateAsync unwind a run's effects in reverse order.

πŸ” Deterministic testing for nondeterministic systems. Record any run as a .trajectory file; replay it byte-for-byte with zero network β€” real tools execute, drift throws a divergence error. Then assert on behavior:

EmissaryAssert.That(result)
    .ToolCalled("refund_payment", times: 1)
    .ToolNotCalledBefore("refund_payment", requiredPredecessor: "verify_identity")
    .NotTainted()
    .NoToolFailures()   // a tool that started throwing β€” the model narrates around it
    .Complete();        // a truncated, refused, or paused answer reads the same as a good one

Golden trajectories run in CI as regression suites β€” Emissary's own test suite is the first customer. And they answer the question every team dreads: what changes when we upgrade the model? Point a candidate agent at a recorded baseline and get a behavioral diff:

var report = await TrajectoryCanary.RunAsync(baseline, candidateAgent);
// Result: BEHAVIOR CHANGED
//   [tool_sequence] baseline [verify_identity -> refund_payment], candidate [refund_payment]

Passed tolerates wording drift; a changed tool sequence, turn count, or stop reason fails the canary. Model upgrades become a managed rollout instead of a leap of faith.

And beyond "did it change?", you can grade "is it good?" β€” LLM-as-judge evaluation against a rubric, where the judge is itself a replayable agent:

var rubric = new EvaluationRubric()
    .Criterion("resolved", "Did the agent resolve the customer's request?")
    .Criterion("safe", "Did it avoid acting on untrusted content?");
var eval = await EmissaryEval.EvaluateAsync(rubric, result, judgeAgent);
if (!eval.Passed) Console.WriteLine(eval.ToText());

πŸš€ Production plumbing included.

  • app.MapEmissaryAgent("/agent") β€” Server-Sent Events streaming out of ASP.NET Core minimal APIs; MapEmissaryApprovals is the resume webhook. Ships as a Native AOT container.
  • MCP server β€” your generated C# tools (or a whole agent) become Model Context Protocol tools callable from Claude Code and Claude Desktop: claude mcp add my-tools -- dotnet run ...
  • IChatClient adapter β€” new EmissaryChatClient(agent) drops an Emissary agent into any Microsoft.Extensions.AI pipeline, including Microsoft Agent Framework orchestrations. Claude-native to build, universally consumable once built.
  • Prompt caching by default β€” automatic cache breakpoints (tools, system prompt, latest message) keep multi-turn cache hit rates high; cache economics are visible per run.
  • Resilience β€” options.Resilience: retries with capped exponential backoff, per-attempt timeouts, and transient-error classification. Retries only before the first streamed event, so partial output is never replayed.
  • Tool failure containment β€” a tool that throws or exceeds options.ToolFailures.Timeout is reported to the model as an error it can recover from, not an exception that ends the run. The exception reaches you in full (result.ToolFailures, AgentToolFailedEvent, the span), while the model is told only the exception type β€” messages carry connection strings and record data, and everything the model sees goes to the API (ADR 0007).
  • Bounded tool concurrency β€” options.MaxParallelTools caps how many of a turn's tool calls run at once, so a model that asks for a dozen lookups can't drain a connection pool the rest of the application shares. Results still come back in tool_use order.
  • Web search β€” options.WebSearch = new WebSearchOptions { MaxUses = 3 } turns on Claude's server-side search (with domain allow/block lists), with a documented single-turn limitation.
  • Durable chat sessions β€” ConversationSession persists history by conversation id through IConversationStore (in-memory or SQLite), so a chatbot resumes across requests and restarts.
  • Token budgets and dollar costs β€” hard token caps that stop a run before the next model call, plus CostEstimator for per-tier (input/output/cache-write/cache-read) pricing.
  • Context compaction β€” options.Compaction.TriggerInputTokens summarizes older turns so long conversations survive past the context window. Emissary compacts client-side on purpose, so compacted runs still replay (ADR 0006).
  • Sub-agent composition β€” agent.AsTool(name, description) hands a whole agent to another agent as one tool, and safety composes: a sub-agent that reads untrusted content taints its caller, so the parent's contracts still hold across the boundary.
  • Multi-agent handoff β€” options.Handoffs.Add(new HandoffTarget("billing", billing, "…")) turns each target into a handoff_to_billing tool; calling it transfers the whole conversation, and the target answers under its own prompt, tools, and contracts. Taint crosses the boundary with it, so a transfer can't launder a tainted conversation into privileged tools.
  • OpenTelemetry GenAI semantic conventions β€” invoke_agent/chat/execute_tool spans, token and tool-call metrics. AddSource("Emissary") and it appears in Aspire.
  • Streaming (IAsyncEnumerable<AgentEvent> including thinking deltas), adaptive thinking and effort control, typed structured outputs end to end.

πŸ“ An engineering bar you can audit. 100% line and branch coverage enforced by CI (with a public, per-line justified baseline for compiler artifacts β€” nine entries covering eleven branches), the AOT proof executed on every commit, every sample compiled on every commit, and architecture decisions recorded as ADRs. The performance claims are measured, not asserted: ~150 ns per tool dispatch, ~4 Β΅s per replayed agent run, 1.53 MB AOT binary with a ~44 ms process lifetime.

Quick start

dotnet add package Emissary --prerelease   # runtime + source generator, one package
export ANTHROPIC_API_KEY=...
using Emissary;

var agent = new ClaudeAgent(new AgentOptions
{
    SystemPrompt = "You are a concise assistant.",
    Tools = { MyTools.GetWeatherTool },
});

await foreach (var e in agent.StreamAsync("Weather in Oslo?"))
{
    if (e is AgentTextEvent text) Console.Write(text.Delta);
}

internal static partial class MyTools
{
    /// <summary>Gets the current weather for a city.</summary>
    /// <param name="city">The city name.</param>
    [ClaudeTool]
    public static string GetWeather(string city) => $"18Β°C and clear in {city}";
}

Typed structured outputs are the same pattern, in reverse:

/// <summary>A triaged support ticket.</summary>
[ClaudeSchema]
public sealed partial record TicketTriage(string Title, Severity Severity, string[] Tags);

var options = new AgentOptions().WithOutput<TicketTriage>();      // compile-time strict schema
var triage = await agent.RunAsync("Triage this…", MyJsonContext.Default.TicketTriage);

The schema is generated from the record, the API enforces it, and the result deserializes through System.Text.Json source generation β€” typed end to end, no reflection.

Documentation

The full site β€” guides plus API reference generated from the XML docs β€” is at zcsizmadia.github.io/Emissary, rebuilt on every push to main. The guides are also plain Markdown in docs/guides if you would rather read them in the repo.

Guide What it covers
Getting started First tool, first agent, what the generator emits
Tools and schemas Descriptions, supported types, tools with injected dependencies, argument validation, structured outputs, diagnostics
Safety and contracts Contracts, taint tracking, RBAC, shadow mode, human-in-the-loop gates, compensation
Testing Record/replay, behavioral assertions, model-upgrade canarying, LLM-as-judge evaluation
Production Hosting, resilience, tool failures, tool concurrency, stop reasons, caching and cost, telemetry
Benchmarks The measured numbers behind the performance claims

Design decisions live in docs/adr β€” why Emissary is Claude-native, why a failed tool tells the model less than it tells you, and why tests at the SDK boundary must use SDK-produced values.

Packages

Package What it adds
Emissary The agent runtime + the [ClaudeTool]/[ClaudeSchema] source generator and analyzer
Emissary.Testing Framework-agnostic behavioral assertions for agent runs and replays
Emissary.AspNetCore SSE streaming endpoints and the human-approval webhook
Emissary.Mcp Expose tools or whole agents as an MCP stdio server
Emissary.Sqlite Durable suspended-run and conversation persistence
Emissary.OpenApi Tools generated from an OpenAPI specification, with taint-safe defaults read from its own verbs
Emissary.Aspire Configuration-bound agents, OpenTelemetry wired to the dashboard, and a health check that costs nothing
Emissary.Extensions.AI IChatClient adapter, for Microsoft.Extensions.AI pipelines and Microsoft Agent Framework interop

Samples

Eleven runnable samples build in CI on every commit β€” documentation that cannot rot. Start at samples/01-HelloTool and walk forward; 04-RecordReplay and 06-ZeroTrustAgent run fully offline via bundled trajectories, and 11-OpenApiTools drives a real public API without one either β€” no API key needed for any of the three. The samples README has the map.

Design

Emissary is deliberately Claude-native, not provider-agnostic (ADR 0001): depth over breadth β€” prompt caching, thinking, compaction, and trajectory fidelity live in provider-specific details that neutral abstractions erase. Interop runs the other direction: Emissary agents are consumable from anywhere via MCP. It targets .NET 10 only, embraces C# 14, and releases are tag-driven (ADR 0005).

Built on the official Anthropic C# SDK, kept behind an internal seam so its types never enter Emissary's public API.

Emissary is free and MIT-licensed. If it saves you time or your team relies on it, please consider sponsoring its development β€” it directly funds the time that keeps the 100%-coverage, provably-safe engineering bar high.

Sponsor

License

MIT

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
0.1.0-preview.4 157 8/24/2026
0.1.0-preview.3 48 8/24/2026
0.1.0-preview.2 56 8/22/2026
0.1.0-preview.1 53 8/22/2026