diff --git a/docs/decisions/0017-agent-additional-properties.md b/docs/decisions/0017-agent-additional-properties.md new file mode 100644 index 0000000000..8531a9bd2b --- /dev/null +++ b/docs/decisions/0017-agent-additional-properties.md @@ -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(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`, 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`, `TryGetValue`, `Contains`, `Remove`), which use `typeof(T).FullName` as the dictionary key.