Compare commits

..
Author SHA1 Message Date
Shyju Krishnankutty ad4b732741 Adding ReflectExecutors method to Workflow. 2026-01-22 12:07:03 -08:00
1073 changed files with 15162 additions and 43982 deletions
+5 -23
View File
@@ -1,34 +1,16 @@
{
"name": "C# (.NET)",
//"image": "mcr.microsoft.com/devcontainers/dotnet",
// Workaround for https://github.com/devcontainers/images/issues/1752
"build": {
"dockerfile": "dotnet.Dockerfile"
},
"image": "mcr.microsoft.com/devcontainers/dotnet:10.0",
"features": {
"ghcr.io/devcontainers/features/azure-cli:1.2.9": {},
"ghcr.io/devcontainers/features/github-cli:1": {
"version": "2"
},
"ghcr.io/devcontainers/features/powershell:1": {
"version": "latest"
},
"ghcr.io/azure/azure-dev/azd:0": {
"version": "latest"
},
"ghcr.io/devcontainers/features/dotnet:2": {
"version": "none",
"dotnetRuntimeVersions": "10.0",
"aspNetCoreRuntimeVersions": "10.0"
},
"ghcr.io/devcontainers/features/copilot-cli:1": {}
"ghcr.io/devcontainers/features/dotnet:2.4.0": {},
"ghcr.io/devcontainers/features/powershell:1.5.1": {},
"ghcr.io/devcontainers/features/azure-cli:1.2.8": {},
"ghcr.io/devcontainers/features/docker-in-docker:2.12.4": {}
},
"workspaceFolder": "/workspaces/agent-framework/dotnet/",
"customizations": {
"vscode": {
"extensions": [
"GitHub.copilot",
"GitHub.vscode-github-actions",
"ms-dotnettools.csdevkit",
"vscode-icons-team.vscode-icons",
"ms-windows-ai-studio.windows-ai-studio"
-5
View File
@@ -1,5 +0,0 @@
FROM mcr.microsoft.com/devcontainers/universal:latest
# Remove Yarn repository with expired GPG key to prevent apt-get update failures
# Tracking issue: https://github.com/devcontainers/images/issues/1752
RUN rm -f /etc/apt/sources.list.d/yarn.list
-3
View File
@@ -2,6 +2,3 @@
# https://docs.github.com/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
python/packages/azurefunctions/ @microsoft/agentframework-durabletask-developers
python/packages/durabletask/ @microsoft/agentframework-durabletask-developers
python/samples/getting_started/azure_functions/ @microsoft/agentframework-durabletask-developers
python/samples/getting_started/durabletask/ @microsoft/agentframework-durabletask-developers
-2
View File
@@ -14,8 +14,6 @@ Here are some general guidelines that apply to all code.
- The top of all *.cs files should have a copyright notice: `// Copyright (c) Microsoft. All rights reserved.`
- All public methods and classes should have XML documentation comments.
- After adding, modifying or deleting code, run `dotnet build`, and then fix any reported build errors.
- After adding or modifying code, run `dotnet format` to automatically fix any formatting errors.
### C# Sample Code Guidelines
+7 -7
View File
@@ -83,7 +83,7 @@ jobs:
dotnet
python
workflow-samples
# Start Cosmos DB Emulator for all integration tests and only for unit tests when CosmosDB changes happened)
- name: Start Azure Cosmos DB Emulator
if: ${{ runner.os == 'Windows' && (needs.paths-filter.outputs.cosmosDbChanges == 'true' || (github.event_name != 'pull_request' && matrix.integration-tests)) }}
@@ -139,7 +139,7 @@ jobs:
popd
popd
rm -rf "$TEMP_DIR"
- name: Run Unit Tests
shell: bash
run: |
@@ -147,7 +147,7 @@ jobs:
for project in $UT_PROJECTS; do
# Query the project's target frameworks using MSBuild with the current configuration
target_frameworks=$(dotnet msbuild $project -getProperty:TargetFrameworks -p:Configuration=${{ matrix.configuration }} -nologo 2>/dev/null | tr -d '\r')
# Check if the project supports the target framework
if [[ "$target_frameworks" == *"${{ matrix.targetFramework }}"* ]]; then
if [[ "${{ matrix.targetFramework }}" == "${{ env.COVERAGE_FRAMEWORK }}" ]]; then
@@ -165,8 +165,8 @@ jobs:
COSMOSDB_KEY: C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==
- name: Log event name and matrix integration-tests
shell: bash
run: echo "github.event_name:${{ github.event_name }} matrix.integration-tests:${{ matrix.integration-tests }} github.event.action:${{ github.event.action }} github.event.pull_request.merged:${{ github.event.pull_request.merged }}"
shell: bash
run: echo "github.event_name:${{ github.event_name }} matrix.integration-tests:${{ matrix.integration-tests }} github.event.action:${{ github.event.action }} github.event.pull_request.merged:${{ github.event.pull_request.merged }}"
- name: Azure CLI Login
if: github.event_name != 'pull_request' && matrix.integration-tests
@@ -192,10 +192,10 @@ jobs:
for project in $INTEGRATION_TEST_PROJECTS; do
# Query the project's target frameworks using MSBuild with the current configuration
target_frameworks=$(dotnet msbuild $project -getProperty:TargetFrameworks -p:Configuration=${{ matrix.configuration }} -nologo 2>/dev/null | tr -d '\r')
# Check if the project supports the target framework
if [[ "$target_frameworks" == *"${{ matrix.targetFramework }}"* ]]; then
dotnet test -f ${{ matrix.targetFramework }} -c ${{ matrix.configuration }} $project --no-build -v Normal --logger trx --filter "Category!=IntegrationDisabled"
dotnet test -f ${{ matrix.targetFramework }} -c ${{ matrix.configuration }} $project --no-build -v Normal --logger trx
else
echo "Skipping $project - does not support target framework ${{ matrix.targetFramework }} (supports: $target_frameworks)"
fi
-266
View File
@@ -1,266 +0,0 @@
#!/usr/bin/env python3
# Copyright (c) Microsoft. All rights reserved.
"""Check Python test coverage against threshold for enforced modules.
This script parses a Cobertura XML coverage report and enforces a minimum
coverage threshold on specific modules. Non-enforced modules are reported
for visibility but don't block the build.
Usage:
python python-check-coverage.py <coverage-xml-path> <threshold>
Example:
python python-check-coverage.py python-coverage.xml 85
"""
import sys
import xml.etree.ElementTree as ET
from dataclasses import dataclass
# =============================================================================
# ENFORCED MODULES CONFIGURATION
# =============================================================================
# Add or remove modules from this set to control which packages must meet
# the coverage threshold. Only these modules will fail the build if below
# threshold. Other modules are reported for visibility only.
#
# Module paths should match the package paths as they appear in the coverage
# report (e.g., "packages.azure-ai.agent_framework_azure_ai" for packages/azure-ai).
# Sub-modules can be included by specifying their full path.
# =============================================================================
ENFORCED_MODULES: set[str] = {
"packages.azure-ai.agent_framework_azure_ai",
# Add more modules here as coverage improves:
# "packages.core.agent_framework",
# "packages.core.agent_framework._workflows",
# "packages.anthropic.agent_framework_anthropic",
}
@dataclass
class PackageCoverage:
"""Coverage data for a single package."""
name: str
line_rate: float
branch_rate: float
lines_valid: int
lines_covered: int
branches_valid: int
branches_covered: int
@property
def line_coverage_percent(self) -> float:
"""Return line coverage as a percentage."""
return self.line_rate * 100
@property
def branch_coverage_percent(self) -> float:
"""Return branch coverage as a percentage."""
return self.branch_rate * 100
def parse_coverage_xml(xml_path: str) -> tuple[dict[str, PackageCoverage], float, float]:
"""Parse Cobertura XML and extract per-package coverage data.
Args:
xml_path: Path to the Cobertura XML coverage report.
Returns:
A tuple of (packages_dict, overall_line_rate, overall_branch_rate).
"""
tree = ET.parse(xml_path)
root = tree.getroot()
# Get overall coverage from root element
overall_line_rate = float(root.get("line-rate", 0))
overall_branch_rate = float(root.get("branch-rate", 0))
packages: dict[str, PackageCoverage] = {}
for package in root.findall(".//package"):
package_path = package.get("name", "unknown")
line_rate = float(package.get("line-rate", 0))
branch_rate = float(package.get("branch-rate", 0))
# Count lines and branches from classes within this package
lines_valid = 0
lines_covered = 0
branches_valid = 0
branches_covered = 0
for class_elem in package.findall(".//class"):
for line in class_elem.findall(".//line"):
lines_valid += 1
if int(line.get("hits", 0)) > 0:
lines_covered += 1
# Branch coverage from line elements
if line.get("branch") == "true":
condition_coverage = line.get("condition-coverage", "")
if condition_coverage:
# Parse "X% (covered/total)" format
try:
coverage_parts = condition_coverage.split("(")[1].rstrip(")").split("/")
branches_covered += int(coverage_parts[0])
branches_valid += int(coverage_parts[1])
except (IndexError, ValueError):
# Ignore malformed condition-coverage strings; treat this line as having no branch data.
pass
# Use full package path as the key (no aggregation)
packages[package_path] = PackageCoverage(
name=package_path,
line_rate=line_rate if lines_valid == 0 else lines_covered / lines_valid,
branch_rate=branch_rate if branches_valid == 0 else branches_covered / branches_valid,
lines_valid=lines_valid,
lines_covered=lines_covered,
branches_valid=branches_valid,
branches_covered=branches_covered,
)
return packages, overall_line_rate, overall_branch_rate
def format_coverage_value(coverage: float, threshold: float, is_enforced: bool) -> str:
"""Format a coverage value with optional pass/fail indicator.
Args:
coverage: Coverage percentage (0-100).
threshold: Minimum required coverage percentage.
is_enforced: Whether this module is enforced.
Returns:
Formatted string like "85.5%" or "85.5%" or "75.0%".
"""
formatted = f"{coverage:.1f}%"
if is_enforced:
icon = "" if coverage >= threshold else ""
formatted = f"{formatted} {icon}"
return formatted
def print_coverage_table(
packages: dict[str, PackageCoverage],
threshold: float,
overall_line_rate: float,
overall_branch_rate: float,
) -> None:
"""Print a formatted coverage summary table.
Args:
packages: Dictionary of package name to coverage data.
threshold: Minimum required coverage percentage.
overall_line_rate: Overall line coverage rate (0-1).
overall_branch_rate: Overall branch coverage rate (0-1).
"""
print("\n" + "=" * 80)
print("PYTHON TEST COVERAGE REPORT")
print("=" * 80)
# Overall coverage
print(f"\nOverall Line Coverage: {overall_line_rate * 100:.1f}%")
print(f"Overall Branch Coverage: {overall_branch_rate * 100:.1f}%")
print(f"Threshold: {threshold}%")
# Package table
print("\n" + "-" * 110)
print(f"{'Package':<80} {'Lines':<15} {'Line Cov':<15}")
print("-" * 110)
# Sort: enforced modules first, then alphabetically
sorted_packages = sorted(
packages.values(),
key=lambda p: (p.name not in ENFORCED_MODULES, p.name),
)
for pkg in sorted_packages:
is_enforced = pkg.name in ENFORCED_MODULES
enforced_marker = "[ENFORCED] " if is_enforced else ""
line_cov = format_coverage_value(pkg.line_coverage_percent, threshold, is_enforced)
lines_info = f"{pkg.lines_covered}/{pkg.lines_valid}"
package_label = f"{enforced_marker}{pkg.name}"
print(f"{package_label:<80} {lines_info:<15} {line_cov:<15}")
print("-" * 110)
def check_coverage(xml_path: str, threshold: float) -> bool:
"""Check if all enforced modules meet the coverage threshold.
Args:
xml_path: Path to the Cobertura XML coverage report.
threshold: Minimum required coverage percentage.
Returns:
True if all enforced modules pass, False otherwise.
"""
packages, overall_line_rate, overall_branch_rate = parse_coverage_xml(xml_path)
print_coverage_table(packages, threshold, overall_line_rate, overall_branch_rate)
# Check enforced modules
failed_modules: list[str] = []
missing_modules: list[str] = []
for module_name in ENFORCED_MODULES:
if module_name not in packages:
missing_modules.append(module_name)
continue
pkg = packages[module_name]
if pkg.line_coverage_percent < threshold:
failed_modules.append(f"{module_name} ({pkg.line_coverage_percent:.1f}%)")
# Report results
if missing_modules:
print(f"\n❌ FAILED: Enforced modules not found in coverage report: {', '.join(missing_modules)}")
return False
if failed_modules:
print(f"\n❌ FAILED: The following enforced modules are below {threshold}% coverage threshold:")
for module in failed_modules:
print(f" - {module}")
print("\nTo fix: Add more tests to improve coverage for the failing modules.")
return False
if ENFORCED_MODULES:
found_enforced = [m for m in ENFORCED_MODULES if m in packages]
if found_enforced:
print(f"\n✅ PASSED: All enforced modules meet the {threshold}% coverage threshold.")
return True
def main() -> int:
"""Main entry point.
Returns:
Exit code: 0 for success, 1 for failure.
"""
if len(sys.argv) != 3:
print(f"Usage: {sys.argv[0]} <coverage-xml-path> <threshold>")
print(f"Example: {sys.argv[0]} python-coverage.xml 85")
return 1
xml_path = sys.argv[1]
try:
threshold = float(sys.argv[2])
except ValueError:
print(f"Error: Invalid threshold value: {sys.argv[2]}")
return 1
try:
success = check_coverage(xml_path, threshold)
return 0 if success else 1
except FileNotFoundError:
print(f"Error: Coverage file not found: {xml_path}")
return 1
except ET.ParseError as e:
print(f"Error: Failed to parse coverage XML: {e}")
return 1
if __name__ == "__main__":
sys.exit(main())
+1 -1
View File
@@ -97,7 +97,7 @@ jobs:
id: azure-functions-setup
- name: Test with pytest
timeout-minutes: 10
run: uv run poe all-tests -n logical --dist loadfile --dist worksteal --timeout 900 --retries 3 --retry-delay 10
run: uv run poe all-tests -n logical --dist loadfile --dist worksteal --timeout 600 --retries 3 --retry-delay 10
working-directory: ./python
- name: Test core samples
timeout-minutes: 10
@@ -9,8 +9,6 @@ on:
env:
# Configure a constant location for the uv cache
UV_CACHE_DIR: /tmp/.uv-cache
# Coverage threshold percentage for enforced modules
COVERAGE_THRESHOLD: 85
jobs:
python-tests-coverage:
@@ -39,8 +37,6 @@ jobs:
UV_CACHE_DIR: /tmp/.uv-cache
- name: Run all tests with coverage report
run: uv run poe all-tests-cov --cov-report=xml:python-coverage.xml -q --junitxml=pytest.xml
- name: Check coverage threshold
run: python ${{ github.workspace }}/.github/workflows/python-check-coverage.py python-coverage.xml ${{ env.COVERAGE_THRESHOLD }}
- name: Upload coverage report
uses: actions/upload-artifact@v6
with:
+5 -6
View File
@@ -209,14 +209,13 @@ WARP.md
**/tmpclaude*
# Azurite storage emulator files
*/__azurite_db_blob__.json*
*/__azurite_db_blob_extent__.json*
*/__azurite_db_queue__.json*
*/__azurite_db_queue_extent__.json*
*/__azurite_db_table__.json*
*/__azurite_db_blob__.json
*/__azurite_db_blob_extent__.json
*/__azurite_db_queue__.json
*/__azurite_db_queue_extent__.json
*/__azurite_db_table__.json
*/__blobstorage__/
*/__queuestorage__/
*/AzuriteConfig
# Azure Functions local settings
local.settings.json
-423
View File
@@ -1,423 +0,0 @@
---
status: accepted
contact: westey-m
date: 2025-01-21
deciders: sergeymenshykh, markwallace, rbarreto, westey-m, stephentoub
consulted: reubenbond
informed:
---
# 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 - Per Run ChatMessageStore Override for hosting Libraries
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 async Task<string> HandleConversationsBasedRequestAsync(AIAgent agent, string conversationId, string userInput)
{
var thread = await 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)
});
await this._threadStore.SaveThreadAsync(conversationId, thread);
return response.Text;
}
// Pseudo-code for an agent hosting library that supports response id based hosting.
public async Task<(string responseMessage, string responseId)> HandleResponseIdBasedRequestAsync(AIAgent agent, string previousResponseId, string userInput)
{
var thread = await this._threadStore.GetOrCreateThreadAsync(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)
{
await messageStore.SaveBufferedMessagesAsync(newResponseId);
}
// Save the updated thread state under the new response id that was generated by the store.
await this._threadStore.SaveThreadAsync(newResponseId, thread);
return (response.Text, newResponseId);
}
```
### Sample Scenario 2 - Structured output
Currently our base abstraction does not support structured output, since the capability is not supported by all agents.
For those agents that don't support structured output, we could add an agent decorator that takes the response from the underlying agent, and applies structured output parsing on top of it via an additional LLM call.
If we add structured output configuration as a feature, then any agent that supports structured output could retrieve the configuration from the feature collection and apply it, and where it is not supported, the configuration would simply be ignored.
We could add a simple StructuredOutputAgentFeature that can be added to the list of features and also be used to return the generated structured output.
```csharp
internal class StructuredOutputAgentFeature
{
public Type? OutputType { get; set; }
public JsonSerializerOptions? SerializerOptions { get; set; }
public bool? UseJsonSchemaResponseFormat { get; set; }
// Contains the result of the structured output parsing request.
public ChatResponse? ChatResponse { get; set; }
}
```
We can add a simple decorator class that does the chat client invocation.
```csharp
public class StructuredOutputAgent : DelegatingAIAgent
{
private readonly IChatClient _chatClient;
public StructuredOutputAgent(AIAgent innerAgent, IChatClient chatClient)
: base(innerAgent)
{
this._chatClient = Throw.IfNull(chatClient);
}
public override async Task<AgentRunResponse> RunAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default)
{
// Run the inner agent first, to get back the text response we want to convert.
var response = await base.RunAsync(messages, thread, options, cancellationToken).ConfigureAwait(false);
if (options?.Features?.TryGet<StructuredOutputAgentFeature>(out var responseFormatFeature) is true
&& responseFormatFeature.OutputType is not null)
{
// Create the chat options to request structured output.
ChatOptions chatOptions = new()
{
ResponseFormat = ChatResponseFormat.ForJsonSchema(responseFormatFeature.OutputType, responseFormatFeature.SerializerOptions)
};
// Invoke the chat client to transform the text output into structured data.
// The feature is updated with the result.
// The code can be simplified by adding a non-generic structured output GetResponseAsync
// overload that takes Type as input.
responseFormatFeature.ChatResponse = await this._chatClient.GetResponseAsync(
messages: new[]
{
new ChatMessage(ChatRole.System, "You are a json expert and when provided with any text, will convert it to the requested json format."),
new ChatMessage(ChatRole.User, response.Text)
},
options: chatOptions,
cancellationToken: cancellationToken).ConfigureAwait(false);
}
return response;
}
}
```
Finally, we can add an extension method on `AIAgent` that can add the feature to the run options and check the feature for the structured output result and add the deserialized result to the response.
```csharp
public static async Task<AgentRunResponse<T>> RunAsync<T>(
this AIAgent agent,
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
JsonSerializerOptions? serializerOptions = null,
AgentRunOptions? options = null,
bool? useJsonSchemaResponseFormat = null,
CancellationToken cancellationToken = default)
{
// Create the structured output feature.
var structuredOutputFeature = new StructuredOutputAgentFeature();
structuredOutputFeature.OutputType = typeof(T);
structuredOutputFeature.UseJsonSchemaResponseFormat = useJsonSchemaResponseFormat;
// Run the agent.
options ??= new AgentRunOptions();
options.Features ??= new AgentFeatureCollection();
options.Features.Set(structuredOutputFeature);
var response = await agent.RunAsync(messages, thread, options, cancellationToken).ConfigureAwait(false);
// Deserialize the JSON output.
if (structuredOutputFeature.ChatResponse is not null)
{
var typed = new ChatResponse<T>(structuredOutputFeature.ChatResponse, serializerOptions ?? AgentJsonUtilities.DefaultOptions);
return new AgentRunResponse<T>(response, typed.Result);
}
throw new InvalidOperationException("No structured output response was generated by the agent.");
}
```
We can then use the extension method with any agent that supports structured output or that has
been decorated with the `StructuredOutputAgent` decorator.
```csharp
agent = new StructuredOutputAgent(agent, chatClient);
AgentRunResponse<PersonInfo> response = await agent.RunAsync<PersonInfo>([new ChatMessage(
ChatRole.User,
"Please provide information about John Smith, who is a 35-year-old software engineer.")]);
```
## 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|
|Ambiguity with existing AdditionalProperties|❌ Yes|✅ No|❌ Yes|
## IServiceProvider
Service Collections and Service Providers provide a very popular way to register and retrieve services by type and could be used as a way to pass features to agents and chat clients.
However, since IServiceProvider is read-only, it is not possible to modify the registered services when progressing down the execution stack.
E.g. an agent decorator cannot add additional services to the IServiceProvider passed to it when calling into the inner agent.
IServiceProvider also does not expose a way to list all services contained in it, making it difficult to copy services from one provider to another.
This lack of mutability makes IServiceProvider unsuitable for our use case, since we will not be able to use it to build sample scenario 2.
## AdditionalProperties dictionary
The AdditionalProperties dictionary is already available on various options classes in the agent framework as well as in the MEAI stack and
allows storing arbitrary key/value pairs, where the key is a string and the value is an object.
While FeatureCollection uses Type as a key, AdditionalProperties uses string keys.
This means that users need to agree on string keys to use for specific features, however it is also possible to use Type.FullName as a key by convention
to avoid key collisions, which is an easy convention to follow.
Since the value of AdditionalProperties is of type object, users need to cast the value to the expected type when retrieving it, which is also
a drawback, but when using the convention of using Type.FullName as a key, there is at least a clear expectation of what type to cast to.
```csharp
// Setting a feature
options.AdditionalProperties[typeof(MyFeature).FullName] = new MyFeature();
// Retrieving a feature
if (options.AdditionalProperties.TryGetValue(typeof(MyFeature).FullName, out var featureObj)
&& featureObj is MyFeature myFeature)
{
// Use myFeature
}
```
It would also be possible to add extension methods to simplify setting and getting features from AdditionalProperties.
Having a base class for features should help make this more feature rich.
```csharp
// Setting a feature, this can use Type.FullName as the key.
options.AdditionalProperties
.WithFeature(new MyFeature());
// Retrieving a feature, this can use Type.FullName as the key.
if (options.AdditionalProperties.TryGetFeature<MyFeature>(out var myFeature))
{
// Use myFeature
}
```
It would also be possible to add extension methods for a feature to simplify setting and getting features from AdditionalProperties.
```csharp
// Setting a feature
options.AdditionalProperties
.WithMyFeature(new MyFeature());
// Retrieving a feature
if (options.AdditionalProperties.TryGetMyFeature(out var myFeature))
{
// Use myFeature
}
```
## Feature Collection
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:**
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
### 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; }
}
var options = new AgentRunOptions();
// This would need to create the feature collection first, if it does not already exist.
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 Collection Implementation
We have two options for implementing feature collections:
1. Create our own [IAgentFeatureCollection interface](https://github.com/microsoft/agent-framework/pull/2354/files#diff-9c42f3e60d70a791af9841d9214e038c6de3eebfc10e3997cb4cdffeb2f1246d) and [implementation](https://github.com/microsoft/agent-framework/pull/2354/files#diff-a435cc738baec500b8799f7f58c1538e3bb06c772a208afc2615ff90ada3f4ca).
2. Reuse the asp.net [IFeatureCollection interface](https://github.com/dotnet/aspnetcore/blob/main/src/Extensions/Features/src/IFeatureCollection.cs) and [implementation](https://github.com/dotnet/aspnetcore/blob/main/src/Extensions/Features/src/FeatureCollection.cs).
#### Roll our own
Advantages:
Creating our own IAgentFeatureCollection interface and implementation has the advantage of being more clearly associated with the agent framework and allows us to
improve on some of the design decisions made in asp.net core's IFeatureCollection.
Drawbacks:
It would mean a different implementation to maintain and test.
#### Reuse asp.net IFeatureCollection
Advantages:
Reusing the asp.net IFeatureCollection has the advantage of being able to reuse the well-established and tested implementation from asp.net
core. Users who are using agents in an asp.net core application may be able to pass feature collections from asp.net core to the agent framework directly.
Drawbacks:
While the package name is `Microsoft.Extensions.Features`, the namespaces of the types are `Microsoft.AspNetCore.Http.Features`, which may create confusion for users of agent framework who are not building web applications or services.
Users may rightly ask: Why do I need to use a class from asp.net core when I'm not building a web application / service?
The current design has some design issues that would be good to avoid. E.g. it does not distinguish between a feature being "not set" and "null". Get returns both as null and there is no tryget method.
Since the [default implementation](https://github.com/dotnet/aspnetcore/blob/main/src/Extensions/Features/src/FeatureCollection.cs) also supports value types, it throws for null values of value types.
A TryGet method would be more appropriate.
## Feature Layering
One possible scenario 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)?
- 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?
- Since the hierarchy changes as it progresses down the execution stack, and the caller can only pass in the action level feature collection, the callee needs to combine it with its own artifact level feature collection and the application level feature collection. Each action will need to build the appropriate feature collection hierarchy, at the start of its execution.
- For Artifact level features, it seems odd to pass them in as a bag of untyped features, when we are constructing a known artifact type and therefore can have typed settings.
- E.g. today we have a strongly typed setting on ChatClientAgentOptions to configure a ChatMessageStore for the agent.
- To avoid global statics for application level features, the user would need to pass in the application level feature collection to each artifact that they create.
- This would be very odd if the user also already has to strongly typed settings for each feature that they want to set at the artifact level.
### 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. Fallback is to any features configured on the artifact via strongly typed settings.
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`
1. The user needs to provide the application level feature collection to each artifact that they create and artifact features are passed via strongly typed settings.
### Accessing application level features Options
We need to consider how application level features would be accessed if supported.
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.
## Decisions
- Feature Collections Container: Use AdditionalProperties
- Feature Layering: No layering - only a single collection/dictionary is supported per action. Application layers can be added later if needed.
+7 -9
View File
@@ -11,8 +11,8 @@
</PropertyGroup>
<ItemGroup>
<!-- Aspire.* -->
<PackageVersion Include="Anthropic" Version="12.3.0" />
<PackageVersion Include="Anthropic.Foundry" Version="0.4.1" />
<PackageVersion Include="Anthropic" Version="12.0.1" />
<PackageVersion Include="Anthropic.Foundry" Version="0.1.0" />
<PackageVersion Include="Aspire.Azure.AI.OpenAI" Version="13.0.0-preview.1.25560.3" />
<PackageVersion Include="Aspire.Hosting.AppHost" Version="$(AspireAppHostSdkVersion)" />
<PackageVersion Include="Aspire.Hosting.Azure.CognitiveServices" Version="$(AspireAppHostSdkVersion)" />
@@ -37,12 +37,12 @@
<PackageVersion Include="Microsoft.Bcl.HashCode" Version="6.0.0" />
<PackageVersion Include="System.ClientModel" Version="1.8.1" />
<PackageVersion Include="System.CodeDom" Version="10.0.0" />
<PackageVersion Include="System.Collections.Immutable" Version="10.0.1" />
<PackageVersion Include="System.Collections.Immutable" Version="10.0.0" />
<PackageVersion Include="System.CommandLine" Version="2.0.0-rc.2.25502.107" />
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="10.0.2" />
<PackageVersion Include="System.Linq.AsyncEnumerable" Version="10.0.0" />
<PackageVersion Include="System.Net.Http.Json" Version="10.0.0" />
<PackageVersion Include="System.Net.ServerSentEvents" Version="10.0.1" />
<PackageVersion Include="System.Net.ServerSentEvents" Version="10.0.0" />
<PackageVersion Include="System.Text.Json" Version="10.0.2" />
<PackageVersion Include="System.Threading.Channels" Version="10.0.2" />
<PackageVersion Include="System.Threading.Tasks.Extensions" Version="4.6.3" />
@@ -89,7 +89,6 @@
<PackageVersion Include="Microsoft.SemanticKernel.Agents.AzureAI" Version="1.67.0-preview" />
<PackageVersion Include="Microsoft.SemanticKernel.Plugins.OpenApi" Version="1.67.0" />
<!-- Agent SDKs -->
<PackageVersion Include="GitHub.Copilot.SDK" Version="0.1.18" />
<PackageVersion Include="Microsoft.Agents.CopilotStudio.Client" Version="1.3.171-beta" />
<!-- M365 Agents SDK -->
<PackageVersion Include="AdaptiveCards" Version="3.1.0" />
@@ -108,9 +107,9 @@
<!-- Identity -->
<PackageVersion Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.78.0" />
<!-- Workflows -->
<PackageVersion Include="Microsoft.Agents.ObjectModel" Version="2026.1.2.3" />
<PackageVersion Include="Microsoft.Agents.ObjectModel.Json" Version="2026.1.2.3" />
<PackageVersion Include="Microsoft.Agents.ObjectModel.PowerFx" Version="2026.1.2.3" />
<PackageVersion Include="Microsoft.Bot.ObjectModel" Version="1.2025.1106.1" />
<PackageVersion Include="Microsoft.Bot.ObjectModel.Json" Version="1.2025.1106.1" />
<PackageVersion Include="Microsoft.Bot.ObjectModel.PowerFx" Version="1.2025.1106.1" />
<PackageVersion Include="Microsoft.PowerFx.Interpreter" Version="1.5.0-build.20251008-1002" />
<!-- Durable Task -->
<PackageVersion Include="Microsoft.DurableTask.Client" Version="1.18.0" />
@@ -144,7 +143,6 @@
<!-- Symbols -->
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
<!-- Toolset -->
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="10.0.100" />
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers">
+22 -34
View File
@@ -23,29 +23,29 @@
<Project Path="samples/AGUIClientServer/AGUIDojoServer/AGUIDojoServer.csproj" />
<Project Path="samples/AGUIClientServer/AGUIServer/AGUIServer.csproj" />
</Folder>
<Folder Name="/Samples/Durable/" />
<Folder Name="/Samples/Durable/Agents/" />
<Folder Name="/Samples/Durable/Agents/AzureFunctions/">
<File Path="samples/Durable/Agents/AzureFunctions/.editorconfig" />
<File Path="samples/Durable/Agents/AzureFunctions/README.md" />
<Project Path="samples/Durable/Agents/AzureFunctions/01_SingleAgent/01_SingleAgent.csproj" />
<Project Path="samples/Durable/Agents/AzureFunctions/02_AgentOrchestration_Chaining/02_AgentOrchestration_Chaining.csproj" />
<Project Path="samples/Durable/Agents/AzureFunctions/03_AgentOrchestration_Concurrency/03_AgentOrchestration_Concurrency.csproj" />
<Project Path="samples/Durable/Agents/AzureFunctions/04_AgentOrchestration_Conditionals/04_AgentOrchestration_Conditionals.csproj" />
<Project Path="samples/Durable/Agents/AzureFunctions/05_AgentOrchestration_HITL/05_AgentOrchestration_HITL.csproj" />
<Project Path="samples/Durable/Agents/AzureFunctions/06_LongRunningTools/06_LongRunningTools.csproj" />
<Project Path="samples/Durable/Agents/AzureFunctions/07_AgentAsMcpTool/07_AgentAsMcpTool.csproj" />
<Project Path="samples/Durable/Agents/AzureFunctions/08_ReliableStreaming/08_ReliableStreaming.csproj" />
<Folder Name="/Samples/AzureFunctions/">
<File Path="samples/AzureFunctions/.editorconfig" />
<File Path="samples/AzureFunctions/README.md" />
<Project Path="samples/AzureFunctions/01_SingleAgent/01_SingleAgent.csproj" />
<Project Path="samples/AzureFunctions/02_AgentOrchestration_Chaining/02_AgentOrchestration_Chaining.csproj" />
<Project Path="samples/AzureFunctions/03_AgentOrchestration_Concurrency/03_AgentOrchestration_Concurrency.csproj" />
<Project Path="samples/AzureFunctions/04_AgentOrchestration_Conditionals/04_AgentOrchestration_Conditionals.csproj" />
<Project Path="samples/AzureFunctions/05_AgentOrchestration_HITL/05_AgentOrchestration_HITL.csproj" />
<Project Path="samples/AzureFunctions/06_LongRunningTools/06_LongRunningTools.csproj" />
<Project Path="samples/AzureFunctions/07_AgentAsMcpTool/07_AgentAsMcpTool.csproj" />
<Project Path="samples/AzureFunctions/08_ReliableStreaming/08_ReliableStreaming.csproj" />
</Folder>
<Folder Name="/Samples/Durable/Agents/ConsoleApps/">
<File Path="samples/Durable/Agents/ConsoleApps/README.md" />
<Project Path="samples/Durable/Agents/ConsoleApps/01_SingleAgent/01_SingleAgent.csproj" />
<Project Path="samples/Durable/Agents/ConsoleApps/02_AgentOrchestration_Chaining/02_AgentOrchestration_Chaining.csproj" />
<Project Path="samples/Durable/Agents/ConsoleApps/03_AgentOrchestration_Concurrency/03_AgentOrchestration_Concurrency.csproj" />
<Project Path="samples/Durable/Agents/ConsoleApps/04_AgentOrchestration_Conditionals/04_AgentOrchestration_Conditionals.csproj" />
<Project Path="samples/Durable/Agents/ConsoleApps/05_AgentOrchestration_HITL/05_AgentOrchestration_HITL.csproj" />
<Project Path="samples/Durable/Agents/ConsoleApps/06_LongRunningTools/06_LongRunningTools.csproj" />
<Project Path="samples/Durable/Agents/ConsoleApps/07_ReliableStreaming/07_ReliableStreaming.csproj" />
<Folder Name="/Samples/DurableAgents/">
<File Path="samples/DurableAgents/ConsoleApps/README.md" />
</Folder>
<Folder Name="/Samples/DurableAgents/ConsoleApps/">
<Project Path="samples/DurableAgents/ConsoleApps/01_SingleAgent/01_SingleAgent.csproj" />
<Project Path="samples/DurableAgents/ConsoleApps/02_AgentOrchestration_Chaining/02_AgentOrchestration_Chaining.csproj" />
<Project Path="samples/DurableAgents/ConsoleApps/03_AgentOrchestration_Concurrency/03_AgentOrchestration_Concurrency.csproj" />
<Project Path="samples/DurableAgents/ConsoleApps/04_AgentOrchestration_Conditionals/04_AgentOrchestration_Conditionals.csproj" />
<Project Path="samples/DurableAgents/ConsoleApps/05_AgentOrchestration_HITL/05_AgentOrchestration_HITL.csproj" />
<Project Path="samples/DurableAgents/ConsoleApps/06_LongRunningTools/06_LongRunningTools.csproj" />
<Project Path="samples/DurableAgents/ConsoleApps/07_ReliableStreaming/07_ReliableStreaming.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/">
<File Path="samples/GettingStarted/README.md" />
@@ -65,7 +65,6 @@
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_AzureOpenAIChatCompletion/Agent_With_AzureOpenAIChatCompletion.csproj" />
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_AzureOpenAIResponses/Agent_With_AzureOpenAIResponses.csproj" />
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_CustomImplementation/Agent_With_CustomImplementation.csproj" />
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_GitHubCopilot/Agent_With_GitHubCopilot.csproj" />
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_GoogleGemini/Agent_With_GoogleGemini.csproj" />
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_Ollama/Agent_With_Ollama.csproj" />
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_ONNX/Agent_With_ONNX.csproj" />
@@ -230,7 +229,6 @@
<Folder Name="/Samples/GettingStarted/Workflows/Agents/">
<Project Path="samples/GettingStarted/Workflows/Agents/CustomAgentExecutors/CustomAgentExecutors.csproj" />
<Project Path="samples/GettingStarted/Workflows/Agents/FoundryAgent/FoundryAgent.csproj" />
<Project Path="samples/GettingStarted/Workflows/Agents/GroupChatToolApproval/GroupChatToolApproval.csproj" />
<Project Path="samples/GettingStarted/Workflows/Agents/WorkflowAsAnAgent/WorkflowAsAnAgent.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/Workflows/Checkpoint/">
@@ -301,11 +299,6 @@
<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/0010-ag-ui-support.md" />
<File Path="../docs/decisions/0011-create-get-agent-api.md" />
<File Path="../docs/decisions/0012-python-typeddict-options.md" />
<File Path="../docs/decisions/0013-python-get-response-simplification.md" />
<File Path="../docs/decisions/0014-feature-collections.md" />
<File Path="../docs/decisions/adr-short-template.md" />
<File Path="../docs/decisions/adr-template.md" />
<File Path="../docs/decisions/README.md" />
@@ -397,7 +390,6 @@
<Project Path="src/Microsoft.Agents.AI.Abstractions/Microsoft.Agents.AI.Abstractions.csproj" />
<Project Path="src/Microsoft.Agents.AI.AGUI/Microsoft.Agents.AI.AGUI.csproj" />
<Project Path="src/Microsoft.Agents.AI.Anthropic/Microsoft.Agents.AI.Anthropic.csproj" />
<Project Path="src/Microsoft.Agents.AI.GitHub.Copilot/Microsoft.Agents.AI.GitHub.Copilot.csproj" />
<Project Path="src/Microsoft.Agents.AI.AzureAI.Persistent/Microsoft.Agents.AI.AzureAI.Persistent.csproj" />
<Project Path="src/Microsoft.Agents.AI.AzureAI/Microsoft.Agents.AI.AzureAI.csproj" />
<Project Path="src/Microsoft.Agents.AI.CopilotStudio/Microsoft.Agents.AI.CopilotStudio.csproj" />
@@ -417,7 +409,6 @@
<Project Path="src/Microsoft.Agents.AI.Workflows.Declarative.AzureAI/Microsoft.Agents.AI.Workflows.Declarative.AzureAI.csproj" />
<Project Path="src/Microsoft.Agents.AI.Workflows.Declarative/Microsoft.Agents.AI.Workflows.Declarative.csproj" />
<Project Path="src/Microsoft.Agents.AI.Workflows/Microsoft.Agents.AI.Workflows.csproj" />
<Project Path="src/Microsoft.Agents.AI.Workflows.Generators/Microsoft.Agents.AI.Workflows.Generators.csproj" />
<Project Path="src/Microsoft.Agents.AI/Microsoft.Agents.AI.csproj" />
</Folder>
<Folder Name="/Tests/" />
@@ -427,7 +418,6 @@
<Project Path="tests/AzureAI.IntegrationTests/AzureAI.IntegrationTests.csproj" />
<Project Path="tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistent.IntegrationTests.csproj" />
<Project Path="tests/CopilotStudio.IntegrationTests/CopilotStudio.IntegrationTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.GitHub.Copilot.IntegrationTests/Microsoft.Agents.AI.GitHub.Copilot.IntegrationTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/Microsoft.Agents.AI.DurableTask.IntegrationTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests.csproj" />
@@ -442,7 +432,6 @@
<Project Path="tests/Microsoft.Agents.AI.Abstractions.UnitTests/Microsoft.Agents.AI.Abstractions.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.AGUI.UnitTests/Microsoft.Agents.AI.AGUI.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Anthropic.UnitTests/Microsoft.Agents.AI.Anthropic.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.GitHub.Copilot.UnitTests/Microsoft.Agents.AI.GitHub.Copilot.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.AzureAI.Persistent.UnitTests/Microsoft.Agents.AI.AzureAI.Persistent.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.AzureAI.UnitTests/Microsoft.Agents.AI.AzureAI.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.CosmosNoSql.UnitTests/Microsoft.Agents.AI.CosmosNoSql.UnitTests.csproj" />
@@ -459,7 +448,6 @@
<Project Path="tests/Microsoft.Agents.AI.Purview.UnitTests/Microsoft.Agents.AI.Purview.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.UnitTests/Microsoft.Agents.AI.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Workflows.Generators.UnitTests/Microsoft.Agents.AI.Workflows.Generators.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Workflows.UnitTests/Microsoft.Agents.AI.Workflows.UnitTests.csproj" />
</Folder>
</Solution>
-1
View File
@@ -6,7 +6,6 @@
"src\\Microsoft.Agents.AI.Abstractions\\Microsoft.Agents.AI.Abstractions.csproj",
"src\\Microsoft.Agents.AI.AGUI\\Microsoft.Agents.AI.AGUI.csproj",
"src\\Microsoft.Agents.AI.Anthropic\\Microsoft.Agents.AI.Anthropic.csproj",
"src\\Microsoft.Agents.AI.GitHub.Copilot\\Microsoft.Agents.AI.GitHub.Copilot.csproj",
"src\\Microsoft.Agents.AI.AzureAI.Persistent\\Microsoft.Agents.AI.AzureAI.Persistent.csproj",
"src\\Microsoft.Agents.AI.AzureAI\\Microsoft.Agents.AI.AzureAI.csproj",
"src\\Microsoft.Agents.AI.CopilotStudio\\Microsoft.Agents.AI.CopilotStudio.csproj",
+3 -3
View File
@@ -2,9 +2,9 @@
<PropertyGroup>
<!-- Central version prefix - applies to all nuget packages. -->
<VersionPrefix>1.0.0</VersionPrefix>
<PackageVersion Condition="'$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).260128.1</PackageVersion>
<PackageVersion Condition="'$(VersionSuffix)' == ''">$(VersionPrefix)-preview.260128.1</PackageVersion>
<GitTag>1.0.0-preview.260128.1</GitTag>
<PackageVersion Condition="'$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).260121.1</PackageVersion>
<PackageVersion Condition="'$(VersionSuffix)' == ''">$(VersionPrefix)-preview.260121.1</PackageVersion>
<GitTag>1.0.0-preview.260121.1</GitTag>
<Configurations>Debug;Release;Publish</Configurations>
<IsPackable>true</IsPackable>
@@ -42,7 +42,7 @@ public static class Program
// Create the Host agent
var hostAgent = new HostClientAgent(loggerFactory);
await hostAgent.InitializeAgentAsync(modelId, apiKey, agentUrls!.Split(";"));
AgentSession session = await hostAgent.Agent!.GetNewSessionAsync(cancellationToken);
AgentThread thread = await hostAgent.Agent!.GetNewThreadAsync(cancellationToken);
try
{
while (true)
@@ -61,7 +61,7 @@ public static class Program
break;
}
var agentResponse = await hostAgent.Agent!.RunAsync(message, session, cancellationToken: cancellationToken);
var agentResponse = await hostAgent.Agent!.RunAsync(message, thread, cancellationToken: cancellationToken);
foreach (var chatMessage in agentResponse.Messages)
{
Console.ForegroundColor = ConsoleColor.Cyan;
@@ -88,7 +88,7 @@ public static class Program
description: "AG-UI Client Agent",
tools: [changeBackground, readClientClimateSensors]);
AgentSession session = await agent.GetNewSessionAsync(cancellationToken);
AgentThread thread = await agent.GetNewThreadAsync(cancellationToken);
List<ChatMessage> messages = [new(ChatRole.System, "You are a helpful assistant.")];
try
{
@@ -112,23 +112,23 @@ public static class Program
// Call RunStreamingAsync to get streaming updates
bool isFirstUpdate = true;
string? sessionId = null;
string? threadId = null;
var updates = new List<ChatResponseUpdate>();
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(messages, session, cancellationToken: cancellationToken))
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(messages, thread, cancellationToken: cancellationToken))
{
// Use AsChatResponseUpdate to access ChatResponseUpdate properties
ChatResponseUpdate chatUpdate = update.AsChatResponseUpdate();
updates.Add(chatUpdate);
if (chatUpdate.ConversationId != null)
{
sessionId = chatUpdate.ConversationId;
threadId = chatUpdate.ConversationId;
}
// Display run started information from the first update
if (isFirstUpdate && sessionId != null && update.ResponseId != null)
if (isFirstUpdate && threadId != null && update.ResponseId != null)
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"\n[Run Started - Session: {sessionId}, Run: {update.ResponseId}]");
Console.WriteLine($"\n[Run Started - Thread: {threadId}, Run: {update.ResponseId}]");
Console.ResetColor();
isFirstUpdate = false;
}
@@ -177,7 +177,7 @@ public static class Program
var lastUpdate = updates[^1];
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine();
Console.WriteLine($"[Run Ended - Session: {sessionId}, Run: {lastUpdate.ResponseId}]");
Console.WriteLine($"[Run Ended - Thread: {threadId}, Run: {lastUpdate.ResponseId}]");
Console.ResetColor();
}
messages.Clear();
@@ -19,21 +19,21 @@ internal sealed class AgenticUIAgent : DelegatingAIAgent
this._jsonSerializerOptions = jsonSerializerOptions;
}
protected override Task<AgentResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentSession? session = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
protected override Task<AgentResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
return this.RunCoreStreamingAsync(messages, session, options, cancellationToken).ToAgentResponseAsync(cancellationToken);
return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken).ToAgentResponseAsync(cancellationToken);
}
protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session = null,
AgentThread? thread = null,
AgentRunOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
// Track function calls that should trigger state events
var trackedFunctionCalls = new Dictionary<string, FunctionCallContent>();
await foreach (var update in this.InnerAgent.RunStreamingAsync(messages, session, options, cancellationToken).ConfigureAwait(false))
await foreach (var update in this.InnerAgent.RunStreamingAsync(messages, thread, options, cancellationToken).ConfigureAwait(false))
{
// Process contents: track function calls and emit state events for results
List<AIContent> stateEventsToEmit = new();
@@ -20,21 +20,21 @@ internal sealed class PredictiveStateUpdatesAgent : DelegatingAIAgent
this._jsonSerializerOptions = jsonSerializerOptions;
}
protected override Task<AgentResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentSession? session = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
protected override Task<AgentResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
return this.RunCoreStreamingAsync(messages, session, options, cancellationToken).ToAgentResponseAsync(cancellationToken);
return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken).ToAgentResponseAsync(cancellationToken);
}
protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session = null,
AgentThread? thread = null,
AgentRunOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
// Track the last emitted document state to avoid duplicates
string? lastEmittedDocument = null;
await foreach (var update in this.InnerAgent.RunStreamingAsync(messages, session, options, cancellationToken).ConfigureAwait(false))
await foreach (var update in this.InnerAgent.RunStreamingAsync(messages, thread, options, cancellationToken).ConfigureAwait(false))
{
// Check if we're seeing a write_document tool call and emit predictive state
bool hasToolCall = false;
@@ -19,21 +19,21 @@ internal sealed class SharedStateAgent : DelegatingAIAgent
this._jsonSerializerOptions = jsonSerializerOptions;
}
protected override Task<AgentResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentSession? session = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
protected override Task<AgentResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
return this.RunCoreStreamingAsync(messages, session, options, cancellationToken).ToAgentResponseAsync(cancellationToken);
return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken).ToAgentResponseAsync(cancellationToken);
}
protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session = null,
AgentThread? thread = null,
AgentRunOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
if (options is not ChatClientAgentRunOptions { ChatOptions.AdditionalProperties: { } properties } chatRunOptions ||
!properties.TryGetValue("ag_ui_state", out JsonElement state))
{
await foreach (var update in this.InnerAgent.RunStreamingAsync(messages, session, options, cancellationToken).ConfigureAwait(false))
await foreach (var update in this.InnerAgent.RunStreamingAsync(messages, thread, options, cancellationToken).ConfigureAwait(false))
{
yield return update;
}
@@ -64,7 +64,7 @@ internal sealed class SharedStateAgent : DelegatingAIAgent
var firstRunMessages = messages.Append(stateUpdateMessage);
var allUpdates = new List<AgentResponseUpdate>();
await foreach (var update in this.InnerAgent.RunStreamingAsync(firstRunMessages, session, firstRunOptions, cancellationToken).ConfigureAwait(false))
await foreach (var update in this.InnerAgent.RunStreamingAsync(firstRunMessages, thread, firstRunOptions, cancellationToken).ConfigureAwait(false))
{
allUpdates.Add(update);
@@ -98,7 +98,7 @@ internal sealed class SharedStateAgent : DelegatingAIAgent
ChatRole.System,
[new TextContent("Please provide a concise summary of the state changes in at most two sentences.")]));
await foreach (var update in this.InnerAgent.RunStreamingAsync(secondRunMessages, session, options, cancellationToken).ConfigureAwait(false))
await foreach (var update in this.InnerAgent.RunStreamingAsync(secondRunMessages, thread, options, cancellationToken).ConfigureAwait(false))
{
yield return update;
}
@@ -36,7 +36,7 @@ var pirateAgentBuilder = builder.AddAIAgent(
chatClientServiceKey: "chat-model")
.WithAITool(new CustomAITool())
.WithAITool(new CustomFunctionTool())
.WithInMemorySessionStore();
.WithInMemoryThreadStore();
var knightsKnavesAgentBuilder = builder.AddAIAgent("knights-and-knaves", (sp, key) =>
{
@@ -28,13 +28,13 @@ internal sealed class A2AAgentClient : AgentClientBase
public override async IAsyncEnumerable<AgentResponseUpdate> RunStreamingAsync(
string agentName,
IList<ChatMessage> messages,
string? sessionId = null,
string? threadId = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
this._logger.LogInformation("Running agent {AgentName} with {MessageCount} messages via A2A", agentName, messages.Count);
var (a2aClient, _) = this.ResolveClient(agentName);
var contextId = sessionId ?? Guid.NewGuid().ToString("N");
var contextId = threadId ?? Guid.NewGuid().ToString("N");
// Convert and send messages via A2A without try-catch in yield method
var results = new List<AgentResponseUpdate>();
@@ -16,13 +16,13 @@ internal abstract class AgentClientBase
/// </summary>
/// <param name="agentName">The name of the agent to run.</param>
/// <param name="messages">The messages to send to the agent.</param>
/// <param name="sessionId">Optional session identifier for conversation continuity.</param>
/// <param name="threadId">Optional thread identifier for conversation continuity.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>An asynchronous enumerable of agent response updates.</returns>
public abstract IAsyncEnumerable<AgentResponseUpdate> RunStreamingAsync(
string agentName,
IList<ChatMessage> messages,
string? sessionId = null,
string? threadId = null,
CancellationToken cancellationToken = default);
/// <summary>
@@ -34,3 +34,16 @@ internal abstract class AgentClientBase
public virtual Task<AgentCard?> GetAgentCardAsync(string agentName, CancellationToken cancellationToken = default)
=> Task.FromResult<AgentCard?>(null);
}
/// <summary>
/// Helper class to create a thread-like wrapper for agent clients.
/// </summary>
public class AgentClientThread
{
public string ThreadId { get; }
public AgentClientThread(string? threadId = null)
{
this.ThreadId = threadId ?? Guid.NewGuid().ToString("N");
}
}
@@ -19,7 +19,7 @@ internal sealed class OpenAIChatCompletionsAgentClient(HttpClient httpClient) :
public override async IAsyncEnumerable<AgentResponseUpdate> RunStreamingAsync(
string agentName,
IList<ChatMessage> messages,
string? sessionId = null,
string? threadId = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
OpenAIClientOptions options = new()
@@ -18,7 +18,7 @@ internal sealed class OpenAIResponsesAgentClient(HttpClient httpClient) : AgentC
public override async IAsyncEnumerable<AgentResponseUpdate> RunStreamingAsync(
string agentName,
IList<ChatMessage> messages,
string? sessionId = null,
string? threadId = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
OpenAIClientOptions options = new()
@@ -30,7 +30,7 @@ internal sealed class OpenAIResponsesAgentClient(HttpClient httpClient) : AgentC
var openAiClient = new ResponsesClient(model: agentName, credential: new ApiKeyCredential("dummy-key"), options: options).AsIChatClient();
var chatOptions = new ChatOptions()
{
ConversationId = sessionId
ConversationId = threadId
};
await foreach (var update in openAiClient.GetStreamingResponseAsync(messages, chatOptions, cancellationToken: cancellationToken))
@@ -36,7 +36,7 @@
</ItemGroup>
-->
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
</Project>
@@ -7,4 +7,4 @@
"AZURE_OPENAI_ENDPOINT": "<AZURE_OPENAI_ENDPOINT>",
"AZURE_OPENAI_DEPLOYMENT": "<AZURE_OPENAI_DEPLOYMENT>"
}
}
}
@@ -36,7 +36,7 @@
</ItemGroup>
-->
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
</Project>
@@ -19,15 +19,15 @@ public static class FunctionTriggers
public static async Task<string> RunOrchestrationAsync([OrchestrationTrigger] TaskOrchestrationContext context)
{
DurableAIAgent writer = context.GetAgent("WriterAgent");
AgentSession writerSession = await writer.GetNewSessionAsync();
AgentThread writerThread = await writer.GetNewThreadAsync();
AgentResponse<TextResponse> initial = await writer.RunAsync<TextResponse>(
message: "Write a concise inspirational sentence about learning.",
session: writerSession);
thread: writerThread);
AgentResponse<TextResponse> refined = await writer.RunAsync<TextResponse>(
message: $"Improve this further while keeping it under 25 words: {initial.Result.Text}",
session: writerSession);
thread: writerThread);
return refined.Result.Text;
}
@@ -21,7 +21,7 @@ AzureOpenAIClient client = !string.IsNullOrEmpty(azureOpenAiKey)
? new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(azureOpenAiKey))
: new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential());
// Single agent used by the orchestration to demonstrate sequential calls on the same session.
// Single agent used by the orchestration to demonstrate sequential calls on the same thread.
const string WriterName = "WriterAgent";
const string WriterInstructions =
"""
@@ -1,11 +1,11 @@
# Single Agent Orchestration Sample
This sample demonstrates how to use the Durable Agent Framework (DAFx) to create a simple Azure Functions app that orchestrates sequential calls to a single AI agent using the same session for context continuity.
This sample demonstrates how to use the Durable Agent Framework (DAFx) to create a simple Azure Functions app that orchestrates sequential calls to a single AI agent using the same conversation thread for context continuity.
## Key Concepts Demonstrated
- Orchestrating multiple interactions with the same agent in a deterministic order
- Using the same `AgentSession` across multiple calls to maintain conversational context
- Using the same `AgentThread` across multiple calls to maintain conversational context
- Durable orchestration with automatic checkpointing and resumption from failures
- HTTP API integration for starting and monitoring orchestrations
@@ -36,7 +36,7 @@
</ItemGroup>
-->
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
</Project>
@@ -36,7 +36,7 @@
</ItemGroup>
-->
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
</Project>
@@ -21,7 +21,7 @@ public static class FunctionTriggers
// Get the spam detection agent
DurableAIAgent spamDetectionAgent = context.GetAgent("SpamDetectionAgent");
AgentSession spamSession = await spamDetectionAgent.GetNewSessionAsync();
AgentThread spamThread = await spamDetectionAgent.GetNewThreadAsync();
// Step 1: Check if the email is spam
AgentResponse<DetectionResult> spamDetectionResponse = await spamDetectionAgent.RunAsync<DetectionResult>(
@@ -31,7 +31,7 @@ public static class FunctionTriggers
Email ID: {email.EmailId}
Content: {email.EmailContent}
""",
session: spamSession);
thread: spamThread);
DetectionResult result = spamDetectionResponse.Result;
// Step 2: Conditional logic based on spam detection result
@@ -43,7 +43,7 @@ public static class FunctionTriggers
// Generate and send response for legitimate email
DurableAIAgent emailAssistantAgent = context.GetAgent("EmailAssistantAgent");
AgentSession emailSession = await emailAssistantAgent.GetNewSessionAsync();
AgentThread emailThread = await emailAssistantAgent.GetNewThreadAsync();
AgentResponse<EmailResponse> emailAssistantResponse = await emailAssistantAgent.RunAsync<EmailResponse>(
message:
@@ -53,7 +53,7 @@ public static class FunctionTriggers
Email ID: {email.EmailId}
Content: {email.EmailContent}
""",
session: emailSession);
thread: emailThread);
EmailResponse emailResponse = emailAssistantResponse.Result;
@@ -37,7 +37,7 @@
</ItemGroup>
-->
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
</Project>
@@ -24,7 +24,7 @@ public static class FunctionTriggers
// Get the writer agent
DurableAIAgent writerAgent = context.GetAgent("WriterAgent");
AgentSession writerSession = await writerAgent.GetNewSessionAsync();
AgentThread writerThread = await writerAgent.GetNewThreadAsync();
// Set initial status
context.SetCustomStatus($"Starting content generation for topic: {input.Topic}");
@@ -32,7 +32,7 @@ public static class FunctionTriggers
// Step 1: Generate initial content
AgentResponse<GeneratedContent> writerResponse = await writerAgent.RunAsync<GeneratedContent>(
message: $"Write a short article about '{input.Topic}'.",
session: writerSession);
thread: writerThread);
GeneratedContent content = writerResponse.Result;
// Human-in-the-loop iteration - we set a maximum number of attempts to avoid infinite loops
@@ -81,7 +81,7 @@ public static class FunctionTriggers
Human Feedback: {humanResponse.Feedback}
""",
session: writerSession);
thread: writerThread);
content = writerResponse.Result;
}
@@ -36,7 +36,7 @@
</ItemGroup>
-->
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
</Project>
@@ -20,7 +20,7 @@ public static class FunctionTriggers
// Get the writer agent
DurableAIAgent writerAgent = context.GetAgent("Writer");
AgentSession writerSession = await writerAgent.GetNewSessionAsync();
AgentThread writerThread = await writerAgent.GetNewThreadAsync();
// Set initial status
context.SetCustomStatus($"Starting content generation for topic: {input.Topic}");
@@ -28,7 +28,7 @@ public static class FunctionTriggers
// Step 1: Generate initial content
AgentResponse<GeneratedContent> writerResponse = await writerAgent.RunAsync<GeneratedContent>(
message: $"Write a short article about '{input.Topic}'.",
session: writerSession);
thread: writerThread);
GeneratedContent content = writerResponse.Result;
// Human-in-the-loop iteration - we set a maximum number of attempts to avoid infinite loops
@@ -102,7 +102,7 @@ public static class FunctionTriggers
Human Feedback: {humanResponse.Feedback}
""",
session: writerSession);
thread: writerThread);
content = writerResponse.Result;
}
@@ -47,7 +47,7 @@ internal sealed class Tools(ILogger<Tools> logger)
{
this._logger.LogInformation("Getting status for workflow instance: {InstanceId}", instanceId);
// Get the current agent context using the session-static property
// Get the current agent context using the thread-static property
OrchestrationMetadata? status = await DurableAgentContext.Current.GetOrchestrationStatusAsync(
instanceId,
includeDetails);
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net10.0</TargetFrameworks>
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
@@ -36,7 +36,7 @@
</ItemGroup>
-->
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
</Project>
@@ -41,7 +41,7 @@
</ItemGroup>
-->
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
</Project>
@@ -94,15 +94,15 @@ public sealed class FunctionTriggers
AIAgent agentProxy = durableClient.AsDurableAgentProxy(context, "TravelPlanner");
// Create a new agent session
AgentSession session = await agentProxy.GetNewSessionAsync(cancellationToken);
string agentSessionId = session.GetService<AgentSessionId>().ToString();
// Create a new agent thread
AgentThread thread = await agentProxy.GetNewThreadAsync(cancellationToken);
string agentSessionId = thread.GetService<AgentSessionId>().ToString();
this._logger.LogInformation("Creating new agent session: {AgentSessionId}", agentSessionId);
// Run the agent in the background (fire-and-forget)
DurableAgentRunOptions options = new() { IsFireAndForget = true };
await agentProxy.RunAsync(prompt, session, options, cancellationToken);
await agentProxy.RunAsync(prompt, thread, options, cancellationToken);
this._logger.LogInformation("Agent run started for session: {AgentSessionId}", agentSessionId);
@@ -65,9 +65,9 @@ public sealed class RedisStreamResponseHandler : IAgentResponseHandler
"DurableAgentContext.Current is not set. This handler must be used within a durable agent context.");
}
// Get session ID from the current session context, which is only available in the context of
// Get session ID from the current thread context, which is only available in the context of
// a durable agent execution.
string agentSessionId = context.CurrentSession.GetService<AgentSessionId>().ToString();
string agentSessionId = context.CurrentThread.GetService<AgentSessionId>().ToString();
string streamKey = GetStreamKey(agentSessionId);
IDatabase db = this._redis.GetDatabase();
@@ -24,7 +24,7 @@
</ItemGroup>
-->
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.DurableTask\Microsoft.Agents.AI.DurableTask.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.DurableTask\Microsoft.Agents.AI.DurableTask.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
</Project>
@@ -60,8 +60,8 @@ Console.ResetColor();
Console.WriteLine("Enter a message for the Joker agent (or 'exit' to quit):");
Console.WriteLine();
// Create a session for the conversation
AgentSession session = await agentProxy.GetNewSessionAsync();
// Create a thread for the conversation
AgentThread thread = await agentProxy.GetNewThreadAsync();
while (true)
{
@@ -85,7 +85,7 @@ while (true)
{
AgentResponse agentResponse = await agentProxy.RunAsync(
message: input,
session: session,
thread: thread,
cancellationToken: CancellationToken.None);
Console.WriteLine(agentResponse.Text);
@@ -24,7 +24,7 @@
</ItemGroup>
-->
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.DurableTask\Microsoft.Agents.AI.DurableTask.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.DurableTask\Microsoft.Agents.AI.DurableTask.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
</Project>
@@ -33,7 +33,7 @@ AzureOpenAIClient client = !string.IsNullOrEmpty(azureOpenAiKey)
? new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(azureOpenAiKey))
: new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential());
// Single agent used by the orchestration to demonstrate sequential calls on the same session.
// Single agent used by the orchestration to demonstrate sequential calls on the same thread.
const string WriterName = "WriterAgent";
const string WriterInstructions =
"""
@@ -47,15 +47,15 @@ AIAgent writerAgent = client.GetChatClient(deploymentName).AsAIAgent(WriterInstr
static async Task<string> RunOrchestratorAsync(TaskOrchestrationContext context)
{
DurableAIAgent writer = context.GetAgent("WriterAgent");
AgentSession writerSession = await writer.GetNewSessionAsync();
AgentThread writerThread = await writer.GetNewThreadAsync();
AgentResponse<TextResponse> initial = await writer.RunAsync<TextResponse>(
message: "Write a concise inspirational sentence about learning.",
session: writerSession);
thread: writerThread);
AgentResponse<TextResponse> refined = await writer.RunAsync<TextResponse>(
message: $"Improve this further while keeping it under 25 words: {initial.Result.Text}",
session: writerSession);
thread: writerThread);
return refined.Result.Text;
}
@@ -1,11 +1,11 @@
# Single Agent Orchestration Sample
This sample demonstrates how to use the durable agents extension to create a simple console app that orchestrates sequential calls to a single AI agent using the same session for context continuity.
This sample demonstrates how to use the durable agents extension to create a simple console app that orchestrates sequential calls to a single AI agent using the same conversation thread for context continuity.
## Key Concepts Demonstrated
- Orchestrating multiple interactions with the same agent in a deterministic order
- Using the same `AgentSession` across multiple calls to maintain conversational context
- Using the same `AgentThread` across multiple calls to maintain conversational context
- Durable orchestration with automatic checkpointing and resumption from failures
- Waiting for orchestration completion using `WaitForInstanceCompletionAsync`
@@ -24,7 +24,7 @@
</ItemGroup>
-->
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.DurableTask\Microsoft.Agents.AI.DurableTask.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.DurableTask\Microsoft.Agents.AI.DurableTask.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
</Project>
@@ -24,7 +24,7 @@
</ItemGroup>
-->
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.DurableTask\Microsoft.Agents.AI.DurableTask.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.DurableTask\Microsoft.Agents.AI.DurableTask.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
</Project>
@@ -56,7 +56,7 @@ static async Task<string> RunOrchestratorAsync(TaskOrchestrationContext context,
{
// Get the spam detection agent
DurableAIAgent spamDetectionAgent = context.GetAgent(SpamDetectionAgentName);
AgentSession spamSession = await spamDetectionAgent.GetNewSessionAsync();
AgentThread spamThread = await spamDetectionAgent.GetNewThreadAsync();
// Step 1: Check if the email is spam
AgentResponse<DetectionResult> spamDetectionResponse = await spamDetectionAgent.RunAsync<DetectionResult>(
@@ -66,7 +66,7 @@ static async Task<string> RunOrchestratorAsync(TaskOrchestrationContext context,
Email ID: {email.EmailId}
Content: {email.EmailContent}
""",
session: spamSession);
thread: spamThread);
DetectionResult result = spamDetectionResponse.Result;
// Step 2: Conditional logic based on spam detection result
@@ -78,7 +78,7 @@ static async Task<string> RunOrchestratorAsync(TaskOrchestrationContext context,
// Generate and send response for legitimate email
DurableAIAgent emailAssistantAgent = context.GetAgent(EmailAssistantAgentName);
AgentSession emailSession = await emailAssistantAgent.GetNewSessionAsync();
AgentThread emailThread = await emailAssistantAgent.GetNewThreadAsync();
AgentResponse<EmailResponse> emailAssistantResponse = await emailAssistantAgent.RunAsync<EmailResponse>(
message:
@@ -88,7 +88,7 @@ static async Task<string> RunOrchestratorAsync(TaskOrchestrationContext context,
Email ID: {email.EmailId}
Content: {email.EmailContent}
""",
session: emailSession);
thread: emailThread);
EmailResponse emailResponse = emailAssistantResponse.Result;

Some files were not shown because too many files have changed in this diff Show More