// SPDX-License-Identifier: MIT
// Copyright (c) 2026 AgentEval Contributors
using AgentEval.MAF.Gatekeeper;
using AgentEval.MAF.Gatekeeper.Memory;
using AgentEval.Testing;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
namespace AgentEval.Samples;
/// Offline dynamic-tool inventory and context-provider boundary demonstration.
public static class GatekeeperDynamicContextProviderBoundary
{
public static async Task RunAsync()
{
GatekeeperSampleContractRenderer.Print("24");
Console.WriteLine("\n=== Gatekeeper — Dynamic Context Provider Boundary (offline) ===\n");
var weather = Function("weather");
var unsupportedMemoryTool = Function("remember_unknown");
var inner = new DynamicToolProvider(weather, unsupportedMemoryTool);
var events = new EventSink();
var gatedProvider = new GatedAIContextProvider(
inner,
Pipeline(),
new MemoryToolOperationRegistry([]),
new ContextAdapter(),
eventSink: events,
options: new MemoryContextProviderOptions(
recallFailureMode: MemoryContextProviderFailureMode.ContinueWithoutProviderData));
var agent = new ChatClientAgent(
new ScriptedChatClient().AddText("ok"),
new ChatClientAgentOptions
{
Name = "dynamic-provider-sample",
ChatOptions = new ChatOptions { MaxOutputTokens = 64 },
AIContextProviders = [gatedProvider],
});
var report = GatekeeperCoverageAnalyzer.Analyze(agent);
Require(!report.ToolInventoryAvailable,
"a dynamic-only provider must not receive vacuous 100% static coverage");
RequireThrows(
() => GatekeeperCoverageAnalyzer.AnalyzeOrThrow(agent),
"promotion must fail closed when the runtime tool inventory is unavailable");
#pragma warning disable MAAI001 // Sample exercises the current MAF AIContextProvider lifecycle deliberately.
var context = await gatedProvider.InvokingAsync(
new AIContextProvider.InvokingContext(agent, session: null, new AIContext()));
#pragma warning restore MAAI001
var admitted = context.Tools?.Select(tool => tool.Name).ToArray() ?? [];
Require(admitted.SequenceEqual(["weather"], StringComparer.Ordinal),
"the real provider boundary must exclude only the unsupported dynamic memory tool");
Require(events.Events is [{ Kind: MemoryContextProviderEventKind.DynamicToolExcluded }],
"dynamic exclusion must emit one privacy-minimized event");
Require(gatedProvider.Coverage == MemoryCoverageLevel.Boundary,
"the provider must report boundary coverage, not fabricated static tool coverage");
Console.WriteLine(" static analyzer: inventory unavailable → promotion refused");
Console.WriteLine(" provider boundary: weather admitted; unsupported memory tool excluded");
Console.WriteLine(" coverage claim: boundary only, with one content-free exclusion event");
Console.WriteLine(" ✅ runtime-contributed tools were handled at their real provider seam.");
}
private static MemoryGatePipeline Pipeline() => new(
[],
capabilities: null,
new MemorySecurityPolicy(
"dynamic-provider-sample",
"1",
MemorySecurityProfile.Enforce,
MemoryGateAction.Reject,
MemoryCoverageLevel.Boundary),
TimeProvider.System);
private static AIFunction Function(string name) => AIFunctionFactory.Create(() => "ok", name);
private static void RequireThrows(Action action, string message)
where TException : Exception
{
try
{
action();
}
catch (TException)
{
return;
}
throw new InvalidOperationException("Dynamic-provider sample failed: " + message + ".");
}
private static void Require(bool condition, string message)
{
if (!condition)
{
throw new InvalidOperationException("Dynamic-provider sample failed: " + message + ".");
}
}
private sealed class DynamicToolProvider(params AITool[] tools) : AIContextProvider
{
private readonly AIContext _context = new() { Tools = tools };
protected override ValueTask ProvideAIContextAsync(
InvokingContext context,
CancellationToken cancellationToken = default) => ValueTask.FromResult(_context);
protected override ValueTask StoreAIContextAsync(
InvokedContext context,
CancellationToken cancellationToken = default) => default;
}
private sealed class ContextAdapter : IMemoryContextProviderAdapter
{
private int _operationId;
public MemoryGateContext CreateRecallInstructionsContext(
AIContextProvider.InvokingContext context,
string instructions) => Create(MemoryGateStage.AfterRead, instructions);
public MemoryGateContext CreateRecallMessageContext(
AIContextProvider.InvokingContext context,
ChatMessage message,
int ordinal) => Create(MemoryGateStage.AfterRead, message.Text);
public MemoryGateContext CreateWriteMessageContext(
AIContextProvider.InvokedContext context,
ChatMessage message,
MemoryContextMessageOrigin origin,
int ordinal) => Create(MemoryGateStage.BeforeWrite, message.Text);
public ChatMessage ApplySanitizedMessage(ChatMessage message, string sanitizedContent)
{
var clone = message.Clone();
clone.Contents = [new TextContent(sanitizedContent)];
clone.RawRepresentation = null;
return clone;
}
private MemoryGateContext Create(MemoryGateStage stage, string? content) => new(
$"dynamic-provider-{Interlocked.Increment(ref _operationId)}",
stage,
new MemoryOperationContract(
stage == MemoryGateStage.BeforeWrite ? "context_write" : "context_recall",
stage == MemoryGateStage.BeforeWrite ? MemoryOperationKind.Write : MemoryOperationKind.Recall,
MemorySurface.AIContextProvider,
stage == MemoryGateStage.BeforeWrite ? ["content"] : [],
[],
MemoryCategory.Message,
isSideEffecting: stage == MemoryGateStage.BeforeWrite,
mayReturnSensitiveContent: stage != MemoryGateStage.BeforeWrite),
"context-provider",
new MemorySecurityScope("tenant-a", "user-a"),
new MemoryProvenance(MemorySourceKind.ContextProvider, "provider", MemoryTrustLevel.Medium),
content,
budget: new MemoryBudgetSnapshot());
}
private sealed class EventSink : IMemoryContextProviderEventSink
{
public List Events { get; } = [];
public ValueTask RecordAsync(
MemoryContextProviderEvent @event,
CancellationToken cancellationToken = default)
{
Events.Add(@event);
return default;
}
}
}