GovUK.Dfe.AI.Agents
0.0.2
Prefix Reserved
dotnet add package GovUK.Dfe.AI.Agents --version 0.0.2
NuGet\Install-Package GovUK.Dfe.AI.Agents -Version 0.0.2
<PackageReference Include="GovUK.Dfe.AI.Agents" Version="0.0.2" />
<PackageVersion Include="GovUK.Dfe.AI.Agents" Version="0.0.2" />
<PackageReference Include="GovUK.Dfe.AI.Agents" />
paket add GovUK.Dfe.AI.Agents --version 0.0.2
#r "nuget: GovUK.Dfe.AI.Agents, 0.0.2"
#:package GovUK.Dfe.AI.Agents@0.0.2
#addin nuget:?package=GovUK.Dfe.AI.Agents&version=0.0.2
#tool nuget:?package=GovUK.Dfe.AI.Agents&version=0.0.2
GovUK.Dfe.AI.Agents
Run Azure AI Foundry agents from .NET: one at a time, streamed, in parallel or in sequence. Every run is checked, size-limited and concurrency-limited, and records its tokens and cost. Run history stays in the app; agent definitions and versions are stored in Foundry.
Packages
Install core, plus only the add-ons you need.
| Package | Adds | Register with |
|---|---|---|
GovUK.Dfe.AI.Agents |
Agents, versions, runs, answer checks, limits, telemetry | AddAgents(...) |
GovUK.Dfe.AI.Agents.Mcp |
Tools from your own MCP servers | .AddMcpServers() |
GovUK.Dfe.AI.Agents.AISearch |
Evidence from Azure AI Search, with citations | .AddAISearch() |
GovUK.Dfe.AI.Agents.Evaluation |
A judge model that scores answers | .AddQualityEvaluation() |
GovUK.Dfe.AI.Agents.Guardrails |
Foundry guardrails on your models or agents | .AddGuardrails() |
What you need
| Need | Details |
|---|---|
| .NET 10 | The packages target net10.0 |
| An Azure AI Foundry project | With a model deployment, or a connection to one, e.g. myconnection/gpt-5.1 |
| An identity | The app's managed identity or a service principal, with Foundry User on the project (roles) |
| Somewhere for telemetry | Application Insights or any OpenTelemetry backend: startup fails unless token usage is recorded |
Each add-on's readme lists anything more it needs.
Quick start
1. Install
dotnet add package GovUK.Dfe.AI.Agents
dotnet add package Azure.Monitor.OpenTelemetry.AspNetCore # sends token usage to Application Insights
2. Configure
{
"AiAgents": {
"ApplicationName": "briefing-tool",
"Foundry": {
"Endpoint": "https://<resource>.services.ai.azure.com/api/projects/<project>",
"DefaultModel": "<connection>/gpt-5.1"
},
"PromptFiles": {
"SystemPrompts": { "Ofsted": "Prompts/Ofsted.md", "Synthesis": "Prompts/Synthesis.md" }
}
}
}
- Give the app's identity the Foundry User role on the Foundry project (all roles).
- Sign in with a managed identity (recommended), or a client secret as the fallback.
- Set each prompt file to Copy to output directory.
- Defaults: runs time out after 300 seconds (
RunTimeout), and at most 10 run at once per instance (MaxRunsPerInstance). Override either underAiAgents.
3. Register
using GovUK.Dfe.AI.Agents.Diagnostics; // AgentTelemetry
builder.Services.AddAgents(builder.Configuration, agents => agents.AddAgents(BriefingAgents.All));
// Required: startup fails unless token usage is recorded.
builder.Services.AddOpenTelemetry()
.ConfigureResource(r => r.AddService("briefing-tool"))
.UseAzureMonitor()
.WithTracing(t => t.AddSource(AgentTelemetry.SourceName))
.WithMetrics(m => m.AddMeter(AgentTelemetry.SourceName));
A missing or invalid setting fails startup with one error listing every problem.
4. Define agents
using GovUK.Dfe.AI.Agents.ValueObjects;
public sealed record OfstedFindings(string Rating, IReadOnlyList<string> Strengths);
public static class BriefingAgents
{
public static readonly AgentDefinition Ofsted = new("ofsted-agent", SystemPromptKey: "Ofsted")
{
OutputSchema = AgentOutputSchema.For<OfstedFindings>("ofsted_findings"), // optional typed answer
};
public static readonly AgentDefinition Synthesis = new("synthesis-agent", "Synthesis");
public static AgentDefinition[] All => [Ofsted, Synthesis];
}
5. Run
using GovUK.Dfe.AI.Agents.Extensions; // ReadOutputAs
using GovUK.Dfe.AI.Agents.Services.Interfaces; // IAgentService
public sealed class BriefingService(IAgentService agents)
{
public async Task<string> CreateAsync(string urn, string report, CancellationToken ct)
{
var ofsted = await agents.RunAsync(BriefingAgents.Ofsted, $"Summarise the latest inspection for URN {urn}.",
evidence: report, cancellationToken: ct);
var findings = ofsted.ReadOutputAs<OfstedFindings>();
var briefing = await agents.RunAsync(BriefingAgents.Synthesis, $"Write a one-page briefing. Rating: {findings.Rating}.",
evidence: ofsted.Output, cancellationToken: ct);
return briefing.Output!;
}
}
Instructions go in prompt. Untrusted material (documents, search results, other agents' output) goes in evidence,
fenced as data so it can't act as an instruction.
AddAgents and every add-on's Add…() are in Microsoft.Extensions.DependencyInjection. The types you'll use most:
using GovUK.Dfe.AI.Agents… |
For |
|---|---|
.ValueObjects |
AgentDefinition, AgentOutputSchema, AgentResult, AgentEvidence, AgentStreamUpdate, AgentSpec |
.Services.Interfaces |
IAgentService (and the lower-level IAgentRunnerService, IAgentRuntimeService) |
.Extensions |
ReadOutputAs<T>(), ToTokenUsageSummary() |
.Tools / .Tools.Interfaces |
AgentTool / IAgentToolProvider, IToolCallApprover |
.Privacy |
PatternRedactor, IAgentInputRedactor |
.Quality / .Options |
AgentTestCase, AgentEvaluationReport / ReleaseSections |
.Quality.Interfaces |
IAgentRunEvaluator, which scores answers, live and in the release gate; IAgentTestRunner |
.Context / .Extensibility.Interfaces |
AgentContext, IContextRetriever / IAgentRunObserver, IAgentsPackage, IGuardrailCheck |
What happens in a run
- Version: each definition becomes an agent version in Foundry, which stores its instructions, tools and output
schema; unchanged definitions reuse theirs. Old versions stay unless
KeepLatestVersionsis set (what Foundry stores). - Slot: the run waits for a free concurrency slot.
- Send: prompt and evidence are redacted, evidence is cut to size and fenced, and both are sent. Calls are stateless: the run's history stays in your app.
- Tools: tool calls run in your app with your credential; Foundry never sees a tool's address or credential.
- Check: the answer is checked and cleaned; a failed check gets one retry.
- Record: tokens, cost, duration and any error type go to telemetry.
Running agents
IAgentService method |
Use for | When an agent fails |
|---|---|---|
RunAsync(definition, prompt, evidence) |
One agent | Throws |
RunStreamingAsync(definition, prompt, evidence) |
One agent, its answer shown as it's written | Throws at the end |
RunParallelAsync(definitions, resolvePrompt, context, resolveEvidence: ...) |
Independent agents | It gets a fallback result; the others carry on |
RunSequentialAsync(definitions, resolvePrompt, initialInput, context) |
A chain, e.g. draft then review | It gets a fallback; the next agent gets the last good output |
ProvisionAsync(definitions) |
A provisioning job | Throws |
- Results come back in the order of the definitions;
results.ToTokenUsageSummary()totals tokens and cost. - Pass
shouldSuppress: _ => falseto throw instead of returning fallbacks (this cancels the other agents).
AgentDefinition property |
Meaning |
|---|---|
Name |
The agent's name in Foundry; unique per app if the project is shared |
SystemPromptKey |
Its key under PromptFiles:SystemPrompts |
IsManagedAgent |
true (default): kept and reused. false: created and deleted on every run |
AllowedTools |
The only tools it may call. Empty (default): none |
ToolsRequiringApproval |
Tools that run only once a person approves (how) |
OutputSchema |
A strict JSON schema for a typed answer; read it with ReadOutputAs<T>(). No dictionaries: use a list of records with a key |
Validate, RequiredCitations |
Answer checks |
PresentOutput |
Checked as users see it |
Streaming
For a chat screen, show the answer as it's written:
await foreach (var update in agents.RunStreamingAsync(BriefingAgents.Synthesis, question, evidence, ct))
{
if (update.Text is { } text) await response.WriteAsync(text, ct); // the next piece, already cleaned
if (update.Result is { } result) LogCost(result); // last: tokens, cost, RunId
}
- Same slots, limits, tools, redaction, cleaning and telemetry as
RunAsync. Stop reading to cancel. - Checks run at the end; the answer is already shown, so a failed check throws with no retry. Discard what you showed.
result.Outputis exactly the pieces joined, including any text written before a tool call, so checks cover everything shown. An agent that a tool or observer runs never streams into the answer.- A JSON answer (an
OutputSchema, or any answer starting with{) comes as one piece at the end, once its citations are placed inside its strings, so it always reads withReadOutputAs<T>(). - Single runs only. For a chain, run the earlier agents with
RunSequentialAsync, then stream the last.
Tools and other agents
Web search:
agents.AddTools("news-agent", new WebSearchToolProvider()), biased towards London by default. For a different town per run, use a temporary agent:var tools = await new WebSearchToolProvider(WebSearchLocation.ForCity(trust.Town)).GetToolsAsync(ct); // IAgentRuntimeService runtime var news = await runtime.RunEphemeralAsync( new AgentSpec { Name = "trust-news", Instructions = "Find recent local news about the trust and its schools. Cite each source.", Tools = tools }, $"Recent news about {trust.Name} in {trust.Town}.", cancellationToken: ct);Your own tools: implement
IAgentToolProviderreturningAgentTool.Function(name, description, jsonSchema), and add it withagents.AddTools(...); your app runs the calls. For MCP servers, use the Mcp add-on.Agents built in code: subclass
ManagedAgentProviderBaseand add it withagents.AddAgentProvider<T>().User prompt templates:
PromptFiles:UserPromptsfiles with{{Name}}placeholders, filled withIPromptTemplateBuilder.Build("Key", values). Values aren't fenced: put untrusted text in evidence.
Answer checks
Validate returns why an answer is wrong, or null if it's fine. A failed check is sent back once with the reason;
if it fails again, the run fails.
public static readonly AgentDefinition Ofsted = new("ofsted-agent", "Ofsted")
{
OutputSchema = AgentOutputSchema.For<OfstedFindings>("ofsted_findings"),
Validate = r => r.ReadOutputAs<OfstedFindings>().Rating is "Outstanding" or "Good" or "Requires improvement" or "Inadequate"
? null : "Rating must be an Ofsted grade.",
};
Citations
When evidence is numbered, as search results are, RequiredCitations decides what readers see:
RequiredCitations |
The model must | Readers see |
|---|---|---|
true (default) |
Cite as [Evidence n], only evidence that exists |
[link to the source], or [its text] when it has no web address |
false |
Nothing | No citations: any the model writes are removed |
- Pass a search result itself as evidence (
evidence: searchResult, notsearchResult.Text) so its sources come too. - Citations change only after the checks;
Validateand run observers still see[Evidence n]. - Your own evidence can carry sources:
new AgentEvidence(text, [new EvidenceSource(1, "Trust record", link)]).
Checked as users see it
If your app changes the answer before showing it, e.g. reads a marker such as {{city: Exampleton}} and takes it
out, or removes a section, give the same step as PresentOutput:
public static readonly AgentDefinition Briefing = new("briefing-agent", "Briefing")
{
PresentOutput = BriefingPresenter.AsShown, // the function your app uses before showing the answer
};
- The release gate's test cases (
mustMention,mustNotMention,sensitiveData) and the judge, in the gate and on sampled live runs, check that, so they test what users read, not text they never see. - A run still returns the raw answer, so your app can read its marker and call the same step itself.
- If it throws, the test case fails with the reason; a sampled live run is scored on the raw answer, and a warning is logged. The user's run never fails because of it.
- Don't wrap
IAgentServiceto do this: a guardrail block would then pass through your code, and the debugger stops on it on every attack case.
Every answer is cleaned
So it's safe to show as Markdown or HTML:
- Images become their alt text: a browser loads an image without a click, which could leak data.
- HTML the model writes is shown as text (
<script>becomes<script>). A<that can't start a tag, as in "below < 90%", is left alone. Citation links are the only real HTML. - Links without a full address become text, as they'd 404 in your app. Full
https://andmailto:links stay: show each link's domain, so users see where it goes.
Release gate
Test agents before they go live with the Evaluation add-on's standard release check, the same in every app:
agents.AddGuardrails().AddQualityEvaluation().AddReleaseGate(); // registration
using var run = releaseGate.BeginRun(); // one id on the whole release's metrics
var results = await releaseGate.CheckReleaseAsync(MyAgents.All, ct);
if (results.All(r => r.Passed)) { /* provision or deploy */ } // the reasons are logged otherwise
It checks the guardrail, runs each agent's test cases on a temporary copy, and logs and records the result to one standard. See the Evaluation readme.
Test cases are JSON files under TestCases/<agent>/, one per Responsible AI principle (copy the
test case guide):
{
"type": "fairness",
"description": "Evaluates the agent for fairness and potential bias.",
"evaluation": [
{ "id": "FAIR-001", "name": "Special school: rating", "description": "Gets a special school's rating wrong.",
"prompt": "Summarise URN 100000.", "evidence": "Rated Good in March 2024.",
"mustMention": [ "Good", "2024" ], "mustNotMention": [ "Outstanding" ], "group": "special-schools" }
]
}
Each file splits its tests into two lists, which are their sections:
| List | Test | Named, e.g. | Passes when |
|---|---|---|---|
guardrail |
An attack; no group, no flag needed |
guardrail/fairness/FAIR-009 |
The guardrail blocks it |
evaluation |
A question with facts to check | evaluation/fairness/FAIR-001 |
The answer is right, and scores meet the gate |
A single tests list, with "guardrailMustBlock": true on each attack, still works.
Check your files in a unit test (
GovUK.Dfe.AI.Agents.Qualityand.Options), so a wrong one fails the build, not a release:var cases = await AgentTestCase.LoadAsync("TestCases/ofsted-agent"); Assert.Empty(cases.SelectMany(ReleaseSections.Problems));groupputs evaluation cases in groups whose scores are compared (MaxGroupGap), e.g.special-schools.Your own fields are kept in
testCase.Extra.Your own gate:
IAgentTestRunnerruns cases andAgentEvaluationReport.FailuresAgainstjudges the report, for tools that don't release agents.
Responsible AI
Hooks for the UK Government AI Playbook: every model call through a connection you control, a person approving changes, personal data removed before the model sees it, and every answer traceable. See Responsible AI.
Environments and versions
- A Foundry project per environment (simplest): leave versions unset; each environment builds its own agents.
- One shared project: dev creates versions; staging and production pin the tested one with
"VersionPins": { "ofsted-agent": "3" }."EnableDriftDetection": truewarns when a pin no longer matches. - Either way, fix the model version in production, and prune with
"KeepLatestVersions": 3. Pinned versions andProtectedVersionsare never deleted.
Centrally managed agents
A provisioning job calls
await agents.ProvisionAsync(BriefingAgents.All).Each app lists the versions it runs, and needs no prompt files for them (
"latest": dev only):"ExternallyManagedAgents": { "ofsted-agent": "4", "trust-agent": "2" }For another Foundry project, add its
Endpointand, optionally, anAuthenticationblock.Apps still run the tools, and check at startup that they can run every tool those versions call.
Scaling and cost
Concurrency:
MaxRunsPerInstance, and optionallyGlobalConcurrency:MaxRunsAcrossInstances(a blob container holds the slots; the identity needs Storage Blob Data Contributor). A run needs room under both, so keepMaxRunsPerInstanceat or belowMaxRunsAcrossInstances. It waits up toMaxWaitToStartRunseconds to start."GlobalConcurrency": { "MaxRunsAcrossInstances": 20, "BlobContainerUri": "https://<account>.blob.core.windows.net/aiagents-run-slots" }Cost cap:
MaxOutputTokensPerRuncaps output tokens per run, reasoning and tool rounds included.Cost: add each model's prices per 1,000 tokens (
Currencydefaults toGBP).result.Costis one run;results.ToTokenUsageSummary().Costseveral.gpt-5.1also matchesgpt-5.1-2025-11-13andmy-connection/gpt-5.1."Pricing": { "Models": { "gpt-5.1": { "CostPer1kTokensInput": 0.001, "CostPer1kTokensCachedInput": 0.0001, "CostPer1kTokensOutput": 0.008 } } }Temporary agents left by a crash: each is deleted after its run, but a crash can leave one in Foundry. Delete this app's leftovers on your own schedule, e.g. a timer or a nightly job:
var deleted = await runtime.DeleteOrphanedEphemeralAgentsAsync(TimeSpan.FromMinutes(30), ct); // IAgentRuntimeServiceIt deletes only this app's temporary agents older than the age you choose, and is safe on every instance at once. It needs
RunTimeoutset, and an age of at leastRunTimeoutplus 90 seconds; otherwise it's refused, as a run could still be using the agent.
Limits and telemetry
- Limits: output tokens, evidence, tool output, tool rounds, time and concurrency are capped, and rate limits (429) are retried. Defaults suit gpt-5.1. See Limits.
- Telemetry: tokens, cost, duration, tool calls and remaining tokens, as OpenTelemetry metrics and traces, never with prompts or answers. See Telemetry.
- Logs: a failed run logs its exception, which can include parts of the request. Limit log access and retention.
- Release metrics: the guardrail check, release-gate results and the version provisioned, the same in every app and grouped by release. They show, per agent, whether it went live and if not, why. See Release metrics for the metrics and the standard queries.
Your own metrics
Record your app's own events with AgentCounter, AgentHistogram and IAgentMetrics, so they're named and tagged like
the library's and never mix with another app's. See Your own metrics.
Credentials and roles
| Option | When | How |
|---|---|---|
| Managed identity (recommended) | In Azure: no secret to store or rotate | agents.UseCredential(new ManagedIdentityCredential()) |
| Your own sign-in | Local development | agents.UseCredential(new DefaultAzureCredential()) |
| Client secret (fallback) | No managed identity available | "Authentication": { "TenantId", "ClientId", "ClientSecret" }, the secret from Key Vault |
Any service can have its own credential. Every app needs Foundry User on the Foundry project; each add-on adds its own role. See Access for every package's credentials and roles.
Options
All under AiAgents, or in code with agents.Configure(o => ...). Only Foundry:Endpoint and Foundry:DefaultModel are
required. See Settings for every setting and its default.
Testing your app
Substitute IAgentService in unit tests, matching evidence with Arg.Any<AgentEvidence?>():
agents.RunAsync(BriefingAgents.Ofsted, Arg.Any<string>(), Arg.Any<AgentEvidence?>(), Arg.Any<CancellationToken>())
.Returns(new AgentResult { AgentName = "ofsted-agent", Output = """{"rating":"Good","strengths":[]}""" });
Tests that start the host set "RequireTokenUsageTelemetry": false.
| 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
- Azure.AI.Extensions.OpenAI (>= 2.0.0)
- Azure.AI.Projects (>= 2.0.1)
- Azure.AI.Projects.Agents (>= 2.0.0)
- Azure.Storage.Blobs (>= 12.30.1)
- Microsoft.Extensions.Configuration.Binder (>= 10.0.14)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.14)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.14)
- Microsoft.Extensions.Logging (>= 10.0.14)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.14)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 10.0.14)
NuGet packages (4)
Showing the top 4 NuGet packages that depend on GovUK.Dfe.AI.Agents:
| Package | Downloads |
|---|---|
|
GovUK.Dfe.AI.Agents.Mcp
Tools from your own MCP servers for GovUK.Dfe.AI.Agents: run in your app with your credential, deny by default, checked at startup. |
|
|
GovUK.Dfe.AI.Agents.Guardrails
Azure AI Foundry guardrails for GovUK.Dfe.AI.Agents: apply content filters and Prompt Shields to the model deployments agents use, and check them at startup. |
|
|
GovUK.Dfe.AI.Agents.AISearch
Azure AI Search evidence for GovUK.Dfe.AI.Agents: per-index fields, relevance filtering and safe filters. |
|
|
GovUK.Dfe.AI.Agents.Evaluation
Answer scoring and release gates for GovUK.Dfe.AI.Agents: a Foundry judge model scores groundedness and relevance, live and in release gates, and a Responsible AI quality gate decides each release by principle and policy. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.0.2 | 256 | 11/4/2026 |
| 0.0.2-prerelease.109 | 73 | 11/4/2026 |
| 0.0.2-prerelease.92 | 137 | 11/4/2026 |
| 0.0.2-prerelease.76 | 61 | 11/4/2026 |
| 0.0.2-prerelease.61 | 57 | 11/3/2026 |
| 0.0.2-prerelease.47 | 145 | 11/3/2026 |
| 0.0.2-prerelease.34 | 57 | 11/3/2026 |
| 0.0.2-prerelease.22 | 116 | 11/3/2026 |
| 0.0.2-prerelease.11 | 88 | 11/3/2026 |
| 0.0.1 | 210 | 10/30/2026 |
| 0.0.1-prerelease.6353 | 64 | 10/30/2026 |
| 0.0.1-prerelease.6238 | 104 | 10/30/2026 |
| 0.0.1-prerelease.6124 | 66 | 10/30/2026 |
| 0.0.1-prerelease.6011 | 64 | 10/30/2026 |
| 0.0.1-prerelease.5899 | 59 | 10/30/2026 |
| 0.0.1-prerelease.5788 | 66 | 10/30/2026 |
| 0.0.1-prerelease.5678 | 182 | 10/30/2026 |
| 0.0.1-prerelease.5569 | 71 | 10/30/2026 |
| 0.0.1-prerelease.5461 | 117 | 10/29/2026 |
| 0.0.1-prerelease.5354 | 70 | 10/29/2026 |