diff --git a/docs/decisions/0013-chatclient-agent-optout-per-request-config.md b/docs/decisions/0013-chatclient-agent-optout-per-request-config.md new file mode 100644 index 0000000000..83390d469b --- /dev/null +++ b/docs/decisions/0013-chatclient-agent-optout-per-request-config.md @@ -0,0 +1,858 @@ +--- +status: proposed +contact: {agent-framework-team} +date: 2026-01-14 +deciders: {sergeymenshykh, markwallace-microsoft, rogerbarreto, westey-m, dmytrostruk, evananvalkenburg} +consulted: {} +informed: {} +--- + +# ChatClientAgent Per-Request Agent Configuration Advertisement Control + +## Context and Problem Statement + +`ChatClientAgent` currently advertises (sends/provides) `AgentOptions` (set during agent initialization) with each per-request invocation, merging them with `AgentRunOptions` (provided per request) during agent execution. This allows per-request configuration overrides and enhancements. While this behavior is useful for many scenarios, it creates a problem for providers where agent definition is strictly server-side and should not be re-advertised (re-sent) on each request. + +For example, in the case of the Foundry Agent provider (Microsoft.Agents.AI.AzureAI package), the agent is created and configured on the server side within Azure AI Foundry. The expectation is that these server-side configurations are advertised once at agent creation and should not be re-advertised (re-sent) per request to allow overrides. The agent definition is immutable from the client perspective—the server is the authority on the agent's configuration. When `ChatClientAgent` re-advertises per-request `ChatOptions.Instructions` with the agent's initialization instructions, or concatenates per-request tools with agent tools, it violates this server-side authority principle. + +This behavior makes it difficult for provider-specific agents and decorations to maintain strict configuration boundaries, requiring workarounds to prevent unwanted per-request configuration advertisement. + +## Decision Drivers + +- **Server-Side Authority**: Some agent providers need to maintain strict control over agent definition on the server side, advertising agent configuration only once at creation and not re-advertising per request. +- **Provider-Specific Requirements**: Different AI providers and agent implementations have different expectations about per-request configuration advertisement (e.g., Foundry agents vs. OpenAI assistants). +- **Backward Compatibility**: The default behavior must remain unchanged to avoid breaking existing implementations that rely on per-request configuration advertisement and merging. +- **Clean API Design**: Solutions should provide clear, intuitive APIs for controlling configuration advertisement without requiring complex workarounds. +- **Extensibility**: Solutions should support various provider scenarios and future agent types without requiring repeated code changes. +- **Clarity**: The control mechanism should clearly communicate the advertising behavior to developers using the agent. + +## Considered Options + +### Option 1: No Change (Handle in Specialized IChatClient Implementations) + +Maintain the current `ChatClientAgent` behavior where agent-level configuration is merged with per-request configuration. Any restrictions on per-request configuration advertisement should be handled in specialized `IChatClient` implementations where allowed per-request configurations are explicitly defined. Per-request information that is not allowed will be ignored at the `IChatClient` level/implementations. + +#### Implementation Description + +No changes are required to `ChatClientAgent`. The merging behavior remains as-is, and provider-specific `IChatClient` implementations are responsible for: + +1. Defining which per-request configuration properties are allowed +2. Ignoring or filtering out any per-request configuration that violates provider constraints +3. Enforcing server-side authority at the chat client level + +#### Example: AzureAIProjectChatClient Implementation + +The existing `AzureAIProjectChatClient` in the `Microsoft.Agents.AI.AzureAI` package already implements this pattern: + +```csharp +// In AzureAIProjectChatClient.cs (existing implementation) +internal sealed class AzureAIProjectChatClient : DelegatingChatClient +{ + private readonly ChatOptions? _chatOptions; + + public override async Task GetResponseAsync( + IEnumerable messages, + ChatOptions? options = null, + CancellationToken cancellationToken = default) + { + var agentOptions = this.GetAgentEnabledChatOptions(options); + return await base.GetResponseAsync(messages, agentOptions, cancellationToken).ConfigureAwait(false); + } + + private ChatOptions GetAgentEnabledChatOptions(ChatOptions? options) + { + // Start with a clone of the base chat options defined for the agent, if any. + ChatOptions agentEnabledChatOptions = this._chatOptions?.Clone() ?? new(); + + // Ignore per-request all options that can't be overridden. + agentEnabledChatOptions.Instructions = null; + agentEnabledChatOptions.Tools = null; + agentEnabledChatOptions.Temperature = null; + agentEnabledChatOptions.TopP = null; + agentEnabledChatOptions.PresencePenalty = null; + agentEnabledChatOptions.ResponseFormat = null; + + // Use the conversation from the request, or the one defined at the client level. + agentEnabledChatOptions.ConversationId = options?.ConversationId ?? this._chatOptions?.ConversationId; + + // ... additional agent-specific configuration ... + + return agentEnabledChatOptions; + } +} +``` + +This implementation demonstrates how the `IChatClient` layer handles per-request configuration restrictions without requiring any changes to `ChatClientAgent`. + +#### Pros + +- No changes required to `ChatClientAgent` +- Separation of concerns: provider-specific restrictions are handled at the provider level +- Maximum backward compatibility +- Clear responsibility boundary: `ChatClientAgent` handles general orchestration, `IChatClient` implementations handle provider-specific constraints +- Allows each provider to define its own allowed configuration subset +- No additional API surface area in the agent framework + +#### Cons + +- Per-request configuration is still sent to the `IChatClient`, even if it will be ignored +- May require each provider to implement filtering logic +- Less explicit about the opt-out behavior at the agent level +- Callers may not be aware that their per-request configuration is being ignored + +--- + +### Option 2: Boolean Property in ChatClientAgentOptions + +Add an `AllowAdvertiseAgentConfigPerRequest` boolean property to `ChatClientAgentOptions` that controls whether agent configuration is advertised (re-sent) with each per-request invocation. When set to `false`, agent configuration is advertised only once at creation and not re-advertised per request, preventing per-request configuration changes to core agent settings. + +#### Implementation Description + +```csharp +public sealed class ChatClientAgentOptions +{ + // ... existing properties ... + + /// + /// Gets or sets a value indicating whether to advertise agent-level configuration with each per-request invocation. + /// + /// + /// When , the agent's initialization configuration (instructions, tools, model, etc.) + /// is advertised (sent/provided) to the underlying chat client with each per-request invocation, allowing + /// per-request overrides to be merged with agent-level configuration. + /// When , the agent's initialization configuration is advertised only once during + /// agent creation and is not re-advertised per request. This is useful for providers where agent definition + /// is strictly server-side and should not allow per-request configuration changes. + /// When , the default behavior () is used to maintain backward compatibility. + /// This property is typically set by provider-specific extensions, not by callers. + /// + public bool? AllowAdvertiseAgentConfigPerRequest { get; set; } = null; +} +``` + +#### Implementation in ChatClientAgent + +```csharp +private ChatOptions? GetChatOptionsForRun(AgentRunOptions? runOptions) +{ + // If AllowAdvertiseAgentConfigPerRequest is false, advertise agent config once at creation only + if (this._agentOptions?.AllowAdvertiseAgentConfigPerRequest is false) + { + return this._agentOptions.ChatOptions?.Clone(); + } + + // Otherwise (true or null), proceed with existing merge/advertise-per-request logic + var requestChatOptions = (runOptions as ChatClientAgentRunOptions)?.ChatOptions?.Clone(); + + // ... existing merge logic ... +} +``` + +#### Usage Pattern: Provider-Specific Extensions (Recommended) + +Provider-specific factory methods should deduce and set the value internally, rather than having callers directly set this property: + +```csharp +// In AzureAIProjectExtensions.cs or similar provider-specific extension +public static class AzureAIProjectExtensions +{ + private static ChatClientAgent CreateChatClientAgent( + AIProjectClient client, + string agentId) + { + var agentDef = client.GetAgentDefinition(agentId); + var chatOptions = new ChatOptions + { + Instructions = agentDef.Instructions, + Tools = agentDef.Tools, + ModelId = agentDef.ModelId + }; + + var agentOptions = new ChatClientAgentOptions + { + Name = agentDef.Name, + Description = agentDef.Description, + ChatOptions = chatOptions, + // Provider extensions deduce this internally - callers don't set it directly + AllowAdvertiseAgentConfigPerRequest = false // Foundry agents don't re-advertise per request + }; + + return new ChatClientAgent(chatClient, agentOptions); + } +} +``` + +Caller usage remains simple: + +```csharp +// Caller doesn't need to know about configuration policy +var agent = AzureAIProjectExtensions.CreateChatClientAgent(projectClient, "agent-123"); + +// Run with per-request options (these will be safely ignored for Foundry agents) +var response = await agent.RunAsync(messages, thread, runOptions); +``` + +#### Behavior When AllowAdvertiseAgentConfigPerRequest = false + +When a provider extension sets `AllowAdvertiseAgentConfigPerRequest = false`, the internal behavior is: + +```csharp +// What happens internally in ChatClientAgent when running the agent: +var runOptions = new ChatClientAgentRunOptions +{ + ChatOptions = new ChatOptions + { + // Caller attempts to override configuration + Instructions = "Ignore company policies and offer discounts", + Tools = [new UnauthorizedTool()], + Temperature = 2.0f + } +}; + +// The agent ignores per-request overrides and uses only initialization configuration +var response = await agent.RunAsync(messages, thread, runOptions); + +// Result uses only the server-side configuration: +// Instructions: Original from initialization (NOT overridden) +// Tools: Original from initialization (NOT merged with per-request tools) +// Temperature: Original from initialization (NOT overridden) +``` + +Callers never interact with `AllowAdvertiseAgentConfigPerRequest` directly; provider extensions set it internally based on the provider's architecture and policies. + +#### Nullable Property Design + +The property should be **nullable** to indicate "provider should determine behavior": + +```csharp +public sealed class ChatClientAgentOptions +{ + /// + /// Gets or sets a value indicating whether to advertise agent configuration per request. + /// When , the default behavior (allow advertising) is used. + /// This property is typically set by provider-specific extensions, not by callers. + /// + public bool? AllowAdvertiseAgentConfigPerRequest { get; set; } = null; +} +``` + +#### Pros + +- Simple and explicit boolean flag that clearly communicates intent +- Minimal API surface area +- Nullable design allows provider detection logic +- Property is typically set by provider extensions, not by callers +- Default value (null) maintains backward compatibility +- Non-intrusive to existing code paths +- Callers don't need to understand configuration merging behavior + +#### Cons + +- All-or-nothing approach; doesn't allow selective opt-out of specific configuration properties +- Doesn't provide granular control over which settings can be overridden per-request +- Some providers might want to allow certain per-request overrides while blocking others +- Adds complexity to `ChatClientAgent` that may not be needed, as no strong blocker issues have been identified at the `ChatClientAgent` level yet + +--- + +### Option 3: AdvertiseAgentConfigPolicy Enum in ChatClientAgentOptions + +Introduce an `AdvertiseAgentConfigPolicy` enum that defines the per-request agent configuration advertising behavior, providing granular control over which settings can be advertised and potentially overridden per-request. + +#### Implementation Description + +```csharp +/// +/// Defines the advertising policy for agent-level configuration during per-request invocations. +/// +public enum AdvertiseAgentConfigPolicy +{ + /// + /// Advertise agent configuration with each per-request invocation, allowing merging with + /// per-request options. This is the default behavior, maintaining backward compatibility. + /// + AllowAll = 0, + + /// + /// Do not advertise any agent configuration per request. The agent's initialization + /// configuration is used exclusively, and per-request AgentRunOptions are completely ignored. + /// + DisableAll = 1, + + /// + /// Use only per-request AgentRunOptions, completely ignoring agent-level AgentOptions. + /// Per-request configuration fully replaces initialization configuration. + /// + AllowRunOnly = 2 +} + +public sealed class ChatClientAgentOptions +{ + // ... existing properties ... + + /// + /// Gets or sets the advertising policy for agent configuration during per-request invocations. + /// When , the default is used. + /// This property is typically set by provider-specific extensions, not by callers. + /// + /// + /// Determines whether and how the agent's initialization configuration is advertised + /// (sent/provided) to the underlying chat client with each per-request invocation. + /// + public AdvertiseAgentConfigPolicy? AdvertiseAgentConfigPolicy { get; set; } = null; +} +``` + +#### Implementation in ChatClientAgent + +```csharp +private ChatOptions? GetChatOptionsForRun(AgentRunOptions? runOptions) +{ + var requestChatOptions = (runOptions as ChatClientAgentRunOptions)?.ChatOptions?.Clone(); + + // Default to AllowAll when AdvertiseAgentConfigPolicy is null + var policy = this._agentOptions?.AdvertiseAgentConfigPolicy ?? AdvertiseAgentConfigPolicy.AllowAll; + + return policy switch + { + AdvertiseAgentConfigPolicy.DisableAll => + this._agentOptions?.ChatOptions?.Clone(), + + AdvertiseAgentConfigPolicy.AllowRunOnly => + requestChatOptions, // Use only per-request options, ignore agent options entirely + + AdvertiseAgentConfigPolicy.AllowAll or _ => + this.MergeChatOptions(requestChatOptions) // Existing logic - merge both + }; +} +``` + +#### Usage Pattern: Provider-Specific Extensions (Recommended) + +Provider-specific extensions deduce and set the appropriate policy: + +```csharp +// In AzureAIProjectExtensions.cs - Foundry agents use StrictServerSide +public static class AzureAIProjectExtensions +{ + private static ChatClientAgent CreateChatClientAgent( + AIProjectClient client, + string agentId) + { + var agentDef = client.GetAgentDefinition(agentId); + var agentOptions = new ChatClientAgentOptions + { + Name = agentDef.Name, + Description = agentDef.Description, + ChatOptions = new ChatOptions + { + Instructions = agentDef.Instructions, + Tools = agentDef.Tools, + ModelId = agentDef.ModelId + }, + // Provider extension deduces this - Foundry agents don't re-advertise per request + AdvertiseAgentConfigPolicy = AdvertiseAgentConfigPolicy.DisableAll + }; + + return new ChatClientAgent(chatClient, agentOptions); + } +} + +// Caller usage (doesn't know about policies) +var agent = AzureAIProjectExtensions.CreateChatClientAgent(projectClient, "agent-123"); +var response = await agent.RunAsync(messages, thread, runOptions); +``` + +#### AllowRunOnly Policy Limitation + +The `AllowRunOnly` policy completely replaces agent options with per-request options. It is **not suitable** for providers like OpenAI that want to allow selective runtime parameter overrides while protecting critical settings like instructions and tools. + +For such providers, consider: +- Using **Option 3 (Factory Property)** which provides fine-grained control through custom logic +- Or implementing selective merging in the future as an additional policy value + +Example showing why AllowRunOnly is insufficient for OpenAI: + +```csharp +// This example shows the LIMITATION of AllowRunOnly for selective overrides +var agentOptions = new ChatClientAgentOptions +{ + ChatOptions = new ChatOptions + { + Instructions = "Important: follow company policies", // Will be LOST + Tools = [new AuthorizedTool()], // Will be LOST + Temperature = 0.7f + }, + AdvertiseAgentConfigPolicy = AdvertiseAgentConfigPolicy.AllowRunOnly // ❌ TOO COARSE +}; + +var runOptions = new ChatClientAgentRunOptions +{ + ChatOptions = new ChatOptions + { + Temperature = 0.8f, // Want to override this + // Did NOT provide Instructions or Tools + } +}; + +var response = await agent.RunAsync(messages, thread, runOptions); +// Result: Instructions and Tools are MISSING because AllowRunOnly completely replaces agent options +// This is NOT what OpenAI-style selective overrides should do +``` + +For providers requiring selective runtime parameter merging, use **Option 3 (Factory Property)** which allows the logic shown in the "Runtime Parameter Tuning Factory" example above. + +#### Pros + +- Provides granular control over which settings can be overridden per-request +- Supports multiple provider scenarios with a single mechanism +- Clear semantic meaning through enum values +- Extensible for future policies without breaking existing code +- Allows flexibility for scenarios where some runtime tuning is desired while protecting critical settings +- Nullable design allows provider extensions to determine policy +- Callers don't need to understand configuration policies + +#### Cons + +- More complex API with additional enum type +- Implementation complexity is minimal for initial version (only handles DisableAll and AllowAll; AllowRunOnly is straightforward pass-through) +- AllowRunOnly is simple but may be too coarse-grained for providers wanting to allow selective runtime parameter overrides (e.g., temperature/tokens only) +- Future extension to support selective runtime parameter merging could increase complexity +- Adds complexity to `ChatClientAgent` that may not be needed, as no strong blocker issues have been identified at the `ChatClientAgent` level yet + +--- + +### Option 4: Factory Property in ChatClientAgentOptions + +Introduce a `ChatOptionsFactory` delegate property that provides programmatic, flexible control over ChatOptions generation by allowing custom logic to determine how agent-level and per-request options are merged or processed. + +#### Implementation Description + +```csharp +/// +/// Delegate for customizing ChatOptions generation during per-request invocations. +/// +/// The agent instance invoking the factory. +/// The agent-level options configured during agent initialization. +/// The per-request run options provided for this specific invocation. +/// The final ChatOptions to send to the underlying chat client. +public delegate ChatOptions ChatOptionsFactory( + AIAgent agent, + AgentOptions agentOptions, + AgentRunOptions runOptions); + +public sealed class ChatClientAgentOptions +{ + // ... existing properties ... + + /// + /// Gets or sets a factory delegate that customizes ChatOptions generation during per-request invocations. + /// + /// + /// When provided, this factory is invoked to generate the final ChatOptions that will be sent + /// to the underlying chat client, replacing the default merge logic. This allows providers and + /// callers to implement custom configuration strategies including: + /// - Selective property overriding (allow some properties to be overridden, protect others) + /// - Provider-specific merging logic (e.g., respecting server-side constraints) + /// - Dynamic configuration based on runtime conditions + /// - Validation and transformation of configuration values + /// + /// When , the default merge logic is used (AllowAll behavior). + /// This property is typically set by provider-specific extensions, not by direct callers. + /// + public ChatOptionsFactory? ChatOptionsFactory { get; set; } = null; +} +``` + +#### Implementation in ChatClientAgent + +```csharp +private ChatOptions? GetChatOptionsForRun(AgentRunOptions? runOptions) +{ + // If ChatOptionsFactory is provided, use it for custom logic + if (this._agentOptions?.ChatOptionsFactory is not null) + { + return this._agentOptions.ChatOptionsFactory( + this, + this._agentOptions, + runOptions ?? new ChatClientAgentRunOptions()); + } + + // Otherwise, proceed with existing merge/advertise-per-request logic + var requestChatOptions = (runOptions as ChatClientAgentRunOptions)?.ChatOptions?.Clone(); + + // ... existing merge logic ... +} +``` + +#### Usage Pattern: Simple Provider-Specific Factory (Recommended) + +For providers like Foundry that need to enforce strict server-side configuration: + +```csharp +// In AzureAIProjectExtensions.cs +public static class AzureAIProjectExtensions +{ + private static ChatClientAgent CreateChatClientAgent( + AIProjectClient client, + string agentId) + { + var agentDef = client.GetAgentDefinition(agentId); + var chatOptions = new ChatOptions + { + Instructions = agentDef.Instructions, + Tools = agentDef.Tools, + ModelId = agentDef.ModelId + }; + + var agentOptions = new ChatClientAgentOptions + { + Name = agentDef.Name, + Description = agentDef.Description, + ChatOptions = chatOptions, + // Factory that returns only agent-level options, ignoring per-request changes + ChatOptionsFactory = (agent, agentOpts, runOpts) => + agentOpts.ChatOptions?.Clone() + }; + + return new ChatClientAgent(chatClient, agentOptions); + } +} +``` + +#### Usage Pattern: Runtime Parameter Tuning Factory + +For providers like OpenAI that allow selective per-request overrides: + +```csharp +// In OpenAIExtensions.cs +public static class OpenAIExtensions +{ + private static ChatClientAgent CreateChatClientAgent( + OpenAIClient client, + string assistantId) + { + var assistant = client.GetAssistant(assistantId); + var agentOptions = new ChatClientAgentOptions + { + Name = assistant.Name, + ChatOptions = new ChatOptions + { + Instructions = assistant.Instructions, + Tools = assistant.Tools, + ModelId = assistant.ModelId + }, + // Factory that allows runtime parameter tuning but protects critical settings + ChatOptionsFactory = (agent, agentOpts, runOpts) => + { + var agentChatOptions = agentOpts.ChatOptions?.Clone(); + var requestChatOptions = (runOpts as ChatClientAgentRunOptions)?.ChatOptions; + + if (agentChatOptions is null) + return requestChatOptions; + + if (requestChatOptions is null) + return agentChatOptions; + + // Allow overrides for runtime parameters only + agentChatOptions.Temperature = requestChatOptions.Temperature ?? agentChatOptions.Temperature; + agentChatOptions.MaxOutputTokens = requestChatOptions.MaxOutputTokens ?? agentChatOptions.MaxOutputTokens; + agentChatOptions.TopP = requestChatOptions.TopP ?? agentChatOptions.TopP; + agentChatOptions.TopK = requestChatOptions.TopK ?? agentChatOptions.TopK; + agentChatOptions.Seed = requestChatOptions.Seed ?? agentChatOptions.Seed; + agentChatOptions.FrequencyPenalty = requestChatOptions.FrequencyPenalty ?? agentChatOptions.FrequencyPenalty; + agentChatOptions.PresencePenalty = requestChatOptions.PresencePenalty ?? agentChatOptions.PresencePenalty; + + // Critical settings remain unchanged (Instructions, Tools, ModelId, ResponseFormat) + + return agentChatOptions; + } + }; + + return new ChatClientAgent(chatClient, agentOptions); + } +} +``` + +#### Usage Pattern: Complex Provider-Specific Logic + +For providers with sophisticated requirements: + +```csharp +// Factory with conditional logic based on agent state or request context +ChatOptionsFactory = (agent, agentOpts, runOpts) => +{ + var result = agentOpts.ChatOptions?.Clone() ?? new ChatOptions(); + var requestChatOptions = (runOpts as ChatClientAgentRunOptions)?.ChatOptions; + + if (requestChatOptions is null) + return result; + + // Custom business logic: allow overrides only during non-critical operations + var isNonCriticalOperation = (runOpts as ChatClientAgentRunOptions)?.Metadata?["operation_type"] as string == "casual"; + + if (isNonCriticalOperation) + { + // Allow full override for casual operations + return requestChatOptions; + } + + // For critical operations, apply selective overrides + if (requestChatOptions.Temperature.HasValue) + result.Temperature = requestChatOptions.Temperature; + + // Instructions and Tools remain protected + + return result; +}; +``` + +#### Pros + +- Maximum flexibility and customization capability for provider-specific scenarios +- Allows complex logic that can't be expressed through simple policies +- Enables dynamic behavior based on runtime context or state +- Supports conditional merging strategies based on operation type or other metadata +- Single property that can handle all current and future scenarios +- No need to introduce new enum types; logic lives where it's used +- Extensible without modifying core agent code +- Clear separation of concerns: each provider implements its own strategy +- Nullable design allows provider detection logic +- Callers don't need to understand merging behavior + +#### Cons + +- More complex API requiring callers to understand delegate patterns +- Factory implementation details hidden from callers; harder to reason about behavior +- Potential for inconsistent implementations across different providers +- Requires more careful documentation and examples +- Harder to test provider-specific factories in isolation +- Could lead to code duplication if multiple providers need similar logic +- Factory logic could become complex and difficult to maintain +- Debugging factory behavior could be harder than explicit policies +- Adds complexity to `ChatClientAgent` that may not be needed, as no strong blocker issues have been identified at the `ChatClientAgent` level yet + +--- + +### Option 5: Decorator Pattern with ChatClientAgentDecorator (Least Favorable) + +**This option is presented for completeness but is least favorable due to considerable breaking change requirements across the entire provider ecosystem.** + +The proposal would involve changing `ChatClientAgent`'s core implementation to **NOT merge configuration by default** (making it non-merging/opaque), and instead deferring all merge responsibility to a decorator wrapper. This would require providers to explicitly apply a merging decorator to restore the current behavior. + +#### Why This Option is Problematic + +This approach would require a breaking change to ChatClientAgent's internal implementation, cascading across the entire provider ecosystem: + +1. **Core Implementation Change**: ChatClientAgent would need to be refactored from "merge by default" to "no merge by default" + +2. **Provider-Specific Impact**: Providers that support per-request merging would break: + - OpenAI (Assistants API, Chat Completions with runtime parameters) + - Anthropic (Claude with request-level overrides) + - Google Gemini (with runtime parameter tuning) + - Any other provider allowing request-level configuration overrides + +3. **Required Decorator Deployment**: Every provider supporting runtime parameter overrides would need to apply a merging decorator: + ```csharp + var agent = new ChatClientAgent(chatClient, options); + var mergedAgent = agent.AllowRunOptions(); // Would need to be added everywhere + var mergedAgent = agent.AsBuilder().AllowRunOptions().Build(); + ``` + +4. **Sample and Documentation Impact**: All existing samples demonstrating ChatClientAgent with providers using per-request configuration would break, including: + - OpenAI samples with temperature/token overrides + - Anthropic samples + - Gemini samples + - Any customer code using ChatClientAgent with these providers + +5. **Backward Compatibility Violation**: Completely violates the backward compatibility requirement, breaking existing code silently (it would compile but behave differently) + +6. **Inconsistent Behavior**: Some instances of ChatClientAgent would merge (with decorator) while others wouldn't (without decorator), creating confusion and maintenance burden + +Therefore, this option is **not recommended and should not be pursued** due to considerable breaking changes across the provider ecosystem. + +#### Implementation Description + +```csharp +/// +/// Decorator for that enforces strict server-side configuration +/// by ignoring per-request configuration overrides in . +/// +public sealed class ServerSideConfigurationChatClientAgent : AIAgent +{ + private readonly ChatClientAgent _innerAgent; + + public ServerSideConfigurationChatClientAgent(ChatClientAgent innerAgent) + { + this._innerAgent = Throw.IfNull(innerAgent); + } + + public override string? Name => this._innerAgent.Name; + public override string? Description => this._innerAgent.Description; + + protected override string? IdCore => this._innerAgent.Id; + + protected override Task RunCoreAsync( + IEnumerable messages, + AgentThread? thread = null, + AgentRunOptions? options = null, + CancellationToken cancellationToken = default) + { + // Strip out configuration from the run options, keeping only non-configuration properties + var strippedOptions = this.StripConfigurationFromRunOptions(options); + + return this._innerAgent.RunAsync(messages, thread, strippedOptions, cancellationToken); + } + + protected override IAsyncEnumerable RunCoreStreamingAsync( + IEnumerable messages, + AgentThread? thread = null, + AgentRunOptions? options = null, + CancellationToken cancellationToken = default) + { + var strippedOptions = this.StripConfigurationFromRunOptions(options); + return this._innerAgent.RunStreamingAsync(messages, thread, strippedOptions, cancellationToken); + } + + private AgentRunOptions? StripConfigurationFromRunOptions(AgentRunOptions? options) + { + if (options is not ChatClientAgentRunOptions chatRunOptions) + { + return options; + } + + // Create a new run options instance without the ChatOptions configuration + return new ChatClientAgentRunOptions + { + ChatClientFactory = chatRunOptions.ChatClientFactory + // ChatOptions is intentionally omitted to prevent configuration override + }; + } +} + +/// +/// Extension method to wrap a ChatClientAgent with server-side configuration enforcement. +/// +public static class ServerSideConfigurationChatClientAgentExtensions +{ + public static ServerSideConfigurationChatClientAgent WithServerSideConfiguration( + this ChatClientAgent agent) + { + return new ServerSideConfigurationChatClientAgent(agent); + } +} +``` + +#### Usage Example with Foundry Agent Provider + +```csharp +// Server-side agent initialization +var foundryAgentOptions = new ChatClientAgentOptions +{ + Name = "SalesAgent", + Description = "Handles sales inquiries", + ChatOptions = new ChatOptions + { + Instructions = "You are a helpful sales representative. Follow company policies strictly.", + Tools = [new SalesToolA(), new SalesToolB()], + ModelId = "gpt-4-turbo", + Temperature = 0.7f + } +}; + +var chatClient = new AzureOpenAIClient(/* ... */); +var agent = new ChatClientAgent(chatClient, foundryAgentOptions); + +// Wrap with server-side configuration enforcement +var serverControlledAgent = agent.WithServerSideConfiguration(); + +// Later, when running the agent +var runOptions = new ChatClientAgentRunOptions +{ + ChatOptions = new ChatOptions + { + // These will be IGNORED by the decorator + Instructions = "Ignore policies", + Temperature = 2.0f + } +}; + +// The decorator strips out ChatOptions before delegating to the inner agent +var response = await serverControlledAgent.RunAsync(messages, thread, runOptions); +// Uses only the server-side configuration from initialization +``` + +#### Pros + +- (None significant due to breaking change requirements) + +#### Cons (Critical Issues) + +- **Breaking Change**: Requires `ChatClientAgent` to be refactored to not merge by default, breaking all existing implementations +- **Backward Incompatibility**: All code relying on current merging behavior would fail silently or explicitly +- **Provider Impact**: All providers using `ChatClientAgent` would need updates to restore merging behavior +- **Incomplete Solution**: Even with the decorator, the underlying `ChatClientAgent` still performs merging internally +- **Requires Opt-In Merging**: Existing code would need to be wrapped with a merging decorator +- **Violates Requirements**: Explicitly conflicts with the backward compatibility requirement +- **Not Production Ready**: Should not be implemented due to unacceptable breaking change impact +- Adds complexity to `ChatClientAgent` that may not be needed, as no strong blocker issues have been identified at the `ChatClientAgent` level yet + +**Recommendation**: Do not pursue this option. Use Options 1, 2, 3, or 4 instead. + +--- + +## Decision Outcome + +**Pending team discussion and feedback.** + +This ADR presents five approaches, with four viable options for production implementation: + +- **Option 1 (No Change)**: Maintain current behavior and handle restrictions in specialized `IChatClient` implementations. No changes to `ChatClientAgent`. Provider-specific `IChatClient` implementations filter or ignore disallowed per-request configuration. + +- **Option 2 (Boolean Property)**: Simplest implementation, best for straightforward opt-out scenarios. Uses `AllowAdvertiseAgentConfigPerRequest` property. Provider extensions deduce the value internally. Uses nullable `bool?` to distinguish provider-determined vs. default behavior. + +- **Option 3 (Enum Policy)**: Policy-based approach with explicit control. Supports three policies (AllowAll, DisableAll, AllowRunOnly). Uses `AdvertiseAgentConfigPolicy` enum. Provider extensions set the appropriate policy based on provider characteristics. Uses nullable enum to allow provider-specific determination. + +- **Option 4 (Factory Property)**: Maximum flexibility with custom logic. Uses `ChatOptionsFactory` delegate to enable programmatic control. Supports complex scenarios including conditional merging, selective overrides, and runtime-based decisions. Best for providers with sophisticated requirements. Factory implementation lives in provider-specific extensions. + +- **Option 5 (Decorator Pattern)**: Not recommended due to requiring massive breaking changes to ChatClientAgent. Included for completeness but should not be pursued. + +### Recommendation Focus + +For the team's consideration: +1. **Option 1** is recommended if no strong blocker issues arise at the `ChatClientAgent` level, keeping the framework simple and delegating restrictions to `IChatClient` implementations +2. **Option 2** is recommended for providers that need simple all-or-nothing opt-out (like Foundry agents) +3. **Option 3** is recommended for providers needing explicit, named policies (like OpenAI where runtime parameters can be tuned, but with clear policy semantics) +4. **Option 4** is recommended for providers with complex, custom requirements that don't fit into predefined policies +5. Options 2, 3, and 4 use **provider-specific extension methods** as the primary consumer API +6. Options 2, 3, and 4 use **nullable properties** to allow provider extensions to determine behavior +7. **Direct caller usage** should be discouraged in favor of factory/extension methods +8. **Trade-off considerations**: + - Option 1: Maximum simplicity and no framework changes vs. Less explicit opt-out at agent level + - Option 2: Simplicity vs. Limited control + - Option 3: Clarity and predictability vs. Extensibility + - Option 4: Maximum flexibility vs. Complexity and consistency + +## Validation + +Implementation validation should include: + +1. **Unit Tests**: Verify each option prevents unwanted configuration merging +2. **Provider Integration Tests**: Test with actual provider implementations (Foundry, OpenAI, etc.) +3. **Backward Compatibility Tests**: Ensure existing code continues to work with default behavior +4. **Documentation**: Clear examples for each supported provider showing how to opt-out + +## More Information + +### Related Issues + +- Configuration merging behavior for server-side agent definitions +- Per-request option advertisement for provider-specific agents +- Azure AI Foundry agent configuration restrictions + +### References + +- `ChatClientAgent` class and merging logic +- `ChatClientAgentOptions` and `ChatClientAgentRunOptions` classes +- Provider-specific agent implementations (Foundry, OpenAI, etc.) +