-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Add Additional Properties ADR #4246
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
westey-m
merged 2 commits into
microsoft:main
from
westey-m:adr-agent-additional-properties
Feb 25, 2026
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,211 @@ | ||
| --- | ||
| status: accepted | ||
| contact: westey-m | ||
| date: 2026-02-24 | ||
| deciders: sergeymenshykh, markwallace, rbarreto, dmytrostruk, westey-m, eavanvalkenburg, stephentoub, lokitoth, alliscode, taochenosu, moonbox3 | ||
| consulted: | ||
| informed: | ||
| --- | ||
|
|
||
| # AdditionalProperties for AIAgent and AgentSession | ||
|
|
||
| ## Context and Problem Statement | ||
|
|
||
| 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](https://github.com/microsoft/agent-framework/issues/2133) | ||
|
|
||
| ## Decision Drivers | ||
|
|
||
| - **Consistency**: Other core types (`AgentRunOptions`, `AgentResponse`, `AgentResponseUpdate`) already expose `AdditionalProperties`. `AIAgent` and `AgentSession` are 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 `AdditionalProperties` on an agent or session means and how it differs from `AdditionalProperties` on `AgentRunOptions`. | ||
|
|
||
| ## Considered Options | ||
|
|
||
| ### Surface Area | ||
|
|
||
| - **Option A**: Public get-only property, auto-initialized (`AdditionalPropertiesDictionary AdditionalProperties { get; } = new()`) on both `AIAgent` and `AgentSession` | ||
| - **Option B**: Public get/set nullable property (`AdditionalPropertiesDictionary? AdditionalProperties { get; set; }`) on both `AIAgent` and `AgentSession` | ||
| - **Option C**: Constructor-injected dictionary with public get-only accessor on both `AIAgent` and `AgentSession` | ||
| - **Option D**: External container/wrapper object — metadata lives outside `AIAgent` and `AgentSession`; no changes to the base classes | ||
|
|
||
| ### Semantics | ||
|
|
||
| - **Option 1**: Metadata only — describes the agent or session; not propagated when calling `IChatClient` | ||
| - **Option 2**: Passed down the stack — merged into `ChatOptions.AdditionalProperties` during `ChatClientAgent` runs | ||
|
|
||
| ## Decision Outcome | ||
|
|
||
| The chosen option is **Option D + Option 1**: an external container/wrapper object, used purely as metadata. | ||
|
|
||
| ### Consequences | ||
|
|
||
| - Good, because `AIAgent` and `AgentSession` remain 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` / `AgentSession` base classes) can internally use `AdditionalPropertiesDictionary` to stay consistent with existing patterns on `AgentRunOptions`, `AgentResponse`, and `AgentResponseUpdate`. | ||
| - 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 `AIAgent` or `AgentSession` when 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. | ||
|
|
||
| ## Pros and Cons of the Options | ||
|
|
||
| ### Option A — Public get-only property, auto-initialized | ||
|
|
||
| The property is always non-null and ready to use. Users add metadata after construction. | ||
|
|
||
| ```csharp | ||
| 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). | ||
|
|
||
| ### Option B — Public get/set nullable property | ||
|
|
||
| Matches the existing pattern on `AgentRunOptions`, `AgentResponse`, and `AgentResponseUpdate`. | ||
|
|
||
| ```csharp | ||
| 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 `AdditionalProperties` pattern on `AgentRunOptions` and `AgentResponse`. | ||
| - 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). | ||
|
|
||
| ### Option C — Constructor-injected with public get | ||
|
|
||
| The dictionary is provided at construction time and exposed as get-only. | ||
|
|
||
| ```csharp | ||
| 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 `AdditionalPropertiesDictionary` has 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 `AIAgent` and `AgentSession` subclasses (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. | ||
|
|
||
| ### Option D — External container/wrapper object | ||
|
|
||
| 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. | ||
|
|
||
| ```csharp | ||
| 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 `AIAgent` or `AgentSession`, 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. | ||
|
|
||
| ### Option 1 — Metadata only | ||
|
|
||
| `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](0014-feature-collections.md)), 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`. | ||
|
|
||
| ### Option 2 — Passed down the stack | ||
|
|
||
| `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-`ChatClientAgent` implementations. | ||
| - Bad, because it duplicates the purpose of `AgentRunOptions.AdditionalProperties`, which already serves as the per-run extensibility point for passing data down the stack. | ||
|
|
||
| ## Serialization Considerations | ||
|
|
||
| `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. | ||
|
|
||
| ### Default behavior — JsonElement round-tripping | ||
|
|
||
| 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. | ||
|
|
||
| ### Custom serialization via JsonSerializerOptions | ||
|
|
||
| `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. | ||
|
|
||
| ## More Information | ||
|
|
||
| - [ADR 0014 — Feature Collections](0014-feature-collections.md) established that `AdditionalProperties` on `AgentRunOptions` serves 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. | ||
| - `AdditionalPropertiesDictionary` is defined in `Microsoft.Extensions.AI` and is already a dependency of `Microsoft.Agents.AI.Abstractions`. No new package references are needed. | ||
| - Type-safe access is available via the existing `AdditionalPropertiesExtensions` helper methods (`Add<T>`, `TryGetValue<T>`, `Contains<T>`, `Remove<T>`), which use `typeof(T).FullName` as the dictionary key. | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.