![]() |
VOOZH | about |
dotnet add package UtilityAi.Maf --version 1.6.5
NuGet\Install-Package UtilityAi.Maf -Version 1.6.5
<PackageReference Include="UtilityAi.Maf" Version="1.6.5" />
<PackageVersion Include="UtilityAi.Maf" Version="1.6.5" />Directory.Packages.props
<PackageReference Include="UtilityAi.Maf" />Project file
paket add UtilityAi.Maf --version 1.6.5
#r "nuget: UtilityAi.Maf, 1.6.5"
#:package UtilityAi.Maf@1.6.5
#addin nuget:?package=UtilityAi.Maf&version=1.6.5Install as a Cake Addin
#tool nuget:?package=UtilityAi.Maf&version=1.6.5Install as a Cake Tool
π NuGet
π .NET 8
π Coverage
A lightweight, modular .NET 8 framework for building AI agent orchestration systems using classic Utility AI decision-making patterns. The framework evaluates and scores candidate actions each tick, executing the highest-utility option based on current context β no hardcoded workflows required.
Don't script workflows β evaluate them.
Utility AI is a decision-making architecture pioneered by Dave Mark, author of Behavioral Mathematics for Game AI (2009) and a leading voice in the game AI community. Through his influential GDC talks β most notably "Architecture Tricks: Managing Behaviors in Time, Space, and Depth" and the "Improving AI Decision Modeling Through Utility Theory" series β Mark demonstrated how mathematical response curves can replace brittle state machines and rigid behavior trees with fluid, context-sensitive reasoning.
Traditional AI decision systems (finite state machines, behavior trees, rule engines) hardcode transitions between states. They become fragile as complexity grows β every new behavior requires hand-authored connections that are difficult to maintain and impossible to tune gracefully.
Utility AI takes a fundamentally different approach: every possible action is scored numerically, and the highest-scoring action wins.
For each candidate action:
score = f(world state, action parameters)
Execute the action with the highest score.
This simple loop produces remarkably sophisticated behavior because:
Mark's architecture β sometimes called the Infinite Axis Utility System (IAUS) β formalizes this pattern into a reusable framework:
| Concept | Role |
|---|---|
| Action | A candidate behavior the agent could perform |
| Consideration | A single scoring axis that evaluates one aspect of the world state (0.0β1.0) |
| Response Curve | A mathematical function that maps a raw signal to a normalized score |
| Utility Score | The combined score of all considerations for an action, determining its priority |
The power of this approach is its composability: considerations are independent and reusable, curves are data-driven and tunable, and the set of candidate actions is open-ended.
While Utility AI was originally developed for game NPCs, its principles apply directly to modern AI agent orchestration:
| Game AI | Agent Orchestration |
|---|---|
| NPC selects an action each frame | Agent selects a capability each tick |
| World state drives considerations | EventBus facts drive considerations |
| Health, ammo, distance as signals | Token budget, task priority, user intent as signals |
| Patrol, attack, flee as actions | Research, summarize, respond as actions |
This framework brings Dave Mark's Utility AI architecture to the .NET ecosystem, extending it with event-sourced state management, LLM intent integration, and multi-agent coordination β while preserving the elegant simplicity of score everything, pick the best.
"The beauty of utility-based systems is that they allow you to ask the question 'what is the best thing to do right now?' rather than 'what state should I be in?'" β Dave Mark
| Category | Highlights |
|---|---|
| Decision Making | Utility-based scoring with response curves (logistic, power, piecewise) |
| LLM Integration | Intent interpretation with rich parameters; self-documenting capability metadata for prompt generation |
| Agent Orchestration | Microsoft Agent Framework (MAF) integration; multi-agent coordination with scoped state |
| Event System | Timestamped event history, type-safe subscriptions, scoped buses |
| Memory | Two-tier memory with automatic archival from EventBus to long-term storage |
| Extensibility | Pluggable sensors, modules, considerations, and selection strategies |
| Tooling | Real-time web dashboard for visualizing proposals, scores, and tick history |
| Observability | Built-in sinks for logging, metrics, and testing |
| Quality | 203+ tests, thread-safe, XML-documented public API |
# NuGet (recommended)
dotnet add package UtilityAi
Or clone the repository to explore the source and examples:
git clone https://github.com/mrrasmussendk/UtilityAi.git
cd UtilityAi
dotnet build
dotnet test
using UtilityAi.Orchestration;
using UtilityAi.Utils;
// 1. Create the event bus (shared state / blackboard)
var bus = new EventBus();
// 2. Configure the orchestrator
var orchestrator = new UtilityAiOrchestrator(bus: bus)
.AddSensor(new MyEnvironmentSensor())
.AddModule(new MyCapabilityModule());
// 3. Run the sense β propose β score β act loop
await orchestrator.RunAsync(maxTicks: 10, CancellationToken.None);
Reduce boilerplate with declarative attributes:
[Capability(Priority = 100, Domain = "validation")]
[RequiresFact<TaskQueue>]
[ActiveWhen("priority_mode", "urgent", "balanced")]
public class ValidationModule : ICapabilityModule
{
public IEnumerable<Proposal> Propose(Runtime rt) { /* ... */ }
}
// Auto-discover all modules from the assembly
var orchestrator = new UtilityAiOrchestrator(bus: bus)
.AddSensor(new MySensor())
.DiscoverCapabilities(Assembly.GetExecutingAssembly());
See the for a complete demo comparing manual and attribute-based approaches.
The framework follows a Sense β Propose β Score β Act loop:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β EventBus β
β (Shared state with history & scoping) β
ββββββββββββ²βββββββββββββββββββββββ¬βββββββββββββββββββ
β β
ββββββββ΄βββββββ βββββββΌβββββββ
β Sensors β β Modules β
β (Observe) β β (Propose) β
βββββββββββββββ βββββββ¬βββββββ
β
ββββββββΌβββββββ
β Proposals β
β + Score β
ββββββββ¬βββββββ
β
ββββββββΌβββββββ
βOrchestrator β
β Select Best β
β & Act β
βββββββββββββββ
| Component | Purpose | Extensibility |
|---|---|---|
| EventBus | Central state container with history, subscriptions, and scoping | Use as-is or wrap for persistence |
| ISensor | Observe environment and publish facts | Implement for your data sources |
| ICapabilityModule | Propose candidate actions | Implement + use attributes for auto-discovery |
| IConsideration | Score proposals (0.0β1.0) | Implement custom scoring logic |
| IOrchestrationSink | Observe orchestration events | Implement for logging/metrics |
utility = prior Γ (geometric_mean_of_considerations) ^ temperature
Proposals declare what parameters they need; the framework exposes this metadata so an LLM can provide structured responses that drive scoring automatically.
yield return ProposalHelper.For("ticket.create.priority")
.WithDescription("Create high-priority support ticket")
.ForIntent("ticket.create", IntentMatchType.Exact)
.ScoreByIntentParameter("urgency", x => Math.Pow(x, 3), (0, 1),
description: "How urgent the issue is (0=low, 1=critical)")
.UsesIntentParameter("customer_tier", "string",
allowedValues: new[] { "free", "premium", "enterprise" })
.WithAction(async ct => await CreatePriorityTicket(rt, ct));
Flow: Proposals declare parameters β Framework exposes metadata via GetCapabilitiesInfo() β LLM prompt includes parameter specs β LLM returns structured intent β Proposals score automatically β Best action wins.
Automatic Validation: Proposals with UsesIntentParameter or ScoreByIntentParameter are automatically protected by HasIntentParametersEligible β they're only eligible when all declared parameters exist in the IntentAnalysis.Parameters dictionary. This prevents proposals from being selected when the LLM hasn't provided the required parameters.
// Timestamped history β perfect for building LLM conversation context
var history = bus.GetHistory<UserMessage>(maxItems: 10);
foreach (var evt in history)
Console.WriteLine($"{evt.Timestamp}: {evt.Value.Text}");
// Type-safe subscriptions β react to events as they happen
using var sub = bus.Subscribe<TaskCompleted>(task =>
logger.LogInformation($"Task {task.Id} completed in {task.Duration}"));
Isolate per-agent state while sharing global facts:
var rootBus = new EventBus();
rootBus.Publish(new GlobalConfig("production"));
var agent1Bus = rootBus.CreateScope("agent-1");
var agent2Bus = rootBus.CreateScope("agent-2");
agent1Bus.Publish(new AgentStatus("busy"));
agent2Bus.TryGet<AgentStatus>(out var status); // β Not found (isolated)
agent1Bus.TryGetWithFallback<GlobalConfig>(out var config); // β
Found in parent
Two-tier architecture: EventBus (fast, last 100 events per type) + IMemoryStore (long-term, unlimited). The MemorySensor archives old facts automatically.
var memoryStore = new InMemoryStore();
var orchestrator = new UtilityAiOrchestrator(bus: bus)
.AddSensor(new MemorySensor(
store: memoryStore,
archiveThreshold: TimeSpan.FromMinutes(10),
typeof(UserMessage), typeof(AssistantMessage)))
.AddModule(new MyModule());
Orchestrate MAF agents with utility-based decision-making:
var orchestrator = new UtilityAiOrchestrator(bus: bus)
.AddMafAgentSensor(
new MafAgentRegistration("research", researchAgent),
new MafAgentRegistration("writer", writerAgent))
.AddMafAgent(researchAgent, "research",
considerations: new IConsideration[] { new MafAgentAvailable("research") })
.AddMafAgent(writerAgent, "writer",
considerations: new IConsideration[] { new HasMafAgentResult("research") });
|
public class OpenAIModule : ICapabilityModule
{
private readonly ChatClient _client;
public OpenAIModule(string apiKey)
=> _client = new ChatClient("gpt-4", apiKey);
public IEnumerable<Proposal> Propose(Runtime rt)
{
var userMsg = rt.Bus.GetOrDefault<UserMessage>();
if (userMsg == null) yield break;
yield return new Proposal(
id: "openai.respond",
cons: new[] { new HasFact<UserMessage>() },
act: async ct =>
{
var history = rt.Bus.GetHistory<UserMessage>(maxItems: 5);
var messages = history.Select(e => new UserChatMessage(e.Value.Text)).ToList();
var response = await _client.CompleteChatAsync(messages, ct);
rt.Bus.Publish(new AssistantMessage(response.Value.Content[0].Text));
});
}
}
The framework also supports Azure OpenAI, Anthropic Claude, and any custom provider via the ILlmProvider abstraction. See the for complete examples.
An optional web dashboard to visualize proposals, consideration scores, and tick history in real time:
var dashboardState = new DashboardState();
app.MapUtilityAiDashboard(dashboardState);
await orchestrator.RunAsync(maxTicks: 10, ct, sink: new DashboardSink(dashboardState));
Navigate to http://localhost:5000/utilityai/dashboard to inspect scores, override priors/temperatures, and step through ticks.
The framework is designed for testability. 203 tests cover all core functionality.
[Fact]
public async Task Orchestrator_ChoosesHighestUtility()
{
var bus = new EventBus();
bus.Publish(new UserMessage("test"));
var sink = new TestingSink();
var orch = new UtilityAiOrchestrator(bus: bus)
.AddModule(new MyModule());
await orch.RunAsync( maxTicks: 1, CancellationToken.None, sink);
Assert.Single(sink.ExecutedProposals);
Assert.Equal("my.action", sink.ExecutedProposals[0]);
}
dotnet test
UtilityAi/
βββ src/
β βββ UtilityAi/ # Core framework (NuGet: UtilityAi)
β βββ Utils/ # EventBus, Runtime
β βββ Orchestration/ # UtilityAiOrchestrator, Extensions
β βββ Sensor/ # ISensor + built-in sensors
β βββ Capabilities/ # ICapabilityModule, Attributes
β βββ Consideration/ # IConsideration + built-in considerations
β βββ Evaluators/ # Response curves (Logistic, Power, etc.)
β βββ Memory/ # IMemoryStore, InMemoryStore, MemorySensor
βββ integrations/
β βββ UtilityAi.Maf/ # Microsoft Agent Framework integration
β βββ UtilityAi.LLM.Abstractions/ # LLM provider abstraction
β βββ UtilityAi.LLM.OpenAI/ # OpenAI provider implementation
βββ examples/
β βββ Example/ # Demo agents (AgentAssistant, SmartHome, ChatBot, Intent)
β βββ Example.Maf/ # MAF integration demo
βββ tools/
β βββ UtilityAi.Dashboard/ # Real-time web dashboard
βββ Tests/ # 203 xUnit tests
βββ docs/ # Architecture, integration, and pattern guides
| Guide | Description |
|---|---|
| Framework design, orchestration loop, component roles | |
| Connect to MAF, OpenAI, Anthropic, Azure AI, and more | |
| Reference for all built-in sensors, considerations, and modules | |
| Best practices and anti-patterns for building proposals | |
| When to use hard gates vs soft scoring |
| Example | Description |
|---|---|
| Multi-strategy conversational AI agent | |
| IoT automation balancing comfort, energy, and security | |
| Simple OpenAI-powered chatbot | |
| LLM intent interpretation with rich parameters | |
| Microsoft Agent Framework orchestration |
Contributions that improve extensibility, documentation, or add well-tested features are welcome!
dotnet test)For maintainers: see for the release process.
MIT License β see for details.
Built with inspiration from:
<p align="center"> <sub>Built with β€οΈ for the AI agent community</sub> </p>
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 net8.0 is compatible. net8.0-android net8.0-android was computed. net8.0-browser net8.0-browser was computed. net8.0-ios net8.0-ios was computed. net8.0-maccatalyst net8.0-maccatalyst was computed. net8.0-macos net8.0-macos was computed. net8.0-tvos net8.0-tvos was computed. net8.0-windows net8.0-windows was computed. net9.0 net9.0 was computed. net9.0-android net9.0-android was computed. net9.0-browser net9.0-browser was computed. net9.0-ios net9.0-ios was computed. net9.0-maccatalyst net9.0-maccatalyst was computed. net9.0-macos net9.0-macos was computed. net9.0-tvos net9.0-tvos was computed. net9.0-windows net9.0-windows was computed. net10.0 net10.0 was computed. net10.0-android net10.0-android was computed. net10.0-browser net10.0-browser was computed. net10.0-ios net10.0-ios was computed. net10.0-maccatalyst net10.0-maccatalyst was computed. net10.0-macos net10.0-macos was computed. net10.0-tvos net10.0-tvos was computed. net10.0-windows net10.0-windows was computed. |
This package is not used by any NuGet packages.
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.6.5 | 136 | 2/25/2026 |
| 1.6.3 | 121 | 2/20/2026 |
| 1.6.2 | 75 | 2/20/2026 |
| 1.6.1 | 73 | 2/20/2026 |
| 1.6.0 | 79 | 2/20/2026 |
| 1.5.9 | 78 | 2/20/2026 |
| 1.5.8 | 72 | 2/20/2026 |
| 1.5.7 | 74 | 2/20/2026 |
| 1.5.6 | 76 | 2/20/2026 |
| 1.5.5 | 76 | 2/20/2026 |
| 1.5.4 | 79 | 2/20/2026 |
| 1.5.3 | 67 | 2/20/2026 |
| 1.5.2 | 75 | 2/20/2026 |
| 1.5.1 | 71 | 2/20/2026 |
| 1.5.0 | 72 | 2/20/2026 |
| 1.4.9 | 75 | 2/20/2026 |
| 1.4.8 | 76 | 2/20/2026 |
| 1.4.7 | 77 | 2/20/2026 |
| 1.4.6 | 74 | 2/20/2026 |
| 1.4.5 | 74 | 2/20/2026 |