Getting started
Install
dotnet add package Emissary --prerelease
export ANTHROPIC_API_KEY=sk-ant-... # PowerShell: $env:ANTHROPIC_API_KEY = "sk-ant-..."
One package brings the runtime, the source generator, and the analyzer. Emissary targets .NET 10 and is Native AOT compatible.
Your first tool
A tool is a method with an attribute. The generator turns it into a wire-ready tool at compile time — the JSON Schema comes from the signature, the descriptions from your doc comments:
using Emissary;
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}";
}
One rule the compiler enforces for you: the containing type must be partial, or you get a build
error (EMS004) rather than a runtime surprise. The method can be static, as here, or an instance
method when the tool needs injected dependencies — see
Tools with dependencies.
Your first agent
var agent = new ClaudeAgent(new AgentOptions
{
SystemPrompt = "You are a concise assistant.",
Tools = { MyTools.GetWeatherTool }, // generated: {MethodName}Tool
});
var result = await agent.RunAsync("What's the weather in Oslo?");
Console.WriteLine(result.FinalText);
Console.WriteLine($"{result.Usage.InputTokens} in / {result.Usage.OutputTokens} out");
RunAsync drives the whole loop: it sends the conversation, executes any tools Claude calls
(in parallel when it calls several), feeds the results back, and repeats until the model
finishes or a limit is hit.
Streaming
Use StreamAsync when you want tokens as they arrive, or want to observe tool calls:
await foreach (var e in agent.StreamAsync("What's the weather in Oslo?"))
{
switch (e)
{
case AgentTextEvent text: Console.Write(text.Delta); break;
case AgentThinkingEvent think: Console.Write(think.Delta); break;
case AgentToolCallEvent call: Console.WriteLine($"[tool] {call.Name}"); break;
case AgentToolResultEvent r: Console.WriteLine($" -> {r.Result}"); break;
case AgentCompletedEvent done: /* done.Result */ break;
}
}
Multi-turn
Conversations are immutable; carry the result's conversation into the next turn:
var first = await agent.RunAsync("My name is Dana.");
var second = await agent.RunAsync(first.Conversation.Append(Message.User("What's my name?")));
For chat that must survive restarts, use a session instead.
Dependency injection
builder.Services.AddEmissary(options =>
{
options.SystemPrompt = "You are a concise assistant.";
options.Tools.Add(MyTools.GetWeatherTool);
});
Where to next
- Tools and schemas — parameter types, structured outputs, diagnostics
- Safety and contracts — the guarantees that make an agent auditable
- Testing agents — record/replay and behavioral assertions