Emissary.Testing
0.1.0-preview.2
See the version list below for details.
dotnet add package Emissary.Testing --version 0.1.0-preview.2
NuGet\Install-Package Emissary.Testing -Version 0.1.0-preview.2
<PackageReference Include="Emissary.Testing" Version="0.1.0-preview.2" />
<PackageVersion Include="Emissary.Testing" Version="0.1.0-preview.2" />
<PackageReference Include="Emissary.Testing" />
paket add Emissary.Testing --version 0.1.0-preview.2
#r "nuget: Emissary.Testing, 0.1.0-preview.2"
#:package Emissary.Testing@0.1.0-preview.2
#addin nuget:?package=Emissary.Testing&version=0.1.0-preview.2&prerelease
#tool nuget:?package=Emissary.Testing&version=0.1.0-preview.2&prerelease
Emissary
Production-grade Claude agents for .NET β compile-time verified, provably safe, Native AOT.
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βEMS010) 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.
π 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. - Taint tracking β output from
Untrustedtools (web pages, documents, email) taints the run;Privilegedtools 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))]letsCompensateAsyncunwind 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()
.Stopped(AgentStopReason.Completed);
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;MapEmissaryApprovalsis 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 ... - 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. - Web search β
options.WebSearch = new WebSearchOptions { MaxUses = 3 }turns on Claude's server-side search (with domain allow/block lists). - Durable chat sessions β
ConversationSessionpersists history by conversation id throughIConversationStore(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
CostEstimatorfor per-tier (input/output/cache-write/cache-read) pricing. - Context compaction β
options.Compaction.TriggerInputTokenssummarizes 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. - OpenTelemetry GenAI semantic conventions β
invoke_agent/chat/execute_toolspans, 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 β currently six entries), 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: ~153 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 # 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.
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 persistence for human-in-the-loop workflows |
Samples
Nine 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, no API key needed. 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.
Sponsor
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.
License
| 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
- Emissary (>= 0.1.0-preview.2)
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 | 171 | 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 |