Universal.Operative.Sdk 2.0.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package Universal.Operative.Sdk --version 2.0.0
                    
NuGet\Install-Package Universal.Operative.Sdk -Version 2.0.0
                    
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="Universal.Operative.Sdk" Version="2.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Universal.Operative.Sdk" Version="2.0.0" />
                    
Directory.Packages.props
<PackageReference Include="Universal.Operative.Sdk" />
                    
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 Universal.Operative.Sdk --version 2.0.0
                    
#r "nuget: Universal.Operative.Sdk, 2.0.0"
                    
#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 Universal.Operative.Sdk@2.0.0
                    
#: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=Universal.Operative.Sdk&version=2.0.0
                    
Install as a Cake Addin
#tool nuget:?package=Universal.Operative.Sdk&version=2.0.0
                    
Install as a Cake Tool

About

Universal.Operative.Sdk is a provider-neutral framework for building long-running, agentic LLM-driven applications. It defines the domain model (Conversation, Message, content Blocks), the extension seams (IModel, ITool, ITransformer), and two complementary building blocks that sit on top of them: an Engine, which drives a single agentic turn end-to-end (stream a model response, dispatch any tool calls, feed results back, loop until the model stops), and an Operative, which wraps an Engine around one ongoing Conversation and decides whether a new inbound message starts a fresh turn or steers into one already running. Pair it with Universal.Operative.Sdk.Anthropic or Universal.Operative.Sdk.OpenAI for a concrete IModel.

How to Use

Installation

dotnet add package Universal.Operative.Sdk

Quick start: a single turn

Use Engine directly when there's no ongoing session to manage — a one-shot request/response, a batch job, a single CLI invocation.

using Universal.Operative.Sdk;

IModel model = /* an IModel, e.g. from Universal.Operative.Sdk.Anthropic or .OpenAI */;

IEngine engine = EngineBuilder.FromDefaults(model)
    .AddBaselineTools()
    .Build();

var conversation = new Conversation
{
    System = new List<Block> { new TextBlock("You are a helpful assistant.") },
    Messages = { new Message { Role = MessageRoles.User, Content = { new TextBlock("List the files in this repo.") } } },
};

await foreach (EngineEvent e in engine.ExecuteAsync(conversation))
{
    switch (e)
    {
        case ModelStreamEvent s: break;
        case AssistantMessageEvent a: Console.WriteLine(a.Message.Content); break;
        case ToolCallStartedEvent t: Console.WriteLine($"Running {t.Call.Name}..."); break;
        case TurnCompletedEvent done: Console.WriteLine($"Done: {done.Reason}"); break;
    }
}

AddBaselineTools() registers Read, Write, Edit, Glob, Grep, a shell (Bash and/or PowerShell, by availability), TodoWrite, and NotebookEdit. EngineBuilder.FromDefaults wires a pre-transformer stack (boundary-aware history windowing, tool-result budgeting, history snipping, auto-compaction) so long-running conversations don't blow the model's context window; use new EngineBuilder() instead to opt into transformers individually. conversation.Messages holds the full transcript once the turn ends, including tool calls and results.

Quick start: a long-running session

Use IOperative when something outside your control — a chat UI, a Slack app, a webhook — can send a new message while a turn is already running, and you want that message to steer the in-progress turn instead of queuing behind it.

IOperative bot = OperativeBuilder.Create(engine, conversation).Build();

SubmitResult result = await bot.SubmitAsync(
    new Message { Role = MessageRoles.User, Content = { new TextBlock("hi") } });

await foreach (OperativeEvent e in result.Events)
{
    if (e is EngineEventOccurred occurred)
    {
        // same EngineEvent types as the IEngine example above
    }
}

Whether this call started the turn (result.Outcome == SubmitOutcome.StartedTurn) or steered a message into one already running (SubmitOutcome.Injected), result.Events is the same live remainder of that turn either way — one shared consciousness: every caller who touched a turn sees its real outcome, not just whoever happened to start it. Call bot.ResumeAsync() to continue the conversation with no new input at all — e.g. after a restart, when the loaded conversation already ends in an unanswered message; if a turn is already running this joins it too (SubmitOutcome.AlreadyRunning) rather than starting a redundant second one.

Two OperativeBuilder options worth knowing:

  • .WithReloadBeforeStart(reload) — reload the conversation from durable storage immediately before each turn starts, so a previous turn that crashed mid-flight self-heals instead of wedging every future one. Runs only once a turn has actually been claimed, so it can't race a turn already in flight.
  • .WithFrameInjectedMessage(frame) — customize (or disable) the note prefixed to a message before it's steered into someone else's in-progress turn.

Core concepts

  • Conversation / Message / Block — the provider-neutral transcript. Message.Content is a list of Blocks (TextBlock, ToolCallBlock, ToolResultBlock, …); round-tripping a Conversation through JSON is enough to resume it later.
  • IModel — the seam to a concrete provider. Implement StreamAsync to translate a Conversation into a provider request and its response stream into StreamEvents; CompleteAsync is provided for free as a fold over the stream.
  • ITool / Tool<TArgs> — a callable the model can invoke. Subclass Tool<TArgs> with a typed argument record and a hand-authored JSON-Schema Definition; the base class handles (de)serialization and turns malformed arguments into a model-readable error instead of an exception. See Tools/BaselineTools.cs and Tools/FileSystem/GlobTool.cs for worked examples.
  • ITransformer — rewrites the conversation (or shared IContext) around a model call — compaction, tool-result pruning, history snipping. Engine runs four stages: BeforeTurnTransformers once before the turn's first iteration, BeforeIterationTransformers before each model call, AfterIterationTransformers after the assistant message lands but before tools run, and AfterTurnTransformers once after the loop ends.
  • Engine / IEngine — drives one turn to completion, exactly as shown above. Every step is a protected virtual method, so subclass to override one piece (tool-result formatting, dispatch policy, timeouts) without reimplementing the whole turn. An Engine holds no session state of its own — one Engine (one model/tool/transformer configuration) is meant to be reused across many independent IOperative sessions, the same way one IModel is reused across many engines.
  • OperativeBuilder / Operative / IOperative — owns one ongoing Conversation and single-flight-serialises turns against it through an Engine. SubmitAsync starts a fresh turn if none is running, or steers a new message into the turn already in flight if one is; ResumeAsync continues the conversation with no new input. Build one through OperativeBuilder, not the Operative constructor directly.
  • EngineEvent — the observable channel for one turn, as driven directly by IEngine.ExecuteAsync: model stream events, the settled assistant message, tool start/complete/error events, the synthesized tool-result message, transformer applications, and a terminal TurnCompletedEvent.
  • OperativeEvent — what IOperative.SubmitAsync/ResumeAsync's SubmitResult.Events actually yields. EngineEventOccurred relays an underlying EngineEvent as-is; MessageQueuedForNextTurnEvent is IOperative-only — it fires when a message arrives too late for the engine's own mid-turn drain to catch, so IOperative appends it directly instead of losing it, ready for the next turn to answer. Kept separate from EngineEvent because IEngine has no notion of IOperative and is fully usable without one.
  • Session isolation — one IOperative is one conversation; every message submitted to it shares that same history and turn. If two channels or two users need to stay unaware of each other, give each its own IOperative (e.g. a Dictionary<string, IOperative> keyed by user/channel/thread) rather than routing everyone through one — the same IEngine (and its model/tools) can safely back any number of them at once, since IEngine holds no session state of its own.

Writing a custom tool

using System.Text.Json;
using System.Text.Json.Serialization;
using Universal.Operative.Sdk.Tools;

public sealed class WeatherTool : Tool<WeatherTool.Args>
{
    public override string Name => "get_weather";
    public override bool IsConcurrencySafe => true;

    public override ToolDefinition Definition { get; } = new()
    {
        Name = "get_weather",
        Description = "Get the current weather for a city.",
        Parameters = JsonDocument.Parse("""
            { "type": "object", "properties": { "city": { "type": "string" } }, "required": ["city"] }
            """).RootElement.Clone(),
    };

    public sealed record Args
    {
        [JsonPropertyName("city")] public string City { get; init; } = string.Empty;
    }

    protected override async Task<ToolResult> ExecuteAsync(Args args, CancellationToken cancellationToken)
        => ToolResult.Json(new { city = args.City, tempF = 72 });
}

IsConcurrencySafe = true marks a read-only tool as safe to run in parallel with other calls in the same turn; mutating tools default to false and run serially in call order. Register a tool with builder.AddTool(new WeatherTool()) and the harness auto-publishes its Definition on outbound messages and dispatches calls to it by name.

Background tool execution

A tool whose work can outlive a single call implements IBackgroundTool (most conveniently via BackgroundTool<TArgs>, which mirrors Tool<TArgs>'s typed-argument split). ShellTool (and so BashTool/PowerShellTool) already does this — pass run_in_background: true in a call's arguments and it hands back a handle instead of running to completion inline.

IEngine engine = new EngineBuilder()
    .WithModel(model)
    .AddBaselineTools()
    .WithBackgroundTasks(new InMemoryBackgroundTaskRegistry())
    .Build();

WithBackgroundTasks does two things: it wires Engine.Options.BackgroundDispatch to route any run_in_background: true call on an IBackgroundTool through the given IBackgroundTaskRegistry instead of running it inline, and it registers TaskOutputTool/TaskStopTool against that same registry so the model can poll or stop what it started, by the task id the initial call returns. Without a registry wired in, run_in_background is simply ignored and every tool runs the ordinary way.

Subagents

AgentTool spawns a subagent — a fresh IEngine run against its own scratch Conversation seeded with a prompt — and returns its final answer, modeled on Claude Code's own subagent tool. It's an IBackgroundTool, so run_in_background: true works the same way as any other background tool.

IEngine SubagentEngineFactory() => new EngineBuilder()
    .WithModel(subagentModel)
    .AddBaselineTools()
    .Build();

builder.AddTool(new AgentTool(SubagentEngineFactory));

A fresh IEngine is requested from the factory for every spawn rather than one shared instance being reused, because transformers that track per-conversation state (a persistence transformer tracking which file it's writing to, for instance) would get confused mixing multiple unrelated spawns through the same instance. The factory is also where an audit trail gets wired up — give the returned engine its own FileSystemConversationPersistenceTransformer pointed at a per-spawn directory to leave an independently reviewable transcript for every subagent run.

License

Free for noncommercial use under the Universal.Operative Noncommercial License. Source is closed; commercial use requires a separate license from Andrew Ong.

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.
  • net10.0

    • No dependencies.

NuGet packages (12)

Showing the top 5 NuGet packages that depend on Universal.Operative.Sdk:

Package Downloads
Universal.Operative.Sdk.Anthropic

Anthropic extensions for the Universal.Operative.Sdk.

Universal.Operative.Sdk.OpenAI

OpenAI extensions for Universal.Operative.Sdk.

Universal.Operative.Sdk.GoogleGemini

Google Gemini extensions for the Universal.Operative.Sdk.

Universal.Operative.Sdk.xAI

xAI (Grok) extensions for the Universal.Operative.Sdk.

Universal.Operative.Sdk.AwsBedrock

Amazon Bedrock extensions for the Universal.Operative.Sdk.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
4.0.0 103 7/28/2026
3.3.0 74 7/28/2026
3.2.0 149 7/27/2026
3.1.0 124 7/26/2026
3.0.0 171 7/23/2026
2.0.0 164 7/21/2026
2.0.0-beta1 91 7/21/2026
1.5.0 104 7/21/2026
1.4.0 129 7/20/2026
1.3.1 118 7/19/2026
1.3.0 168 7/6/2026
1.2.0 98 7/6/2026
1.1.0 102 7/4/2026
1.0.3 108 7/4/2026
1.0.2 115 7/4/2026
1.0.1 119 7/4/2026
1.0.0 140 7/3/2026

2.0.0 — a major structural release. Breaking: what was IOperative/Operative/OperativeBuilder/OperativeEvent (the single-turn execution loop) is now IEngine/Engine/EngineBuilder/EngineEvent. IOperative/Operative/OperativeBuilder are new top-level types representing a long-running session on top of an IEngine, built through OperativeBuilder rather than Operative's constructor directly. IOperative.SubmitAsync(Message, IContext?, ct) starts a turn if idle or steers a message into one already running instead of queuing behind it; IOperative.ResumeAsync(IContext?, ct) continues a conversation with no new input (e.g. after a restart). Both carry the same optional per-call IContext as IEngine.ExecuteAsync, and both return a SubmitResult whose Events is always populated: whether a call started the turn or joined one already running, it gets a live view of that turn's real remainder — the same eventual answer or failure every other caller touching it sees, not just whoever started it. Events is typed OperativeEvent, not EngineEvent: EngineEventOccurred relays an underlying EngineEvent as-is, and MessageQueuedForNextTurnEvent is new — it fires when a message arrives too late for the engine's own mid-turn drain to catch, so IOperative appends it directly instead of losing it silently. OperativeBuilder.WithReloadBeforeStart(reload) reloads a conversation from durable storage immediately before each turn starts, race-free since it only runs once a turn is actually claimed; WithFrameInjectedMessage(frame) customizes the note prefixed to a steered-in message. Validated by migrating Ong.Bot's Sophie and the entire RedMarbleAI.Operative fleet (9 deployments) off their own hand-rolled turn-lock/injection-queue code onto IOperative directly. IEngine.ExecuteAsync gained a per-call (IContext? context, Conversation conversation, ChannelReader<Message>? injectQueue, CancellationToken) overload — validated against every current downstream deployment before promoting it onto the interface. Every previously-[Obsolete] alias (PreTransformers/PostTransformers, AddPreTransformer/AddPostTransformer, TransformerStages.Pre/Post, the ChannelReader-based ExecuteAsync overload, ExecuteOptions) is removed outright rather than deprecated. Added: IBackgroundTool/BackgroundTool<TArgs>, IBackgroundHandle/BackgroundHandle, IBackgroundTaskRegistry/InMemoryBackgroundTaskRegistry, TaskOutputTool/TaskStopTool, and EngineBuilder.WithBackgroundTasks/WithBackgroundDispatch for opt-in background tool execution end to end. Added: AgentTool, a subagent-spawning IBackgroundTool modeled on Claude Code's own subagent tool, with a per-spawn IEngine factory so each subagent's own audit trail (e.g. a dedicated FileSystemConversationPersistenceTransformer) stays isolated. ShellTool (Bash/PowerShell) supports run_in_background and its cancellation now reliably kills the underlying process on any cancellation, not just its own internal timeout. GrepTool bounds each file read to 3 seconds so a non-regular file (e.g. a FIFO with no writer) can no longer hang a search indefinitely. FileSystemConversationPersistenceTransformer checkpoints every iteration by default now, not just once per turn. CompactionTransformer's boundary stamp additionally carries summarisedMessageIds so a summary can be traced back to the original messages it replaced.