Json.Ai.AgentMesh
0.3.0
dotnet add package Json.Ai.AgentMesh --version 0.3.0
NuGet\Install-Package Json.Ai.AgentMesh -Version 0.3.0
<PackageReference Include="Json.Ai.AgentMesh" Version="0.3.0" />
<PackageVersion Include="Json.Ai.AgentMesh" Version="0.3.0" />
<PackageReference Include="Json.Ai.AgentMesh" />
paket add Json.Ai.AgentMesh --version 0.3.0
#r "nuget: Json.Ai.AgentMesh, 0.3.0"
#:package Json.Ai.AgentMesh@0.3.0
#addin nuget:?package=Json.Ai.AgentMesh&version=0.3.0
#tool nuget:?package=Json.Ai.AgentMesh&version=0.3.0
AgentMesh
.NET library for orchestrating AI agents as a graph of nodes and edges, with typed state flowing between nodes and conditional edges deciding the path at runtime. Model access goes through OpenRouter.
Architecture decisions:
docs/architecture.md
Installation
dotnet add package Json.Ai.AgentMesh
Single package — abstractions, execution, and the OpenRouter adapter ship together. Target: net10.0.
Configuration
Everything is registered from AddAgentMesh, at application bootstrap:
services.AddAgentMesh(agentMesh =>
{
agentMesh.UseOpenRouter(options =>
{
options.ApiKey = Environment.GetEnvironmentVariable("OPENROUTER_API_KEY")!;
options.DefaultModelId = "openai/gpt-5-mini";
options.SiteTitle = "My App";
});
agentMesh.AddPipeline<MyState>("my-pipeline", graph => { /* ... */ });
});
OpenRouterOptions
| Property | Default | What it's for |
|---|---|---|
ApiKey |
— | Required. Sent as Authorization: Bearer. |
DefaultModelId |
openai/gpt-5-mini |
Model used by agents that don't call UseModel. |
SiteUrl / SiteTitle |
null |
Sent in HTTP-Referer / X-Title; identify the application in OpenRouter's rankings. |
Timeout |
2 min | Maximum time per call. |
CatalogCacheDuration |
1 h | Cache duration for IModelCatalog. |
BaseAddress |
https://openrouter.ai/api/v1/ |
API endpoint. |
Usage
The full path is tool → agent → node → pipeline.
1. Tool
What the model can invoke. ExecuteAsync receives the raw arguments generated by the model —
it's up to the tool to deserialize and validate them.
public interface IWebSearchTool : ITool;
public sealed class WebSearchTool : IWebSearchTool
{
public string Name => "web_search";
public string Description => "Searches the web for a search term.";
public object ParametersSchema => new
{
type = "object",
properties = new { query = new { type = "string", description = "Search term" } },
required = new[] { "query" },
};
public async Task<ToolOutput> ExecuteAsync(string argumentsJson, CancellationToken ct = default)
{
var query = JsonDocument.Parse(argumentsJson).RootElement.GetProperty("query").GetString();
return await SearchAsync(query, ct); // string implicitly converts to ToolOutput
}
}
2. Agent
The LLM persona, configured through fluent calls in its own constructor. An agent knows
nothing about state or graph — it only knows how to run an AgentTask.
public sealed class ResearcherAgent : AgentBase
{
public ResearcherAgent(IModelAdapter model, IWebSearchTool webSearch) : base(model)
{
WithRole("Researcher");
WithGoal("Find and summarize reliable information about any topic.");
WithBackstory("You always verify sources before answering.");
UseModel("anthropic/claude-sonnet-5");
UseTool(webSearch);
WithRouting(ProviderRouting.Create().PreferLowLatency().AllowFallbacks());
}
}
| Method | Effect |
|---|---|
WithRole |
role — who the agent is. |
WithGoal |
goal — what it's after. |
WithBackstory |
backstory — context and way of working. |
UseTool |
Declares that the agent has the tool. |
UseModel |
Pins the model; without it uses DefaultModelId. |
WithRouting |
Provider preferences. |
AddSubagent |
Exposes another agent as a tool, enabling delegation. |
The first three make up the system prompt. The agent runs in a loop: calls the model, executes whatever tools it asks for, returns the results, and repeats until a final answer comes back.
3. Node
The graph's processing unit. The node is what knows the state: it builds the AgentTask from
it and applies the result back.
public sealed class ResearchNode : INode<MyState>
{
private readonly ResearcherAgent _agent;
public ResearchNode(ResearcherAgent agent) => _agent = agent;
public async Task<MyState> RunAsync(MyState state, CancellationToken ct = default)
{
var task = new AgentTask(
description: $"Research: {state.Topic}",
expectedOutput: "A structured summary, with the sources used.");
var result = await _agent.RunAsync(task, ct);
state.Research = result.Output;
return state;
}
}
The dependency is one-directional: a node may receive an agent; an agent never receives a node.
For the trivial case — text in, text out, with no rule of its own — there's AgentNode<TAgent>,
which uses AgentContext as state and doesn't need to be written by hand.
4. Pipeline
The graph, declared at bootstrap. Nodes and edges, with GraphNode.Start and GraphNode.End
as terminals.
agentMesh.AddPipeline<MyState>("research", graph =>
{
graph.AddTool<IWebSearchTool, WebSearchTool>();
graph.AddAgent<ResearcherAgent>();
graph.AddNode<ResearchNode>();
graph.AddNode<WriteNode>();
graph.AddNode<ReviewNode>();
graph.AddEdge(GraphNode.Start, GraphNode.Of<ResearchNode>());
graph.AddEdge(GraphNode.Of<ResearchNode>(), GraphNode.Of<WriteNode>());
// The path can be decided at runtime, based on the state.
graph.AddConditionalEdge(
GraphNode.Of<WriteNode>(),
state => state.NeedsReview
? GraphNode.Of<ReviewNode>()
: GraphNode.End);
graph.AddEdge(GraphNode.Of<ReviewNode>(), GraphNode.End);
});
The graph is compiled at registration: an invalid topology — a node with no outgoing edge, an edge to an undeclared node, two initial nodes — fails at bootstrap, not in production.
5. Execution
The pipeline is resolved by name (keyed service). The state is an invocation argument, not a container service:
var pipeline = provider.GetRequiredKeyedService<IPipeline<MyState>>("research");
var result = await pipeline.RunAsync(new MyState { Topic = "hexagonal architecture" });
Or injected wherever it's used:
public sealed class ResearchEndpoint(
[FromKeyedServices("research")] IPipeline<MyState> pipeline)
{
public Task<MyState> HandleAsync(string topic, CancellationToken ct) =>
pipeline.RunAsync(new MyState { Topic = topic }, ct);
}
Guardrails
Validate the output after the model has responded. They belong to the task, not the agent — the same agent can run tasks with different validations.
public sealed class NotEmptyGuardrail : IGuardrail
{
public string Name => "not_empty";
public GuardrailResult Verify(string output) =>
string.IsNullOrWhiteSpace(output)
? GuardrailResult.Rejected("the response cannot be empty")
: GuardrailResult.Ok();
}
var task = new AgentTask("Summarize the text.", "One paragraph.")
.AddGuardrails(new NotEmptyGuardrail())
.AddTools(extraTool) // tools valid only for this execution
.WithMaxRetries(2); // retries if a guardrail rejects the output
If rejected, the agent tries again, reporting the reason to the model. Once retries are
exhausted, it throws GuardrailViolationException.
Provider routing
ProviderRouting maps to OpenRouter's routing options:
WithRouting(ProviderRouting.Create()
.PreferLowCost() // or PreferLowLatency() / PreferHighThroughput()
.WithOrder("anthropic", "openai") // try order
.AllowFallbacks(false) // require the specified provider
.Ignore("deepinfra")
.DenyDataCollection() // avoid providers that train on the data
.WithQuantizations("fp16", "bf16"));
For simple cases, the shortcuts on the model name itself work without ProviderRouting:
"openai/gpt-5-mini:floor" (lowest price) and ":nitro" (highest throughput).
Cost and consumption
AgentResult.Usage accumulates tokens and cost across every model call of an execution —
the turns of the tool-calling loop, guardrail retries, and the consumption of delegated
subagents.
var result = await agent.RunAsync(task);
Console.WriteLine($"US$ {result.Usage.CostUsd:F4}");
Console.WriteLine($"{result.Usage.TotalTokens} tokens across {result.Usage.ModelCalls} calls");
Using AgentContext as state, the whole pipeline's accumulated cost comes out in
result.TotalCostUsd.
Model catalog
IModelCatalog looks up the price, context, and capabilities of the available models — useful
for picking a model dynamically:
public sealed class PickModelNode(IModelCatalog catalog) : INode<MyState>
{
public async Task<MyState> RunAsync(MyState state, CancellationToken ct = default)
{
var models = await catalog.ListAsync(ct);
state.Model = models
.Where(m => m.SupportsTools && m.ContextLength >= 128_000)
.OrderBy(m => m.Pricing.PromptPricePerToken)
.First().Id;
return state;
}
}
License
MIT.
| 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
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.0)
- Microsoft.Extensions.Http (>= 10.0.0)
- Microsoft.Extensions.Options (>= 10.0.0)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on Json.Ai.AgentMesh:
| Package | Downloads |
|---|---|
|
Json.Ai.AgentMesh.OpenRouter
Adapter OpenRouter para o AgentMesh: chat completions, tool calling, roteamento de provedor e catálogo de modelos. |
GitHub repositories
This package is not used by any popular GitHub repositories.