| status | accepted |
|---|---|
| contact | westey-m |
| date | 2026-02-24 |
| deciders | sergeymenshykh, markwallace, rbarreto, dmytrostruk, westey-m, eavanvalkenburg, stephentoub, lokitoth, alliscode, taochenosu, moonbox3 |
| consulted | |
| informed |
The AIAgent base class currently exposes Id, Name, and Description as its core metadata properties, and AgentSession exposes only a StateBag property.
Neither type has a mechanism for attaching arbitrary metadata, such as protocol-specific descriptors (e.g., A2A agent cards), hosting attributes, session-level tags, or custom user-defined metadata for discovery and routing.
Other types in the framework already carry AdditionalProperties — notably AgentRunOptions, AgentResponse, and AgentResponseUpdate — all using AdditionalPropertiesDictionary from Microsoft.Extensions.AI.
Adding a similar property to AIAgent and AgentSession would give both types a consistent, extensible metadata surface.
Related: Work Item #2133
- Consistency: Other core types (
AgentRunOptions,AgentResponse,AgentResponseUpdate) already exposeAdditionalProperties.AIAgentandAgentSessionare the major abstractions that lack this. - Extensibility: Hosting libraries, protocol adapters (A2A, AG-UI), and discovery mechanisms need a place to attach agent-level and session-level metadata without subclassing.
- Simplicity: The solution should be easy to understand and use; avoid over-engineering.
- Minimal breaking change: The addition should not require changes to existing agent implementations.
- Clear semantics: Users should understand what
AdditionalPropertieson an agent or session means and how it differs fromAdditionalPropertiesonAgentRunOptions.
- Option A: Public get-only property, auto-initialized (
AdditionalPropertiesDictionary AdditionalProperties { get; } = new()) on bothAIAgentandAgentSession - Option B: Public get/set nullable property (
AdditionalPropertiesDictionary? AdditionalProperties { get; set; }) on bothAIAgentandAgentSession - Option C: Constructor-injected dictionary with public get-only accessor on both
AIAgentandAgentSession - Option D: External container/wrapper object — metadata lives outside
AIAgentandAgentSession; no changes to the base classes
- Option 1: Metadata only — describes the agent or session; not propagated when calling
IChatClient - Option 2: Passed down the stack — merged into
ChatOptions.AdditionalPropertiesduringChatClientAgentruns
The chosen option is Option D + Option 1: an external container/wrapper object, used purely as metadata.
- Good, because
AIAgentandAgentSessionremain unchanged, avoiding any increase to the core framework surface area while still enabling extensible metadata. - Good, because an external wrapper (owned by hosting/protocol libraries or user code, not the
AIAgent/AgentSessionbase classes) can internally useAdditionalPropertiesDictionaryto stay consistent with existing patterns onAgentRunOptions,AgentResponse, andAgentResponseUpdate. - Good, because metadata-only semantics keep a clean separation from per-run extensibility (
AgentRunOptions.AdditionalProperties) and avoid unexpected side effects during agent execution. - Good, because no additional allocation occurs on
AIAgentorAgentSessionwhen no metadata is needed; external wrappers can be created only when metadata is required. - Bad, because callers and libraries must manage and pass around both the agent/session instance and its associated metadata wrapper, keeping them correctly associated.
- Bad, because different hosting or protocol layers may define their own wrapper types, which can fragment the ecosystem unless conventions are agreed upon.
The property is always non-null and ready to use. Users add metadata after construction.
public abstract partial class AIAgent
{
public AdditionalPropertiesDictionary AdditionalProperties { get; } = new();
}
public abstract partial class AgentSession
{
public AdditionalPropertiesDictionary AdditionalProperties { get; } = new();
}
// Usage
agent.AdditionalProperties["protocol"] = "A2A";
agent.AdditionalProperties.Add<MyAgentCardInfo>(cardInfo);
session.AdditionalProperties["tenant"] = tenantId;- Good, because users never encounter
null— no defensive null checks needed. - Good, because the dictionary reference cannot be replaced, preventing accidental data loss.
- Good, because it is the simplest API surface to use.
- Neutral, because it always allocates, even when no metadata is needed. The allocation cost is negligible.
- Bad, because it cannot be set at construction time as a single object (users must populate it post-construction).
Matches the existing pattern on AgentRunOptions, AgentResponse, and AgentResponseUpdate.
public abstract partial class AIAgent
{
public AdditionalPropertiesDictionary? AdditionalProperties { get; set; }
}
public abstract partial class AgentSession
{
public AdditionalPropertiesDictionary? AdditionalProperties { get; set; }
}
// Usage
agent.AdditionalProperties ??= new();
agent.AdditionalProperties["protocol"] = "A2A";
session.AdditionalProperties ??= new();
session.AdditionalProperties["tenant"] = tenantId;- Good, because it is consistent with the existing
AdditionalPropertiespattern onAgentRunOptionsandAgentResponse. - Good, because it avoids allocation when no metadata is needed.
- Bad, because every consumer must null-check before reading or writing.
- Bad, because the entire dictionary can be replaced, risking accidental loss of metadata set by other components (e.g., a hosting library sets metadata, then user code replaces the dictionary).
The dictionary is provided at construction time and exposed as get-only.
public abstract partial class AIAgent
{
public AdditionalPropertiesDictionary AdditionalProperties { get; }
protected AIAgent(AdditionalPropertiesDictionary? additionalProperties = null)
{
this.AdditionalProperties = additionalProperties ?? new();
}
}
public abstract partial class AgentSession
{
public AdditionalPropertiesDictionary AdditionalProperties { get; }
protected AgentSession(AdditionalPropertiesDictionary? additionalProperties = null)
{
this.AdditionalProperties = additionalProperties ?? new();
}
}- Good, because an agent's metadata can be established before any code runs against it.
- Bad, because
AdditionalPropertiesDictionaryhas no read-only variant, so the constructor-injection pattern gives a false sense of immutability — callers can still mutate the dictionary contents after construction. - Bad, because it requires adding a constructor parameter to the abstract base classes, which is a source-breaking change for all existing
AIAgentandAgentSessionsubclasses (even with a default value, it changes the constructor signature that derived classes chain to). - Bad, because it is more complex with little practical benefit over Option A, since post-construction mutation is equally possible.
Rather than adding AdditionalProperties to AIAgent or AgentSession, users wrap the agent or session in a container object that carries both the instance and any associated metadata. No changes to the base classes are required.
public class AgentWithMetadata
{
public required AIAgent Agent { get; init; }
public AdditionalPropertiesDictionary? AdditionalProperties { get; set; }
}
public class SessionWithMetadata
{
public required AgentSession Session { get; init; }
public AdditionalPropertiesDictionary? AdditionalProperties { get; set; }
}
// Usage
var wrapper = new AgentWithMetadata
{
Agent = myAgent,
AdditionalProperties = new() { ["protocol"] = "A2A" }
};- Good, because it requires no changes to
AIAgentorAgentSession, avoiding any risk of breaking existing implementations. - Good, because metadata is clearly external to the agent and session, eliminating any ambiguity about whether it might be passed down the execution stack.
- Good, because the container pattern gives the user full control over the metadata lifecycle and serialization.
- Bad, because it is not discoverable — users must know about the container convention; there is no built-in API surface guiding them.
AdditionalProperties on AIAgent and AgentSession is descriptive metadata. It is not automatically propagated when the agent calls downstream services such as IChatClient.
- Good, because it keeps a clean separation of concerns: agent/session-level metadata vs. per-run options.
- Good, because it avoids unintended side effects — metadata added for discovery or hosting won't leak into LLM requests.
- Good, because per-run extensibility is already served by
AgentRunOptions.AdditionalProperties(see ADR 0014), so there is no gap. - Neutral, because users who want to pass agent metadata to the chat client can still do so manually via
AgentRunOptions.
AdditionalProperties on AIAgent and AgentSession are automatically merged into ChatOptions.AdditionalProperties (or similar) when ChatClientAgent invokes the underlying IChatClient.
- Good, because it provides an automatic way to send agent-level configuration to the LLM provider.
- Bad, because it conflates metadata (describing the agent) with operational parameters (controlling LLM behavior), leading to potential confusion.
- Bad, because it risks leaking unrelated metadata into LLM calls (e.g., hosting tags, discovery URLs).
- Bad, because it would be
ChatClientAgent-specific behavior on a base-class property, creating inconsistency for non-ChatClientAgentimplementations. - Bad, because it duplicates the purpose of
AgentRunOptions.AdditionalProperties, which already serves as the per-run extensibility point for passing data down the stack.
AIAgent instances are not typically serialized, so AdditionalProperties on AIAgent does not raise serialization concerns.
AgentSession instances, however, are routinely serialized and deserialized — for example, to persist conversation state across application restarts. Adding AdditionalProperties to AgentSession introduces a serialization challenge: AdditionalPropertiesDictionary is a Dictionary<string, object?>, and object? values do not carry enough type information for the JSON deserializer to reconstruct the original CLR types.
By default, when an AgentSession with AdditionalProperties is serialized and later deserialized, any complex objects stored as values in the dictionary will be deserialized as JsonElement rather than their original types. This is the same behavior exhibited by ChatMessage.AdditionalProperties and other AdditionalPropertiesDictionary usages in Microsoft.Extensions.AI, and is the approach we will follow.
AIAgent.SerializeSessionAsync and AIAgent.DeserializeSessionAsync already accept an optional JsonSerializerOptions parameter. Users who need strongly-typed round-tripping of AdditionalProperties values can supply custom options with appropriate converters or type info resolvers. This is non-trivial to implement but provides full control over deserialization behavior when needed.
- ADR 0014 — Feature Collections established that
AdditionalPropertiesonAgentRunOptionsserves as the per-run extensibility mechanism. The proposed agent-level and session-level properties serve a complementary, distinct purpose: static metadata describing the agent or session itself. AdditionalPropertiesDictionaryis defined inMicrosoft.Extensions.AIand is already a dependency ofMicrosoft.Agents.AI.Abstractions. No new package references are needed.- Type-safe access is available via the existing
AdditionalPropertiesExtensionshelper methods (Add<T>,TryGetValue<T>,Contains<T>,Remove<T>), which usetypeof(T).FullNameas the dictionary key.