Skip to content
Closed
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
207 changes: 207 additions & 0 deletions docs/decisions/00NN-feature-collections.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
---
status: proposed
contact: westey-m
date: 2025-11-26
deciders: {list everyone involved in the decision}
consulted: {list everyone whose opinions are sought (typically subject-matter experts); and with whom there is a two-way communication}
informed: {list everyone who is kept up-to-date on progress; and with whom there is a one-way communication}
Comment thread
westey-m marked this conversation as resolved.
Comment thread
westey-m marked this conversation as resolved.
Comment on lines +4 to +7

Copilot AI Nov 28, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The date field contains '2025-11-26', but according to the ADR README guidelines, the date format should be YYYY-MM-DD when the decision was last updated. Since we are currently in November 2025, this appears to be a future date. Additionally, the deciders, consulted, and informed fields still contain placeholder text in braces. These should be filled out with actual names or GitHub IDs per the ADR template requirements.

Suggested change
date: 2025-11-26
deciders: {list everyone involved in the decision}
consulted: {list everyone whose opinions are sought (typically subject-matter experts); and with whom there is a two-way communication}
informed: {list everyone who is kept up-to-date on progress; and with whom there is a one-way communication}
date: 2024-06-10
deciders: westey-m, alice-smith, bob-jones
consulted: carol-dev, dave-ai
informed: eve-ml, frank-ops

Copilot uses AI. Check for mistakes.
---

# Feature Collections

## Context and Problem Statement

When using agents, we often have cases where we want to pass some arbitrary services or data to an agent or some component in the agent execution stack.
These services or data are not necessarily known at compile time and can vary by the agent stack that the user has built.
E.g., there may be an agent decorator or chat client decorator that was added to the stack by the user, and an arbitrary payload needs to be passed to that decorator.

Since these payloads are related to components that are not integral parts of the agent framework, they cannot be added as strongly typed settings to the agent run options.
However, the payloads could be added to the agent run options as loosely typed 'features', that can be retrieved as needed.

In some cases certain classes of agents may support the same capability, but not all agents do.
Having the configuration for such a capability on the main abstraction would advertise the functionality to all users, even if their chosen agent does not support it.
The user may type test for certain agent types, and call overloads on the appropriate agent types, with the strongly typed configuration.
Having a feature collection though, would be an alternative way of passing such configuration, without needing to type check the agent type.
All agents that support the functionality would be able to check for the configuration and use it, simplifying the user code.
If the agent does not support the capability, that configuration would be ignored.

### Sample Scenario 1

We are building an agent hosting library, that can host any agent built using the agent framework.
Where an agent is not built on a service that uses in-service chat history storage, the hosting library wants to force the agent to use
the hosting library's chat history storage implementation.
This chat history storage implementation may be specifically tailored to the type of protocol that the hosting library uses, e.g. conversation id based storage or response id based storage.
The hosting library does not know what type of agent it is hosting, so it cannot provide a strongly typed parameter on the agent.
Instead, it adds the chat history storage implementation to a feature collection, and if the agent supports custom chat history storage, it retrieves the implementation from the feature collection and uses it.

```csharp
// Pseudo-code for an agent hosting library that supports conversation id based hosting.
public string HandleConversationsBasedRequest(AIAgent agent, string conversationId, string userInput)
Comment thread
westey-m marked this conversation as resolved.
Outdated
{
var thread = this._threadStore.GetOrCreateThread(conversationId);

// The hosting library can set a per-run chat message store via Features that only applies for that run.
// This message store will load and save messages under the conversation id provided.
ConversationsChatMessageStore messageStore = new(this._dbClient, conversationId);
var response = await agent.RunAsync(
userInput,
thread,
options: new AgentRunOptions()
{
Features = new AgentFeatureCollection().WithFeature<ChatMessageStore>(messageStore)
});

this._threadStore.SaveThread(conversationId, thread);
return response.Text;
}

// Pseudo-code for an agent hosting library that supports response id based hosting.
public (string responseMessage, string responseId) HandleResponseIdBasedRequest(AIAgent agent, string previousResponseId, string userInput)
Comment thread
westey-m marked this conversation as resolved.
Outdated
{
var thread = this._threadStore.GetOrCreateThread(previousResponseId);

// The hosting library can set a per-run chat message store via Features that only applies for that run.
// This message store will buffer newly added messages until explicitly saved after the run.
ResponsesChatMessageStore messageStore = new(this._dbClient, previousResponseId);

var response = await agent.RunAsync(
userInput,
thread,
options: new AgentRunOptions()
{
Features = new AgentFeatureCollection().WithFeature<ChatMessageStore>(messageStore)
});

// Since the message store may not actually have been used at all (if the agent's underlying chat client requires service-based chat history storage),
// we may not have anything to save back to the database.
// We still want to generate a new response id though, so that we can save the updated thread state under that id.
// We should also use the same id to save any buffered messages in the message store if there are any.
var newResponseId = this.GenerateResponseId();
if (messageStore.HasBufferedMessages)
{
messageStore.SaveBufferedMessages(newResponseId);
}

// Save the updated thread state under the new response id that was generated by the store.
this._threadStore.SaveThread(newResponseId, thread);
return (response.Text, newResponseId);
}
```

## Implementation Options

Three options were considered for implementing feature collections:

- **Option 1**: FeatureCollections similar to ASP.NET Core
- **Option 2**: AdditionalProperties Dictionary
- **Option 3**: IServiceProvider

Here are some comparisons about their suitability for our use case:

| Criteria | Feature Collection | Additional Properties | IServiceProvider |
|------------------|--------------------|-----------------------|------------------|
|Ease of use |✅ Good |❌ Bad |✅ Good |
|User familiarity |❌ Bad |✅ Good |✅ Good |
|Type safety |✅ Good |❌ Bad |✅ Good |
|Ability to modify registered options when progressing down the stack|✅ Supported|✅ Supported|❌ Not-Supported (IServiceProvider is read-only)|
|Already available in MEAI stack|❌ No|✅ Yes|❌ No|
|Ability to layer features by scope (e.g., per-agent, per-request)|✅ Supported|❌ Not-Supported|❌ Not-Supported|

## Feature Collection

Copilot AI Jan 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent terminology: The document uses both "Feature Collection" (title case) and "feature collection" (lowercase) throughout. For consistency and following typical technical documentation conventions, consider using lowercase "feature collection" when referring to the concept generically, and title case only when referring to the specific type name "FeatureCollection" or interface name.

Copilot uses AI. Check for mistakes.

If we choose the feature collection option, we need to decide on the design of the feature collection itself.

### Feature Collections extension points

We need to decide the set of actions that feature collections would be supported for. Here is the suggested list of actions:

**MAAI.AIAgent:**
Comment thread
westey-m marked this conversation as resolved.
Comment thread
westey-m marked this conversation as resolved.
Comment thread
westey-m marked this conversation as resolved.
Comment thread
westey-m marked this conversation as resolved.

Copilot AI Jan 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: "MAAI.AIAgent" should be "MEAI.AIAgent" or more likely should reference the Agent Framework package name. Based on the context and usage pattern in other ADR documents where MEAI refers to Microsoft.Extensions.AI, this appears to be a typo. The Agent Framework agents are not part of MEAI but are part of the agent framework itself.

Suggested change
**MAAI.AIAgent:**
**Agent Framework AIAgent:**

Copilot uses AI. Check for mistakes.

1. GetNewThread
1. E.g. this would allow passing an already existing storage id for the thread to use, or an initialized custom chat message store to use.
1. DeserializeThread
1. E.g. this would allow passing an already existing storage id for the thread to use, or an initialized custom chat message store to use.
1. Run / RunStreaming
1. E.g. this would allow passing an override chat message store just for that run, or a desired schema for a structured output middleware component.

**MEAI.ChatClient:**

1. GetResponse / GetStreamingResponse

### Feature Layering

One possible feature when adding support for feature collections is to allow layering of features by scope.

The following levels of scope could be supported:

1. Application - Application wide features that apply to all agents / chat clients
2. Artifact (Agent / ChatClient) - Features that apply to all runs of a specific agent or chat client instance
3. Action (GetNewThread / Run / GetResponse) - Feature that apply to a single action only

When retrieving a feature from the collection, the search would start from the most specific scope (Action) and progress to the least specific scope (Application), returning the first matching feature found.

Introducing layering adds some challenges:

- There may be multiple feature collections at the same scope level, e.g. an Agent that uses a ChatClient where both have their own feature collections.
- Do we layer the agent feature collection over the chat client feature collection (Application -> ChatClient -> Agent -> Run), or only use the agent feature collection in the agent (Application -> Agent -> Run), and the chat client feature collection in the chat client (Application -> ChatClient -> Run)?

Copilot AI Jan 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: "chat client feature collection in the chat client" is redundant. Consider rewording for clarity, such as "only use the agent's feature collection within the agent" or similar phrasing that avoids the repetition.

Suggested change
- Do we layer the agent feature collection over the chat client feature collection (Application -> ChatClient -> Agent -> Run), or only use the agent feature collection in the agent (Application -> Agent -> Run), and the chat client feature collection in the chat client (Application -> ChatClient -> Run)?
- Do we layer the agent feature collection over the chat client feature collection (Application -> ChatClient -> Agent -> Run), or keep each artifact's feature collection scoped to itself (Application -> Agent -> Run for the agent, Application -> ChatClient -> Run for the chat client)?

Copilot uses AI. Check for mistakes.
- The appropriate base feature collection may change when progressing down the stack, e.g. when an Agent calls a ChatClient, the action feature collection stays the same, but the artifact feature collection changes.
- Who creates the feature collection hierarchy?
- If the hierarchy changes as it progresses down the execution stack, then the caller can only pass in the action level feature collection, and the callee needs to combine it with its own artifact level feature collection and the application level feature collection. This will require changes to the feature collection type compared to asp.net, so that it can change its base collections as needed.

#### Layering Options

1. No layering - only a single feature collection is supported per action (the caller can still create a layered collection if desired, but the callee does not do any layering automatically).
1. Simple layering - only support layering at the artifact level (Artifact -> Action).
1. Only apply applicable artifact level features when calling into that artifact.
1. Apply upstream artifact features when calling into downstream artifacts, e.g. Feature hierarchy in ChatClientAgent would be `Agent -> Run` and in ChatClient would be `ChatClient -> Agent -> Run` or `Agent -> ChatClient -> Run`
1. Full layering - support layering at all levels (Application -> Artifact -> Action).
1. Only apply applicable artifact level features when calling into that artifact.
1. Apply upstream artifact features when calling into downstream artifacts, e.g. Feature hierarchy in ChatClientAgent would be `Application -> Agent -> Run` and in ChatClient would be `Application -> ChatClient -> Agent -> Run` or `Application -> Agent -> ChatClient -> Run`

#### Accessing application level features Options

1. The user provides the application level feature collection to each artifact that the user constructs
1. Passing the application level feature collection to each artifact is tedious for the user.
1. There is a static application level feature collection that can be accessed globally.
1. Statics create issues with testing and isolation.

### Reconciling with existing AdditionalProperties

If we decide to add feature collections, separately from the existing AdditionalProperties dictionaries, we need to consider how to explain to users when to use each one.
One possible approach though is to have the one use the other under the hood.
AdditionalProperties could be stored as a feature in the feature collection.

Users would be able to retrieve additional properties from the feature collection, in addition to retrieving it via a dedicated AdditionalProperties property.
E.g. `features.Get<AdditionalPropertiesDictionary>()`

One challenge with this approach is that when setting a value in the AdditionalProperties dictionary, the feature collection would need to be created first if it does not already exist.

```csharp
public class AgentRunOptions
{
public AdditionalPropertiesDictionary? AdditionalProperties { get; set; }
public IAgentFeatureCollection? Features { get; set; }
Comment thread
westey-m marked this conversation as resolved.
}

var options = new AgentRunOptions();
// This would need to create the feature collection first, if it does not already exist.
Comment thread
westey-m marked this conversation as resolved.
options.AdditionalProperties ??= new AdditionalPropertiesDictionary();
```

Since IAgentFeatureCollection is an interface, AgentRunOptions would need to have a concrete implementation of the interface to create, meaning that the user cannot decide.
It also means that if the user doesn't realise that AdditionalProperties is implemented using feature collections, they may set a value on AdditionalProperties, and then later overwrite the entire feature collection, losing the AdditionalProperties feature.

Options to avoid these issues:

1. Make `Features` readonly.
1. This would prevent the user from overwriting the feature collection after setting AdditionalProperties.
1. Since the user cannot set their own implementation of IAgentFeatureCollection, having an interface for it may not be necessary.

### Feature Collections vs Mixins

An alternative to feature collections is to use mixins to add optional capabilities to agents or chat clients, where the 'feature' to pass to the agent would be part of the mixin interface.
Mixins have the advantage of being strongly typed and discoverable via interface checks.
However, mixins are less flexible, in that user code and all matching agent implementations need to share the same interface.
This creates a push towards more centralized mixing contracts which limit flexibility.
Comment thread
westey-m marked this conversation as resolved.
Outdated

Combining multiple features together is also more difficult with mixins, as a new mixin interface needs to be created for each combination of features.
1 change: 1 addition & 0 deletions dotnet/agent-framework-dotnet.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@
<File Path="../docs/decisions/0007-agent-filtering-middleware.md" />
<File Path="../docs/decisions/0008-python-subpackages.md" />
<File Path="../docs/decisions/0009-support-long-running-operations.md" />
<File Path="../docs/decisions/00NN-feature-collections.md" />
Comment thread
westey-m marked this conversation as resolved.
Comment thread
westey-m marked this conversation as resolved.

Copilot AI Jan 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The file name uses placeholder numbering "00NN-feature-collections.md". Based on the existing ADR files in the decisions directory, the next sequential number should be 0011, not "00NN". The file should be renamed to "0011-feature-collections.md" and the corresponding reference in the solution file should also be updated.

Suggested change
<File Path="../docs/decisions/00NN-feature-collections.md" />
<File Path="../docs/decisions/0011-feature-collections.md" />

Copilot uses AI. Check for mistakes.

Copilot AI Jan 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The filename uses "00NN" as a placeholder for the ADR number. This should be replaced with the next sequential ADR number in the sequence (appears to be 0010 based on the previous ADR being 0009).

Suggested change
<File Path="../docs/decisions/00NN-feature-collections.md" />
<File Path="../docs/decisions/0010-feature-collections.md" />

Copilot uses AI. Check for mistakes.
<File Path="../docs/decisions/adr-short-template.md" />
<File Path="../docs/decisions/adr-template.md" />
<File Path="../docs/decisions/README.md" />
Expand Down
Loading