Skip to content

Commit 804dbb6

Browse files
authored
Add Additional Properties ADR (microsoft#4246)
* Add Additional Properties ADR * Address PR comments
1 parent 4dc35e9 commit 804dbb6

File tree

1 file changed

+211
-0
lines changed

1 file changed

+211
-0
lines changed
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
---
2+
status: accepted
3+
contact: westey-m
4+
date: 2026-02-24
5+
deciders: sergeymenshykh, markwallace, rbarreto, dmytrostruk, westey-m, eavanvalkenburg, stephentoub, lokitoth, alliscode, taochenosu, moonbox3
6+
consulted:
7+
informed:
8+
---
9+
10+
# AdditionalProperties for AIAgent and AgentSession
11+
12+
## Context and Problem Statement
13+
14+
The `AIAgent` base class currently exposes `Id`, `Name`, and `Description` as its core metadata properties, and `AgentSession` exposes only a `StateBag` property.
15+
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.
16+
17+
Other types in the framework already carry `AdditionalProperties` — notably `AgentRunOptions`, `AgentResponse`, and `AgentResponseUpdate` — all using `AdditionalPropertiesDictionary` from `Microsoft.Extensions.AI`.
18+
Adding a similar property to `AIAgent` and `AgentSession` would give both types a consistent, extensible metadata surface.
19+
20+
Related: [Work Item #2133](https://github.com/microsoft/agent-framework/issues/2133)
21+
22+
## Decision Drivers
23+
24+
- **Consistency**: Other core types (`AgentRunOptions`, `AgentResponse`, `AgentResponseUpdate`) already expose `AdditionalProperties`. `AIAgent` and `AgentSession` are the major abstractions that lack this.
25+
- **Extensibility**: Hosting libraries, protocol adapters (A2A, AG-UI), and discovery mechanisms need a place to attach agent-level and session-level metadata without subclassing.
26+
- **Simplicity**: The solution should be easy to understand and use; avoid over-engineering.
27+
- **Minimal breaking change**: The addition should not require changes to existing agent implementations.
28+
- **Clear semantics**: Users should understand what `AdditionalProperties` on an agent or session means and how it differs from `AdditionalProperties` on `AgentRunOptions`.
29+
30+
## Considered Options
31+
32+
### Surface Area
33+
34+
- **Option A**: Public get-only property, auto-initialized (`AdditionalPropertiesDictionary AdditionalProperties { get; } = new()`) on both `AIAgent` and `AgentSession`
35+
- **Option B**: Public get/set nullable property (`AdditionalPropertiesDictionary? AdditionalProperties { get; set; }`) on both `AIAgent` and `AgentSession`
36+
- **Option C**: Constructor-injected dictionary with public get-only accessor on both `AIAgent` and `AgentSession`
37+
- **Option D**: External container/wrapper object — metadata lives outside `AIAgent` and `AgentSession`; no changes to the base classes
38+
39+
### Semantics
40+
41+
- **Option 1**: Metadata only — describes the agent or session; not propagated when calling `IChatClient`
42+
- **Option 2**: Passed down the stack — merged into `ChatOptions.AdditionalProperties` during `ChatClientAgent` runs
43+
44+
## Decision Outcome
45+
46+
The chosen option is **Option D + Option 1**: an external container/wrapper object, used purely as metadata.
47+
48+
### Consequences
49+
50+
- Good, because `AIAgent` and `AgentSession` remain unchanged, avoiding any increase to the core framework surface area while still enabling extensible metadata.
51+
- 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`.
52+
- Good, because metadata-only semantics keep a clean separation from per-run extensibility (`AgentRunOptions.AdditionalProperties`) and avoid unexpected side effects during agent execution.
53+
- 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.
54+
- Bad, because callers and libraries must manage and pass around both the agent/session instance and its associated metadata wrapper, keeping them correctly associated.
55+
- Bad, because different hosting or protocol layers may define their own wrapper types, which can fragment the ecosystem unless conventions are agreed upon.
56+
57+
## Pros and Cons of the Options
58+
59+
### Option A — Public get-only property, auto-initialized
60+
61+
The property is always non-null and ready to use. Users add metadata after construction.
62+
63+
```csharp
64+
public abstract partial class AIAgent
65+
{
66+
public AdditionalPropertiesDictionary AdditionalProperties { get; } = new();
67+
}
68+
69+
public abstract partial class AgentSession
70+
{
71+
public AdditionalPropertiesDictionary AdditionalProperties { get; } = new();
72+
}
73+
74+
// Usage
75+
agent.AdditionalProperties["protocol"] = "A2A";
76+
agent.AdditionalProperties.Add<MyAgentCardInfo>(cardInfo);
77+
session.AdditionalProperties["tenant"] = tenantId;
78+
```
79+
80+
- Good, because users never encounter `null` — no defensive null checks needed.
81+
- Good, because the dictionary reference cannot be replaced, preventing accidental data loss.
82+
- Good, because it is the simplest API surface to use.
83+
- Neutral, because it always allocates, even when no metadata is needed. The allocation cost is negligible.
84+
- Bad, because it cannot be set at construction time as a single object (users must populate it post-construction).
85+
86+
### Option B — Public get/set nullable property
87+
88+
Matches the existing pattern on `AgentRunOptions`, `AgentResponse`, and `AgentResponseUpdate`.
89+
90+
```csharp
91+
public abstract partial class AIAgent
92+
{
93+
public AdditionalPropertiesDictionary? AdditionalProperties { get; set; }
94+
}
95+
96+
public abstract partial class AgentSession
97+
{
98+
public AdditionalPropertiesDictionary? AdditionalProperties { get; set; }
99+
}
100+
101+
// Usage
102+
agent.AdditionalProperties ??= new();
103+
agent.AdditionalProperties["protocol"] = "A2A";
104+
session.AdditionalProperties ??= new();
105+
session.AdditionalProperties["tenant"] = tenantId;
106+
```
107+
108+
- Good, because it is consistent with the existing `AdditionalProperties` pattern on `AgentRunOptions` and `AgentResponse`.
109+
- Good, because it avoids allocation when no metadata is needed.
110+
- Bad, because every consumer must null-check before reading or writing.
111+
- 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).
112+
113+
### Option C — Constructor-injected with public get
114+
115+
The dictionary is provided at construction time and exposed as get-only.
116+
117+
```csharp
118+
public abstract partial class AIAgent
119+
{
120+
public AdditionalPropertiesDictionary AdditionalProperties { get; }
121+
122+
protected AIAgent(AdditionalPropertiesDictionary? additionalProperties = null)
123+
{
124+
this.AdditionalProperties = additionalProperties ?? new();
125+
}
126+
}
127+
128+
public abstract partial class AgentSession
129+
{
130+
public AdditionalPropertiesDictionary AdditionalProperties { get; }
131+
132+
protected AgentSession(AdditionalPropertiesDictionary? additionalProperties = null)
133+
{
134+
this.AdditionalProperties = additionalProperties ?? new();
135+
}
136+
}
137+
```
138+
139+
- Good, because an agent's metadata can be established before any code runs against it.
140+
- 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.
141+
- 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).
142+
- Bad, because it is more complex with little practical benefit over Option A, since post-construction mutation is equally possible.
143+
144+
### Option D — External container/wrapper object
145+
146+
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.
147+
148+
```csharp
149+
public class AgentWithMetadata
150+
{
151+
public required AIAgent Agent { get; init; }
152+
public AdditionalPropertiesDictionary? AdditionalProperties { get; set; }
153+
}
154+
155+
public class SessionWithMetadata
156+
{
157+
public required AgentSession Session { get; init; }
158+
public AdditionalPropertiesDictionary? AdditionalProperties { get; set; }
159+
}
160+
161+
// Usage
162+
var wrapper = new AgentWithMetadata
163+
{
164+
Agent = myAgent,
165+
AdditionalProperties = new() { ["protocol"] = "A2A" }
166+
};
167+
```
168+
169+
- Good, because it requires no changes to `AIAgent` or `AgentSession`, avoiding any risk of breaking existing implementations.
170+
- Good, because metadata is clearly external to the agent and session, eliminating any ambiguity about whether it might be passed down the execution stack.
171+
- Good, because the container pattern gives the user full control over the metadata lifecycle and serialization.
172+
- Bad, because it is not discoverable — users must know about the container convention; there is no built-in API surface guiding them.
173+
174+
### Option 1 — Metadata only
175+
176+
`AdditionalProperties` on `AIAgent` and `AgentSession` is descriptive metadata. It is **not** automatically propagated when the agent calls downstream services such as `IChatClient`.
177+
178+
- Good, because it keeps a clean separation of concerns: agent/session-level metadata vs. per-run options.
179+
- Good, because it avoids unintended side effects — metadata added for discovery or hosting won't leak into LLM requests.
180+
- Good, because per-run extensibility is already served by `AgentRunOptions.AdditionalProperties` (see [ADR 0014](0014-feature-collections.md)), so there is no gap.
181+
- Neutral, because users who want to pass agent metadata to the chat client can still do so manually via `AgentRunOptions`.
182+
183+
### Option 2 — Passed down the stack
184+
185+
`AdditionalProperties` on `AIAgent` and `AgentSession` are automatically merged into `ChatOptions.AdditionalProperties` (or similar) when `ChatClientAgent` invokes the underlying `IChatClient`.
186+
187+
- Good, because it provides an automatic way to send agent-level configuration to the LLM provider.
188+
- Bad, because it conflates metadata (describing the agent) with operational parameters (controlling LLM behavior), leading to potential confusion.
189+
- Bad, because it risks leaking unrelated metadata into LLM calls (e.g., hosting tags, discovery URLs).
190+
- Bad, because it would be `ChatClientAgent`-specific behavior on a base-class property, creating inconsistency for non-`ChatClientAgent` implementations.
191+
- Bad, because it duplicates the purpose of `AgentRunOptions.AdditionalProperties`, which already serves as the per-run extensibility point for passing data down the stack.
192+
193+
## Serialization Considerations
194+
195+
`AIAgent` instances are not typically serialized, so `AdditionalProperties` on `AIAgent` does not raise serialization concerns.
196+
197+
`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.
198+
199+
### Default behavior — JsonElement round-tripping
200+
201+
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.
202+
203+
### Custom serialization via JsonSerializerOptions
204+
205+
`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.
206+
207+
## More Information
208+
209+
- [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.
210+
- `AdditionalPropertiesDictionary` is defined in `Microsoft.Extensions.AI` and is already a dependency of `Microsoft.Agents.AI.Abstractions`. No new package references are needed.
211+
- 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.

0 commit comments

Comments
 (0)