diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 0000000..f898596
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,19 @@
+{
+ "name": "Python 3",
+ "image": "mcr.microsoft.com/devcontainers/python:3.13-bullseye",
+ "features": {
+ "ghcr.io/va-h/devcontainers-features/uv:1": {},
+ "ghcr.io/devcontainers/features/azure-cli:1.2.8": {}
+ },
+ "postCreateCommand": "bash ./devsetup.sh",
+ "workspaceFolder": "/workspaces/agent-framework/python/",
+ "customizations": {
+ "vscode": {
+ "extensions": [
+ "ms-python.python",
+ "ms-windows-ai-studio.windows-ai-studio",
+ "littlefoxteam.vscode-python-test-adapter"
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/.devcontainer/dotnet/devcontainer.json b/.devcontainer/dotnet/devcontainer.json
new file mode 100644
index 0000000..59b56a4
--- /dev/null
+++ b/.devcontainer/dotnet/devcontainer.json
@@ -0,0 +1,20 @@
+{
+ "name": "C# (.NET)",
+ "image": "mcr.microsoft.com/devcontainers/dotnet:10.0",
+ "features": {
+ "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": [
+ "ms-dotnettools.csdevkit",
+ "vscode-icons-team.vscode-icons",
+ "ms-windows-ai-studio.windows-ai-studio"
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..0123be9
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,5 @@
+# Auto-detect text files, ensure they use LF.
+* text=auto eol=lf working-tree-encoding=UTF-8
+# Bash scripts
+*.sh text eol=lf
+*.cmd text eol=crlf
diff --git a/.github/.linkspector.yml b/.github/.linkspector.yml
new file mode 100644
index 0000000..eb365c2
--- /dev/null
+++ b/.github/.linkspector.yml
@@ -0,0 +1,32 @@
+dirs:
+ - .
+excludedFiles:
+ - ./python/CHANGELOG.md
+ignorePatterns:
+ - pattern: "/github/"
+ - pattern: "./actions"
+ - pattern: "./blob"
+ - pattern: "./issues"
+ - pattern: "./discussions"
+ - pattern: "./pulls"
+ - pattern: "https:\/\/platform.openai.com"
+ - pattern: "http:\/\/localhost"
+ - pattern: "http:\/\/127.0.0.1"
+ - pattern: "https:\/\/localhost"
+ - pattern: "https:\/\/127.0.0.1"
+ - pattern: "0001-spec.md"
+ - pattern: "0001-madr-architecture-decisions.md"
+ - pattern: "https://api.powerplatform.com/.default"
+ - pattern: "https://your-resource.openai.azure.com/"
+ - pattern: "http://host.docker.internal"
+ - pattern: "https://openai.github.io/openai-agents-js/openai/agents/classes/"
+# excludedDirs:
+ # Folders which include links to localhost, since it's not ignored with regular expressions
+baseUrl: https://github.com/microsoft/agent-framework/
+aliveStatusCodes:
+ - 200
+ - 206
+ - 429
+ - 500
+ - 503
+useGitIgnore: true
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 0000000..66023c6
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1,4 @@
+# Code ownership assignments
+# https://docs.github.com/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
+
+python/packages/azurefunctions/ @microsoft/agentframework-durabletask-developers
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000..29aae92
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,8 @@
+blank_issues_enabled: true
+contact_links:
+ - name: Documentation
+ url: https://aka.ms/agent-framework
+ about: Check out the official documentation for guides and API reference.
+ - name: Discussions
+ url: https://github.com/microsoft/agent-framework/discussions
+ about: Ask questions about Agent Framework.
diff --git a/.github/ISSUE_TEMPLATE/dotnet-issue.yml b/.github/ISSUE_TEMPLATE/dotnet-issue.yml
new file mode 100644
index 0000000..3e02fd9
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/dotnet-issue.yml
@@ -0,0 +1,70 @@
+name: .NET Bug Report
+description: Report a bug in the Agent Framework .NET SDK
+title: ".NET: [Bug]: "
+labels: ["bug", ".NET"]
+type: bug
+body:
+ - type: textarea
+ id: description
+ attributes:
+ label: Description
+ description: Please provide a clear and detailed description of the bug.
+ placeholder: |
+ - What happened?
+ - What did you expect to happen?
+ - Steps to reproduce the issue
+ validations:
+ required: true
+
+ - type: textarea
+ id: code-sample
+ attributes:
+ label: Code Sample
+ description: If applicable, provide a minimal code sample that demonstrates the issue.
+ placeholder: |
+ ```csharp
+ // Your code here
+ ```
+ render: markdown
+ validations:
+ required: false
+
+ - type: textarea
+ id: error-messages
+ attributes:
+ label: Error Messages / Stack Traces
+ description: Include any error messages or stack traces you received.
+ placeholder: |
+ ```
+ Paste error messages or stack traces here
+ ```
+ render: markdown
+ validations:
+ required: false
+
+ - type: input
+ id: dotnet-packages
+ attributes:
+ label: Package Versions
+ description: List the Microsoft.Agents.* packages and versions you are using
+ placeholder: "e.g., Microsoft.Agents.AI.Abstractions: 1.0.0, Microsoft.Agents.AI.OpenAI: 1.0.0"
+ validations:
+ required: true
+
+ - type: input
+ id: dotnet-version
+ attributes:
+ label: .NET Version
+ description: What version of .NET are you using?
+ placeholder: "e.g., .NET 8.0"
+ validations:
+ required: false
+
+ - type: textarea
+ id: additional-context
+ attributes:
+ label: Additional Context
+ description: Add any other context or screenshots that might be helpful.
+ placeholder: "Any additional information..."
+ validations:
+ required: false
diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml
new file mode 100644
index 0000000..1dc1318
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature-request.yml
@@ -0,0 +1,51 @@
+name: Feature Request
+description: Request a new feature for Microsoft Agent Framework
+title: "[Feature]: "
+type: feature
+body:
+
+ - type: textarea
+ id: description
+ attributes:
+ label: Description
+ description: Please describe the feature you'd like and why it would be useful.
+ placeholder: |
+ Describe the feature you're requesting:
+ - What problem does it solve?
+ - What would the expected behavior be?
+ - Are there any alternatives you've considered?
+ validations:
+ required: true
+
+ - type: textarea
+ id: code-sample
+ attributes:
+ label: Code Sample
+ description: If applicable, provide a code sample showing how you'd like to use this feature.
+ placeholder: |
+ ```python
+ # Your code here
+ ```
+
+ or
+
+ ```csharp
+ // Your code here
+ ```
+ render: markdown
+ validations:
+ required: false
+
+ - type: dropdown
+ id: language
+ attributes:
+ label: Language/SDK
+ description: Which language/SDK does this feature apply to?
+ options:
+ - Both
+ - .NET
+ - Python
+ - Other / Not Applicable
+ default: 0
+ validations:
+ required: false
diff --git a/.github/ISSUE_TEMPLATE/python-issue.yml b/.github/ISSUE_TEMPLATE/python-issue.yml
new file mode 100644
index 0000000..3a506c6
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/python-issue.yml
@@ -0,0 +1,70 @@
+name: Python Bug Report
+description: Report a bug in the Agent Framework Python SDK
+title: "Python: [Bug]: "
+labels: ["bug", "Python"]
+type: bug
+body:
+ - type: textarea
+ id: description
+ attributes:
+ label: Description
+ description: Please provide a clear and detailed description of the bug.
+ placeholder: |
+ - What happened?
+ - What did you expect to happen?
+ - Steps to reproduce the issue
+ validations:
+ required: true
+
+ - type: textarea
+ id: code-sample
+ attributes:
+ label: Code Sample
+ description: If applicable, provide a minimal code sample that demonstrates the issue.
+ placeholder: |
+ ```python
+ # Your code here
+ ```
+ render: markdown
+ validations:
+ required: false
+
+ - type: textarea
+ id: error-messages
+ attributes:
+ label: Error Messages / Stack Traces
+ description: Include any error messages or stack traces you received.
+ placeholder: |
+ ```
+ Paste error messages or stack traces here
+ ```
+ render: markdown
+ validations:
+ required: false
+
+ - type: input
+ id: python-packages
+ attributes:
+ label: Package Versions
+ description: List the agent-framework-* packages and versions you are using
+ placeholder: "e.g., agent-framework-core: 1.0.0, agent-framework-azure-ai: 1.0.0"
+ validations:
+ required: true
+
+ - type: input
+ id: python-version
+ attributes:
+ label: Python Version
+ description: What version of Python are you using?
+ placeholder: "e.g., Python 3.11"
+ validations:
+ required: false
+
+ - type: textarea
+ id: additional-context
+ attributes:
+ label: Additional Context
+ description: Add any other context or screenshots that might be helpful.
+ placeholder: "Any additional information..."
+ validations:
+ required: false
diff --git a/.github/actions/azure-functions-integration-setup/action.yml b/.github/actions/azure-functions-integration-setup/action.yml
new file mode 100644
index 0000000..28c1c6c
--- /dev/null
+++ b/.github/actions/azure-functions-integration-setup/action.yml
@@ -0,0 +1,48 @@
+name: Azure Functions Integration Test Setup
+description: Prepare local emulators and tools for Azure Functions integration tests
+
+runs:
+ using: "composite"
+ steps:
+ - name: Start Durable Task Scheduler Emulator
+ shell: bash
+ run: |
+ if [ "$(docker ps -aq -f name=dts-emulator)" ]; then
+ echo "Stopping and removing existing Durable Task Scheduler Emulator"
+ docker rm -f dts-emulator
+ fi
+ echo "Starting Durable Task Scheduler Emulator"
+ docker run -d --name dts-emulator -p 8080:8080 -p 8082:8082 -e DTS_USE_DYNAMIC_TASK_HUBS=true mcr.microsoft.com/dts/dts-emulator:latest
+ echo "Waiting for Durable Task Scheduler Emulator to be ready"
+ timeout 30 bash -c 'until curl --silent http://localhost:8080/healthz; do sleep 1; done'
+ echo "Durable Task Scheduler Emulator is ready"
+ - name: Start Azurite (Azure Storage emulator)
+ shell: bash
+ run: |
+ if [ "$(docker ps -aq -f name=azurite)" ]; then
+ echo "Stopping and removing existing Azurite (Azure Storage emulator)"
+ docker rm -f azurite
+ fi
+ echo "Starting Azurite (Azure Storage emulator)"
+ docker run -d --name azurite -p 10000:10000 -p 10001:10001 -p 10002:10002 mcr.microsoft.com/azure-storage/azurite
+ echo "Waiting for Azurite (Azure Storage emulator) to be ready"
+ timeout 30 bash -c 'until curl --silent http://localhost:10000/devstoreaccount1; do sleep 1; done'
+ echo "Azurite (Azure Storage emulator) is ready"
+ - name: Start Redis
+ shell: bash
+ run: |
+ if [ "$(docker ps -aq -f name=redis)" ]; then
+ echo "Stopping and removing existing Redis"
+ docker rm -f redis
+ fi
+ echo "Starting Redis"
+ docker run -d --name redis -p 6379:6379 redis:latest
+ echo "Waiting for Redis to be ready"
+ timeout 30 bash -c 'until docker exec redis redis-cli ping | grep -q PONG; do sleep 1; done'
+ echo "Redis is ready"
+ - name: Install Azure Functions Core Tools
+ shell: bash
+ run: |
+ echo "Installing Azure Functions Core Tools"
+ npm install -g azure-functions-core-tools@4 --unsafe-perm true
+ func --version
diff --git a/.github/actions/python-setup/action.yml b/.github/actions/python-setup/action.yml
new file mode 100644
index 0000000..7850392
--- /dev/null
+++ b/.github/actions/python-setup/action.yml
@@ -0,0 +1,25 @@
+name: Reusable Setup UV
+description: Reusable workflow to setup uv environment
+
+inputs:
+ python-version:
+ description: The Python version to set up
+ required: true
+ os:
+ description: The operating system to set up
+ required: true
+
+runs:
+ using: "composite"
+ steps:
+ - name: Set up uv
+ uses: astral-sh/setup-uv@v6
+ with:
+ version-file: "python/pyproject.toml"
+ enable-cache: true
+ cache-suffix: ${{ inputs.os }}-${{ inputs.python-version }}
+ cache-dependency-glob: "**/uv.lock"
+ - name: Install the project
+ shell: bash
+ run: |
+ cd python && uv sync --all-packages --all-extras --dev -U --prerelease=if-necessary-or-explicit
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
new file mode 100644
index 0000000..5866f1f
--- /dev/null
+++ b/.github/copilot-instructions.md
@@ -0,0 +1,69 @@
+# GitHub Copilot Instructions
+
+This repository contains both Python and C# code.
+All python code resides under the `python/` directory.
+All C# code resides under the `dotnet/` directory.
+
+The purpose of the code is to provide a framework for building AI agents.
+
+When contributing to this repository, please follow these guidelines:
+
+## C# Code Guidelines
+
+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
+
+Sample code is located in the `dotnet/samples` directory.
+
+When adding a new sample, follow these steps:
+
+- The sample should be a standalone .net project in one of the subdirectories of the samples directory.
+- The directory name should be the same as the project name.
+- The directory should contain a README.md file that explains what the sample does and how to run it.
+- The README.md file should follow the same format as other samples.
+- The csproj file should match the directory name.
+- The csproj file should be configured in the same way as other samples.
+- The project should preferably contain a single Program.cs file that contains all the sample code.
+- The sample should be added to the solution file in the samples directory.
+- The sample should be tested to ensure it works as expected.
+- A reference to the new samples should be added to the README.md file in the parent directory of the new sample.
+
+The sample code should follow these guidelines:
+
+- Configuration settings should be read from environment variables, e.g. `var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");`.
+- Environment variables should use upper snake_case naming convention.
+- Secrets should not be hardcoded in the code or committed to the repository.
+- The code should be well-documented with comments explaining the purpose of each step.
+- The code should be simple and to the point, avoiding unnecessary complexity.
+- Prefer inline literals over constants for values that are not reused. For example, use `new ChatClientAgent(chatClient, instructions: "You are a helpful assistant.")` instead of defining a constant for "instructions".
+- Ensure that all private classes are sealed
+- Use the Async suffix on the name of all async methods that return a Task or ValueTask.
+- Prefer defining variables using types rather than var, to help users understand the types involved.
+- Follow the patterns in the samples in the same directories where new samples are being added.
+- The structure of the sample should be as follows:
+ - The top of the Program.cs should have a copyright notice: `// Copyright (c) Microsoft. All rights reserved.`
+ - Then add a comment describing what the sample is demonstrating.
+ - Then add the necessary using statements.
+ - Then add the main code logic.
+ - Finally, add any helper methods or classes at the bottom of the file.
+
+### C# Unit Test Guidelines
+
+Unit tests are located in the `dotnet/tests` directory in projects with a `.UnitTests.csproj` suffix.
+
+Unit tests should follow these guidelines:
+
+- Use `this.` for accessing class members
+- Add Arrange, Act and Assert comments for each test
+- Ensure that all private classes, that are not subclassed, are sealed
+- Use the Async suffix on the name of all async methods
+- Use the Moq library for mocking objects where possible
+- Validate that each test actually tests the target behavior, e.g. we should not have tests that creates a mock, calls the mock and then verifies that the mock was called, without the target code being involved. We also shouldn't have tests that test language features, e.g. something that the compiler would catch anyway.
+- Avoid adding excessive comments to tests. Instead favour clear easy to understand code.
+- Follow the patterns in the unit tests in the same project or classes to which new tests are being added
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..90b127a
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,52 @@
+# To get started with Dependabot version updates, you'll need to specify which
+# package ecosystems to update and where the package manifests are located.
+# Please see the documentation for all configuration options:
+# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
+
+version: 2
+updates:
+ # Maintain dependencies for nuget
+ - package-ecosystem: "nuget"
+ directory: "dotnet/"
+ schedule:
+ interval: "cron"
+ cronjob: "0 8 * * 4,0" # Every Thursday(4) and Sunday(0) at 8:00 UTC
+ ignore:
+ # For all System.* and Microsoft.Extensions/Bcl.* packages, ignore all major version updates
+ - dependency-name: "System.*"
+ update-types: ["version-update:semver-major"]
+ - dependency-name: "Microsoft.Extensions.*"
+ update-types: ["version-update:semver-major"]
+ - dependency-name: "Microsoft.Bcl.*"
+ update-types: ["version-update:semver-major"]
+ - dependency-name: "Moq"
+ labels:
+ - ".NET"
+ - "dependencies"
+
+ # Maintain dependencies for python
+ - package-ecosystem: "pip"
+ directory: "python/"
+ schedule:
+ interval: "weekly"
+ day: "monday"
+ labels:
+ - "python"
+ - "dependencies"
+ - package-ecosystem: "uv"
+ directory: "python/"
+ schedule:
+ interval: "weekly"
+ day: "monday"
+ labels:
+ - "python"
+ - "dependencies"
+
+ # Maintain dependencies for github-actions
+ - package-ecosystem: "github-actions"
+ # Workflow files stored in the
+ # default location of `.github/workflows`
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ day: "sunday"
diff --git a/.github/instructions/durabletask-dotnet.instructions.md b/.github/instructions/durabletask-dotnet.instructions.md
new file mode 100644
index 0000000..84aeb54
--- /dev/null
+++ b/.github/instructions/durabletask-dotnet.instructions.md
@@ -0,0 +1,17 @@
+---
+applyTo: "dotnet/src/Microsoft.Agents.AI.DurableTask/**,dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/**"
+---
+
+# Durable Task area code instructions
+
+The following guidelines apply to pull requests that modify files under
+`dotnet/src/Microsoft.Agents.AI.DurableTask/**` or
+`dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/**`:
+
+## CHANGELOG.md
+
+- Each pull request that modifies code should add just one bulleted entry to the `CHANGELOG.md` file containing a change title (usually the PR title) and a link to the PR itself.
+- New PRs should be added to the top of the `CHANGELOG.md` file under a "## [Unreleased]" heading.
+- If the PR is the first since the last release, the existing "## [Unreleased]" heading should be replaced with a "## v[X.Y.Z]" heading and the PRs since the last release should be added to the new "## [Unreleased]" heading.
+- The style of new `CHANGELOG.md` entries should match the style of the other entries in the file.
+- If the PR introduces a breaking change, the changelog entry should be prefixed with "[BREAKING]".
diff --git a/.github/labeler.yml b/.github/labeler.yml
new file mode 100644
index 0000000..5663961
--- /dev/null
+++ b/.github/labeler.yml
@@ -0,0 +1,34 @@
+# Add 'python' label to any change within the 'python' directory
+python:
+- changed-files:
+ - any-glob-to-any-file:
+ - python/**
+
+# Add '.NET' label to any change within samples or kernel 'dotnet' directories.
+.NET:
+- changed-files:
+ - any-glob-to-any-file:
+ - dotnet/**
+
+# Add 'documentation' label to any change within the 'docs' directory, or any '.md' files
+documentation:
+- changed-files:
+ - any-glob-to-any-file:
+ - docs/**
+ - '**/*.md'
+
+# Add 'workflows' label to any change within the dotnet or python workflows src or samples
+workflows:
+- changed-files:
+ - any-glob-to-any-file:
+ - dotnet/src/Microsoft.Agents.AI.Workflows/**
+ - dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/**
+ - dotnet/samples/GettingStarted/Workflow/**
+ - python/packages/main/agent_framework/_workflow/**
+ - python/samples/getting_started/workflow/**
+
+# Add 'lab' label to any change within the 'python/packages/lab' directory
+lab:
+- changed-files:
+ - any-glob-to-any-file:
+ - python/packages/lab/**
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 0000000..6658ebc
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,23 @@
+### Motivation and Context
+
+
+
+### Description
+
+
+
+### Contribution Checklist
+
+
+
+- [ ] The code builds clean without any errors or warnings
+- [ ] The PR follows the [Contribution Guidelines](https://github.com/microsoft/agent-framework/blob/main/CONTRIBUTING.md)
+- [ ] All unit tests pass, and I have added new tests where possible
+- [ ] **Is this a breaking change?** If yes, add "[BREAKING]" prefix to the title of the PR.
\ No newline at end of file
diff --git a/.github/upgrades/prompts/SemanticKernelToAgentFramework.md b/.github/upgrades/prompts/SemanticKernelToAgentFramework.md
new file mode 100644
index 0000000..6ff0984
--- /dev/null
+++ b/.github/upgrades/prompts/SemanticKernelToAgentFramework.md
@@ -0,0 +1,1611 @@
+# Instructions for migrating from Semantic Kernel Agents to Agent Framework in .NET projects.
+
+## Scope
+
+When you are asked to migrate a project from `Microsoft.SemanticKernel.Agents` to `Microsoft.Agents.AI` you need to determine for which projects you need to do it.
+If a single project is specified - do it for that project only. If you are asked to do it for a solution, migrate all projects in the solution
+that reference `Microsoft.SemanticKernel.Agents` or related Semantic Kernel agent packages. If you don't know which projects to migrate, ask the user.
+
+## Things to consider while doing migration
+
+- NuGet package names, assembly names, projects names or other dependencies names are case insensitive(!). You ***must take it into account*** when doing something
+ with project dependencies, like searching for dependencies or when removing them from projects etc.
+- Agent Framework uses different namespace patterns and API structures compared to Semantic Kernel Agents
+- Text-based heuristics should be avoided in favor of proper content type inspection when available.
+
+## Planning
+
+For each project that needs to be migrated, you need to do the following:
+
+
+- Find projects depending on `Microsoft.SemanticKernel.Agents` or related Semantic Kernel agent packages (when searching for projects, if some projects are not part of the
+ solution or you could not find the project, notify user and continue with other projects).
+- Identify the specific Semantic Kernel agent types being used:
+ - `ChatCompletionAgent` → `ChatClientAgent`
+ - `OpenAIAssistantAgent` → `assistantsClient.CreateAIAgent()` (via OpenAI Assistants client extension)
+ - `AzureAIAgent` → `persistentAgentsClient.CreateAIAgent()` (via Azure AI Foundry client extension)
+ - `OpenAIResponseAgent` → `responsesClient.CreateAIAgent()` (via OpenAI Responses client extension)
+ - `A2AAgent` → `AIAgent` (via A2A card resolver)
+ - `BedrockAgent` → Custom implementation required (not supported)
+- Determine if agents are being created new or retrieved from hosted services:
+ - **New agents**: Use `CreateAIAgent()` methods
+ - **Existing hosted agents**: Use `GetAIAgent(agentId)` methods for OpenAI Assistants and Azure AI Foundry
+
+
+- Determine the AI provider being used (OpenAI, Azure OpenAI, Azure AI Foundry, etc.)
+- Analyze tool/function registration patterns
+- Review thread management and invocation patterns
+
+## Execution
+
+***Important***: when running steps in this section you must not pause, you must continue until you are done with all steps or you are truly unable to
+continue and need user's interaction (you will be penalized if you stop unnecessarily).
+
+Keep in mind information in the next section about differences and follow these steps in the order they are specified (you will be penalized if you do steps
+below in wrong order or skip any of them):
+
+1. For each project that has an explicit package dependency to Semantic Kernel agent packages in the project file or some imported MSBuild targets (some
+ project could receive package dependencies transitively, so avoid adding new package dependencies for such projects), do the following:
+
+- Remove the Semantic Kernel agent package references from the project file:
+ - `Microsoft.SemanticKernel.Agents.Core`
+ - `Microsoft.SemanticKernel.Agents.OpenAI`
+ - `Microsoft.SemanticKernel.Agents.AzureAI`
+ - `Microsoft.SemanticKernel` (if only used for agents)
+- Add the appropriate Agent Framework package references based on the provider being used:
+ - `Microsoft.Agents.AI.Abstractions` (always required)
+ - `Microsoft.Agents.AI.OpenAI` (for OpenAI and Azure OpenAI providers)
+ - For unsupported providers (Bedrock, CopilotStudio), note in the report that custom implementation is required
+- If projects use Central Package Management, update the `Directory.Packages.props` file to remove the Semantic Kernel agent package versions in addition to
+ removing package reference from projects.
+ When adding the Agent Framework PackageReferences, add them to affected project files without a version and add PackageVersion elements to the
+ Directory.Packages.props file with the version that supports the project's target framework.
+
+2. Update code files using Semantic Kernel Agents in the selected projects (and in projects that depend on them since they could receive Semantic Kernel transitively):
+
+- Find ***all*** code files in the selected projects (and in projects that depend on them since they could receive Semantic Kernel transitively).
+ When doing search of code files that need changes, prefer calling search tools with `upgrade_` prefix if available. Also do pass project's root folder for all
+ selected projects or projects that depend on them.
+- Update the code files that use Semantic Kernel Agents to use Agent Framework instead. You never should add placeholders when updating code, or remove any comments in the code files,
+ you must keep the business logic as close as possible to the original code but use new API. When checking if code file needs to be updated, you should check for
+ using statements, types and API from `Microsoft.SemanticKernel.Agents` namespace (skip comments and string literal constants).
+- Ensure that you replace all Semantic Kernel agent using statements with Agent Framework using statements (always check if there are any other Semantic Kernel agent
+ API used in the file having any of the Semantic Kernel agent using statements; if no other API detected, Semantic Kernel agent using statements should be just removed
+ instead of replaced). If there were no Semantic Kernel agent using statements in the file, do not add Agent Framework using statements.
+- When replacing types you must ensure that you add using statements for them, since some types that lived in main `Microsoft.SemanticKernel.Agents` namespace live in other namespaces
+ under `Microsoft.Agents.AI`. For example, `Microsoft.SemanticKernel.Agents.ChatCompletionAgent` is replaced with `Microsoft.Agents.AI.ChatClientAgent`, when that
+ happens using statement with `Microsoft.Agents.AI` needs to be added (unless you use fully qualified type name)
+- If you see some code that really cannot be converted or will have potential behavior changes at runtime, remember files and code lines where it
+ happens at the end of the migration process you will generate a report markdown file and list all follow up steps user would have to do.
+
+3. Validate that all places where Semantic Kernel Agents were used are migrated. To do that search for `Microsoft.SemanticKernel.Agents` in all affected projects and projects that depend
+ on them again and if still see any Semantic Kernel agent presence go back to step 2. Steps 2 and 3 should be repeated until you see no Semantic Kernel agent references.
+
+4. Build all modified projects to ensure that they compile without errors. If there are any build errors, you must fix them all yourself one by one and
+ don't stop until all errors are fixed without breaking any of the migration guidance.
+
+5. **Validate Migration**: Use the validation checklist below to ensure complete migration.
+
+6. Generate the report file under `\.github folder`, the file name should be `SemanticKernelToAgentFrameworkReport.md`, it is highly important that
+ you generate report when migration complete. Report should contain:
+ - all project dependencies changes (mention what was changed, added or removed, including provider-specific packages)
+ - all code files that were changed (mention what was changed in the file, if it was not changed, just mention that the file was not changed)
+ - provider-specific migration patterns used (OpenAI, Azure OpenAI, Azure AI Foundry, A2A, ONNX, etc.)
+ - all cases where you could not convert the code because of unsupported features and you were unable to find a workaround
+ - unsupported providers that require custom implementation (Bedrock, CopilotStudio)
+ - breaking glass pattern migrations (InnerContent → RawRepresentation) and any CodeInterpreter or advanced tool usage
+ - all behavioral changes that have to be verified at runtime
+ - provider-specific configuration changes that may affect behavior
+ - all follow up steps that user would have to do in the report markdown file
+
+## Migration Validation Checklist
+
+After completing migration, verify these specific items:
+
+1. **Compilation**: Execute `dotnet build` on all modified projects - zero errors required
+2. **Namespace Updates**: Confirm all `using Microsoft.SemanticKernel.Agents` statements are replaced
+3. **Method Calls**: Verify all `InvokeAsync` calls are changed to `RunAsync`
+4. **Return Types**: Confirm handling of `AgentResponse` instead of `IAsyncEnumerable>`
+5. **Thread Creation**: Validate all thread creation uses `agent.GetNewThread()` pattern
+6. **Tool Registration**: Ensure `[KernelFunction]` attributes are removed and `AIFunctionFactory.Create()` is used
+7. **Options Configuration**: Verify `AgentRunOptions` or `ChatClientAgentRunOptions` replaces `AgentInvokeOptions`
+8. **Breaking Glass**: Test `RawRepresentation` access replaces `InnerContent` access
+
+## Detailed information about differences in Semantic Kernel Agents and Agent Framework
+
+
+Agent Framework provides functionality for creating and managing AI agents through the Microsoft.Extensions.AI package ecosystem. The framework uses different APIs and patterns compared to Semantic Kernel Agents.
+
+Key API differences:
+- Agent creation: Remove Kernel dependency, use direct client-based creation
+- Method names: `InvokeAsync` → `RunAsync`, `InvokeStreamingAsync` → `RunStreamingAsync`
+- Return types: `IAsyncEnumerable>` → `AgentResponse`
+- Thread creation: Provider-specific constructors → `agent.GetNewThread()`
+- Tool registration: `KernelPlugin` system → Direct `AIFunction` registration
+- Options: `AgentInvokeOptions` → Provider-specific run options (e.g., `ChatClientAgentRunOptions`)
+
+
+
+Configuration patterns have changed from Kernel-based to direct client configuration:
+- Remove `Kernel.CreateBuilder()` patterns
+- Replace with provider-specific client creation
+- Update namespace imports from `Microsoft.SemanticKernel.Agents` to `Microsoft.Agents.AI`
+- Change tool registration from attribute-based to factory-based
+
+
+### Exact API Mappings
+
+
+Replace these Semantic Kernel agent classes with their Agent Framework equivalents:
+
+| Semantic Kernel Class | Agent Framework Replacement | Constructor Changes |
+|----------------------|----------------------------|-------------------|
+| `IChatCompletionService` | `IChatClient` | Convert to `IChatClient` using `chatService.AsChatClient()` extensions |
+| `ChatCompletionAgent` | `ChatClientAgent` | Remove `Kernel` parameter, add `IChatClient` parameter |
+| `OpenAIAssistantAgent` | `AIAgent` (via extension) | ⚠️ **Deprecated** - Use Responses API instead. **New**: `OpenAIClient.GetAssistantClient().CreateAIAgent()` **Existing**: `OpenAIClient.GetAssistantClient().GetAIAgent(assistantId)` |
+| `AzureAIAgent` | `AIAgent` (via extension) | **New**: `PersistentAgentsClient.CreateAIAgent()` **Existing**: `PersistentAgentsClient.GetAIAgent(agentId)` |
+| `OpenAIResponseAgent` | `AIAgent` (via extension) | Replace with `OpenAIClient.GetOpenAIResponseClient(modelId).CreateAIAgent()` |
+| `A2AAgent` | `AIAgent` (via extension) | Replace with `A2ACardResolver.GetAIAgentAsync()` |
+| `BedrockAgent` | Not supported | Custom implementation required |
+
+**Important distinction:**
+- **CreateAIAgent()**: Use when creating new agents in the hosted service
+- **GetAIAgent(agentId)**: Use when retrieving existing agents from the hosted service
+
+
+
+Replace these method calls:
+
+| Semantic Kernel Method | Agent Framework Method | Parameter Changes |
+|----------------------|----------------------|------------------|
+| `agent.InvokeAsync(message, thread, options)` | `agent.RunAsync(message, thread, options)` | Same parameters, different return type |
+| `agent.InvokeStreamingAsync(message, thread, options)` | `agent.RunStreamingAsync(message, thread, options)` | Same parameters, different return type |
+| `new ChatHistoryAgentThread()` | `agent.GetNewThread()` | No parameters needed |
+| `new OpenAIAssistantAgentThread(client)` | `agent.GetNewThread()` | No parameters needed |
+| `new AzureAIAgentThread(client)` | `agent.GetNewThread()` | No parameters needed |
+| `thread.DeleteAsync()` | Provider-specific cleanup | Use provider client directly |
+
+Return type changes:
+- `IAsyncEnumerable>` → `AgentResponse`
+- `IAsyncEnumerable` → `IAsyncEnumerable`
+
+
+
+Replace these configuration patterns:
+
+| Semantic Kernel Pattern | Agent Framework Pattern |
+|------------------------|------------------------|
+| `AgentInvokeOptions` | `AgentRunOptions` **ChatClientAgent**: `ChatClientAgentRunOptions` |
+| `KernelArguments` | If no arguments are provided, do nothing. If arguments are provided, template is not supported and the prompt must be rendered before calling agent |
+| `[KernelFunction]` attribute | Remove attribute, use `AIFunctionFactory.Create()` |
+| `KernelPlugin` registration | Direct function list in agent creation |
+| `InnerContent` property | `RawRepresentation` property |
+| `content.Metadata` property | `AdditionalProperties` property |
+
+
+
+### Functional Differences
+
+Agent Framework changes these behaviors compared to Semantic Kernel Agents:
+
+1. **Thread Management**: Agent Framework automatically manages thread state. Semantic Kernel required manual thread updates in some scenarios (e.g., OpenAI Responses).
+
+2. **Return Types**:
+ - Non-streaming: Returns single `AgentResponse` instead of `IAsyncEnumerable>`
+ - Streaming: Returns `IAsyncEnumerable` instead of `IAsyncEnumerable`
+
+3. **Tool Registration**: Agent Framework uses direct function registration without requiring `[KernelFunction]` attributes.
+
+4. **Usage Metadata**: Agent Framework provides unified `UsageDetails` access via `response.Usage` and `update.Contents.OfType()`.
+
+5. **Breaking Glass**: Access underlying SDK objects via `RawRepresentation` instead of `InnerContent`.
+
+
+### Namespace Updates
+
+
+Replace these exact namespace imports:
+
+**Remove these Semantic Kernel namespaces:**
+```csharp
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Agents;
+using Microsoft.SemanticKernel.Agents.OpenAI;
+using Microsoft.SemanticKernel.Agents.AzureAI;
+using Microsoft.SemanticKernel.Agents.A2A;
+using Microsoft.SemanticKernel.Connectors.OpenAI;
+```
+
+**Add these Agent Framework namespaces:**
+```csharp
+using Microsoft.Extensions.AI;
+using Microsoft.Agents.AI;
+// Provider-specific namespaces (add only if needed):
+using OpenAI; // For OpenAI provider
+using Azure.AI.OpenAI; // For Azure OpenAI provider
+using Azure.AI.Agents.Persistent; // For Azure AI Foundry provider
+using Azure.Identity; // For Azure authentication
+```
+
+
+### Chat Completion Abstractions
+
+
+
+**Replace this Semantic Kernel pattern:**
+```csharp
+Kernel kernel = Kernel.CreateBuilder()
+ .AddOpenAIChatCompletion(modelId, apiKey)
+ .Build();
+
+ChatCompletionAgent agent = new()
+{
+ Instructions = "You are a helpful assistant",
+ Kernel = kernel
+};
+```
+
+**With this Agent Framework pattern:**
+```csharp
+// Method 1: Direct constructor
+IChatClient chatClient = new OpenAIClient(apiKey).GetChatClient(modelId).AsIChatClient();
+AIAgent agent = new ChatClientAgent(chatClient, instructions: "You are a helpful assistant");
+
+// Method 2: Extension method (recommended)
+AIAgent agent = new OpenAIClient(apiKey)
+ .GetChatClient(modelId)
+ .CreateAIAgent(instructions: "You are a helpful assistant");
+```
+
+
+### Chat Completion Service
+
+
+
+**Replace this Semantic Kernel pattern:**
+
+```csharp
+IChatCompletionService completionService = kernel.GetService();
+
+ChatCompletionAgent agent = new()
+{
+ Instructions = "You are a helpful assistant",
+ Kernel = kernel
+};
+```
+
+**With this Agent Framework pattern:**
+
+Agent Framework does not support `IChatCompletionService` directly. Instead, use `IChatClient` as the common abstraction
+converting from `IChatCompletionService` to `IChatClient` via `AsChatClient()` extension method or creating a new `IChatClient`
+ instance directly using the provider package dedicated extensions.
+
+```csharp
+IChatCompletionService completionService = kernel.GetService();
+IChatClient chatClient = completionService.AsChatClient();
+
+var agent = new ChatClientAgent(chatClient, instructions: "You are a helpful assistant");
+```
+
+
+### Agent Creation Transformation
+
+
+
+**Replace this Semantic Kernel pattern:**
+```csharp
+Kernel kernel = Kernel.CreateBuilder()
+ .AddOpenAIChatClient(modelId, apiKey)
+ .Build();
+
+ChatCompletionAgent agent = new()
+{
+ Instructions = "You are a helpful assistant",
+ Kernel = kernel
+};
+```
+
+**With this Agent Framework pattern:**
+```csharp
+// Method 1: Direct constructor (OpenAI/AzureOpenAI Package specific)
+IChatClient chatClient = new OpenAIClient(apiKey).GetChatClient(modelId).AsIChatClient();
+AIAgent agent = new ChatClientAgent(chatClient, instructions: "You are a helpful assistant");
+
+// Method 2: Extension method (recommended)
+AIAgent agent = new OpenAIClient(apiKey)
+ .GetChatClient(modelId)
+ .CreateAIAgent(instructions: "You are a helpful assistant");
+```
+
+**Required changes:**
+1. Remove `Kernel.CreateBuilder()` and `.Build()` calls
+2. Replace `ChatCompletionAgent` with `ChatClientAgent` or use extension methods
+3. Remove `Kernel` property assignment
+4. Pass `IChatClient` directly to constructor or use extension methods
+
+
+### Thread Management Transformation
+
+
+**Replace these Semantic Kernel thread creation patterns:**
+```csharp
+// Remove these provider-specific thread constructors:
+AgentThread thread = new ChatHistoryAgentThread();
+AgentThread thread = new OpenAIAssistantAgentThread(assistantClient);
+AgentThread thread = new AzureAIAgentThread(azureClient);
+```
+
+**With this unified Agent Framework pattern:**
+```csharp
+// Use this single pattern for all agent types:
+AgentThread thread = agent.GetNewThread();
+```
+
+**Required changes:**
+1. Remove all `new [Provider]AgentThread()` constructor calls
+2. Replace with `agent.GetNewThread()` method call
+3. Remove provider client parameters from thread creation
+4. Use the same pattern regardless of agent provider type
+
+
+### Tool Registration Transformation
+
+
+**Replace this Semantic Kernel tool registration pattern:**
+```csharp
+[KernelFunction] // Remove this attribute
+[Description("Get the weather for a location")]
+static string GetWeather(string location) => $"Weather in {location}";
+
+KernelFunction kernelFunction = KernelFunctionFactory.CreateFromMethod(GetWeather);
+KernelPlugin kernelPlugin = KernelPluginFactory.CreateFromFunctions("WeatherPlugin", [kernelFunction]);
+kernel.Plugins.Add(kernelPlugin);
+
+ChatCompletionAgent agent = new() { Kernel = kernel };
+```
+
+**With this Agent Framework pattern:**
+```csharp
+[Description("Get the weather for a location")] // Keep Description attribute
+static string GetWeather(string location) => $"Weather in {location}";
+
+AIAgent agent = chatClient.CreateAIAgent(
+ instructions: "You are a helpful assistant",
+ tools: [AIFunctionFactory.Create(GetWeather)]);
+```
+
+**Required changes:**
+1. Remove `[KernelFunction]` attributes from methods
+2. Keep `[Description]` attributes for function descriptions
+3. Remove `KernelFunctionFactory.CreateFromMethod()` calls
+4. Remove `KernelPluginFactory.CreateFromFunctions()` calls
+5. Remove `kernel.Plugins.Add()` calls
+6. Replace with `AIFunctionFactory.Create()` in tools parameter
+7. Pass tools directly to agent creation method
+
+
+### Invocation Method Transformation
+
+
+**Replace this Semantic Kernel non-streaming pattern:**
+```csharp
+await foreach (AgentResponseItem item in agent.InvokeAsync(userInput, thread, options))
+{
+ Console.WriteLine(item.Message);
+}
+```
+
+**With this Agent Framework non-streaming pattern:**
+```csharp
+AgentResponse result = await agent.RunAsync(userInput, thread, options);
+Console.WriteLine(result);
+```
+
+**Replace this Semantic Kernel streaming pattern:**
+```csharp
+await foreach (StreamingChatMessageContent update in agent.InvokeStreamingAsync(userInput, thread, options))
+{
+ Console.Write(update.Message);
+}
+```
+
+**With this Agent Framework streaming pattern:**
+```csharp
+await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(userInput, thread, options))
+{
+ Console.Write(update);
+}
+```
+
+**Required changes:**
+1. Replace `agent.InvokeAsync()` with `agent.RunAsync()`
+2. Replace `agent.InvokeStreamingAsync()` with `agent.RunStreamingAsync()`
+3. Change return type handling from `IAsyncEnumerable>` to `AgentResponse`
+4. Change streaming type from `StreamingChatMessageContent` to `AgentResponseUpdate`
+5. Remove `await foreach` for non-streaming calls
+6. Access message content directly from result object instead of iterating
+
+
+### Options and Configuration Transformation
+
+
+**Replace this Semantic Kernel options pattern:**
+```csharp
+OpenAIPromptExecutionSettings settings = new() { MaxTokens = 1000 };
+AgentInvokeOptions options = new() { KernelArguments = new(settings) };
+```
+
+**With this Agent Framework options pattern:**
+```csharp
+ChatClientAgentRunOptions options = new(new ChatOptions { MaxOutputTokens = 1000 });
+```
+
+**Required changes:**
+1. Remove `OpenAIPromptExecutionSettings` (or other provider-specific settings)
+2. Remove `AgentInvokeOptions` wrapper
+3. Remove `KernelArguments` wrapper
+4. Replace with `ChatClientAgentRunOptions` containing `ChatOptions`
+5. Update property names: `MaxTokens` → `MaxOutputTokens`
+6. Pass options directly to `RunAsync()` or `RunStreamingAsync()` methods
+
+
+### Dependency Injection Transformation
+
+
+**Replace this Semantic Kernel DI pattern:**
+
+Different providers require different kernel extensions:
+
+```csharp
+services.AddKernel().AddOpenAIChatClient(modelId, apiKey);
+services.AddTransient(sp => new()
+{
+ Kernel = sp.GetRequiredService(),
+ Instructions = "You are helpful"
+});
+```
+
+**With this Agent Framework DI pattern:**
+```csharp
+services.AddTransient(sp =>
+ new OpenAIClient(apiKey)
+ .GetChatClient(modelId)
+ .CreateAIAgent(instructions: "You are helpful"));
+```
+
+**Required changes:**
+1. Remove `services.AddKernel()` registration
+2. Remove provider-specific kernel extensions (e.g., `.AddOpenAIChatClient()`)
+3. Replace `ChatCompletionAgent` with `AIAgent` in service registration
+4. Remove `Kernel` dependency from constructor
+5. Use direct client creation and extension methods
+6. Remove `sp.GetRequiredService()` calls
+
+
+### Thread Cleanup Transformation
+
+
+**Replace this Semantic Kernel cleanup pattern:**
+```csharp
+await thread.DeleteAsync(); // For hosted threads
+```
+
+**With these Agent Framework cleanup patterns:**
+
+For every thread created if there's intent to cleanup, the caller should track all the created threads for the provider that support hosted threads for cleanup purposes.
+
+```csharp
+// For OpenAI Assistants (when cleanup is needed):
+var assistantClient = new OpenAIClient(apiKey).GetAssistantClient();
+await assistantClient.DeleteThreadAsync(thread.ConversationId);
+
+// For Azure AI Foundry (when cleanup is needed):
+var persistentClient = new PersistentAgentsClient(endpoint, credential);
+await persistentClient.Threads.DeleteThreadAsync(thread.ConversationId);
+
+// No thread and agent cleanup is needed for non-hosted agent providers like
+// - Azure OpenAI Chat Completion
+// - OpenAI Chat Completion
+// - Azure OpenAI Responses
+// - OpenAI Responses
+```
+
+**Required changes:**
+1. Remove `thread.DeleteAsync()` calls
+2. Use provider-specific client for cleanup when required
+3. Access thread ID via `thread.ConversationId` property
+4. Only implement cleanup for providers that require it (Assistants, Azure AI Foundry)
+
+
+### Provider-Specific Creation Patterns
+
+
+Use these exact patterns for each provider:
+
+**OpenAI Chat Completion:**
+```csharp
+AIAgent agent = new OpenAIClient(apiKey)
+ .GetChatClient(modelId)
+ .CreateAIAgent(instructions: instructions);
+```
+
+**OpenAI Assistants (New):** ⚠️ *Deprecated - Use Responses API instead*
+```csharp
+AIAgent agent = new OpenAIClient(apiKey)
+ .GetAssistantClient()
+ .CreateAIAgent(modelId, instructions: instructions);
+```
+
+**OpenAI Assistants (Existing):** ⚠️ *Deprecated - Use Responses API instead*
+```csharp
+AIAgent agent = new OpenAIClient(apiKey)
+ .GetAssistantClient()
+ .GetAIAgent(assistantId);
+```
+
+**Azure OpenAI:**
+```csharp
+AIAgent agent = new AzureOpenAIClient(endpoint, credential)
+ .GetChatClient(deploymentName)
+ .CreateAIAgent(instructions: instructions);
+```
+
+**Azure AI Foundry (New):**
+```csharp
+AIAgent agent = new PersistentAgentsClient(endpoint, credential)
+ .CreateAIAgent(model: deploymentName, instructions: instructions);
+```
+
+**Azure AI Foundry (Existing):**
+```csharp
+AIAgent agent = await new PersistentAgentsClient(endpoint, credential)
+ .GetAIAgentAsync(agentId);
+```
+
+**OpenAI Responses:** *(Recommended for OpenAI)*
+```csharp
+AIAgent agent = new OpenAIClient(apiKey)
+ .GetOpenAIResponseClient(modelId)
+ .CreateAIAgent(instructions: instructions);
+```
+
+**Azure OpenAI Responses:** *(Recommended for Azure OpenAI)*
+```csharp
+AIAgent agent = new AzureOpenAIClient(endpoint, credential)
+ .GetOpenAIResponseClient(deploymentName)
+ .CreateAIAgent(instructions: instructions);
+```
+
+**A2A:**
+```csharp
+A2ACardResolver resolver = new(new Uri(agentHost));
+AIAgent agent = await resolver.GetAIAgentAsync();
+```
+
+
+### Complete Migration Examples
+
+#### Basic Agent Creation Transformation
+
+**Replace this complete Semantic Kernel pattern:**
+```csharp
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Agents;
+
+Kernel kernel = Kernel.CreateBuilder()
+ .AddOpenAIChatClient(modelId, apiKey)
+ .Build();
+
+ChatCompletionAgent agent = new()
+{
+ Instructions = "You are helpful",
+ Kernel = kernel
+};
+
+AgentThread thread = new ChatHistoryAgentThread();
+```
+
+**With this complete Agent Framework pattern:**
+```csharp
+using Microsoft.Agents.AI;
+using OpenAI;
+
+AIAgent agent = new OpenAIClient(apiKey)
+ .GetChatClient(modelId)
+ .CreateAIAgent(instructions: "You are helpful");
+
+AgentThread thread = agent.GetNewThread();
+```
+
+
+#### Tool Registration Transformation
+
+**Replace this complete Semantic Kernel tool pattern:**
+```csharp
+[KernelFunction] // Remove this attribute
+[Description("Get weather information")]
+static string GetWeather([Description("Location")] string location)
+ => $"Weather in {location}";
+
+KernelFunction function = KernelFunctionFactory.CreateFromMethod(GetWeather);
+KernelPlugin plugin = KernelPluginFactory.CreateFromFunctions("Weather", [function]);
+kernel.Plugins.Add(plugin);
+```
+
+**With this complete Agent Framework tool pattern:**
+```csharp
+[Description("Get weather information")] // Keep this attribute
+static string GetWeather([Description("Location")] string location)
+ => $"Weather in {location}";
+
+AIAgent agent = chatClient.CreateAIAgent(
+ instructions: "You are a helpful assistant",
+ tools: [AIFunctionFactory.Create(GetWeather)]);
+```
+
+
+#### Agent Invocation Transformation
+
+**Replace this complete Semantic Kernel invocation pattern:**
+```csharp
+OpenAIPromptExecutionSettings settings = new() { MaxTokens = 1000 };
+AgentInvokeOptions options = new() { KernelArguments = new(settings) };
+
+await foreach (var result in agent.InvokeAsync(input, thread, options))
+{
+ Console.WriteLine(result.Message);
+}
+```
+
+**With this complete Agent Framework invocation pattern:**
+```csharp
+ChatClientAgentRunOptions options = new(new ChatOptions { MaxOutputTokens = 1000 });
+
+AgentResponse result = await agent.RunAsync(input, thread, options);
+Console.WriteLine(result);
+
+// Access underlying content when needed:
+var chatResponse = result.RawRepresentation as ChatResponse;
+// Access underlying SDK objects via chatResponse?.RawRepresentation
+```
+
+
+### Usage Metadata Transformation
+
+
+**Replace this Semantic Kernel non-streaming usage pattern:**
+```csharp
+await foreach (var result in agent.InvokeAsync(input, thread, options))
+{
+ if (result.Message.Metadata?.TryGetValue("Usage", out object? usage) ?? false)
+ {
+ if (usage is ChatTokenUsage openAIUsage)
+ {
+ Console.WriteLine($"Tokens: {openAIUsage.TotalTokenCount}");
+ }
+ }
+}
+```
+
+**With this Agent Framework non-streaming usage pattern:**
+```csharp
+AgentResponse result = await agent.RunAsync(input, thread, options);
+Console.WriteLine($"Tokens: {result.Usage.TotalTokenCount}");
+```
+
+**Replace this Semantic Kernel streaming usage pattern:**
+```csharp
+await foreach (StreamingChatMessageContent response in agent.InvokeStreamingAsync(message, agentThread))
+{
+ if (response.Metadata?.TryGetValue("Usage", out object? usage) ?? false)
+ {
+ if (usage is ChatTokenUsage openAIUsage)
+ {
+ Console.WriteLine($"Tokens: {openAIUsage.TotalTokenCount}");
+ }
+ }
+}
+```
+
+**With this Agent Framework streaming usage pattern:**
+```csharp
+await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(input, thread, options))
+{
+ if (update.Contents.OfType().FirstOrDefault() is { } usageContent)
+ {
+ Console.WriteLine($"Tokens: {usageContent.Details.TotalTokenCount}");
+ }
+}
+```
+
+
+
+
+### Breaking Glass Pattern Transformation
+
+
+**Replace this Semantic Kernel breaking glass pattern:**
+```csharp
+await foreach (var content in agent.InvokeAsync(userInput, thread))
+{
+ UnderlyingSdkType? underlyingChatMessage = content.Message.InnerContent as UnderlyingSdkType;
+}
+```
+
+**With this Agent Framework breaking glass pattern:**
+```csharp
+var agentRunResponse = await agent.RunAsync(userInput, thread);
+
+// If the agent uses a ChatClient the first breaking glass probably will be a Microsoft.Extensions.AI.ChatResponse
+ChatResponse? chatResponse = agentRunResponse.RawRepresentation as ChatResponse;
+
+// If thats the case, to access the underlying SDK types you will need to break glass again.
+UnderlyingSdkType? underlyingChatMessage = chatResponse?.RawRepresentation as UnderlyingSdkType;
+```
+
+**Required changes:**
+1. Replace `InnerContent` property access with `RawRepresentation` property access
+2. Cast `RawRepresentation` to appropriate type expected
+3. If the `RawRepresentation` is a `Microsoft.Extensions.AI` type, break glass again to access the underlying SDK types
+
+
+#### CodeInterpreter Tool Transformation
+
+
+**Replace this Semantic Kernel CodeInterpreter pattern:**
+```csharp
+await foreach (var content in agent.InvokeAsync(userInput, thread))
+{
+ bool isCode = content.Message.Metadata?.ContainsKey(AzureAIAgent.CodeInterpreterMetadataKey) ?? false;
+ Console.WriteLine($"# {content.Message.Role}{(isCode ? "\n# Generated Code:\n" : ":")}{content.Message.Content}");
+
+ // Process annotations
+ foreach (var item in content.Message.Items)
+ {
+ if (item is AnnotationContent annotation)
+ {
+ Console.WriteLine($"[{item.GetType().Name}] {annotation.Label}: File #{annotation.ReferenceId}");
+ }
+ else if (item is FileReferenceContent fileReference)
+ {
+ Console.WriteLine($"[{item.GetType().Name}] File #{fileReference.FileId}");
+ }
+ }
+}
+```
+
+**With this Agent Framework CodeInterpreter pattern:**
+```csharp
+using System.Text;
+using Microsoft.Agents.AI;
+using Microsoft.Extensions.AI;
+
+var result = await agent.RunAsync(userInput, thread);
+Console.WriteLine(result);
+
+// Get the CodeInterpreterToolCallContent (code input)
+CodeInterpreterToolCallContent? toolCallContent = result.Messages
+ .SelectMany(m => m.Contents)
+ .OfType()
+ .FirstOrDefault();
+
+if (toolCallContent?.Inputs is not null)
+{
+ DataContent? codeInput = toolCallContent.Inputs.OfType().FirstOrDefault();
+ if (codeInput?.HasTopLevelMediaType("text") ?? false)
+ {
+ Console.WriteLine($"Code Input: {Encoding.UTF8.GetString(codeInput.Data.ToArray())}");
+ }
+}
+
+// Get the CodeInterpreterToolResultContent (code output)
+CodeInterpreterToolResultContent? toolResultContent = result.Messages
+ .SelectMany(m => m.Contents)
+ .OfType()
+ .FirstOrDefault();
+
+if (toolResultContent?.Outputs is not null)
+{
+ TextContent? resultOutput = toolResultContent.Outputs.OfType().FirstOrDefault();
+ if (resultOutput is not null)
+ {
+ Console.WriteLine($"Code Tool Result: {resultOutput.Text}");
+ }
+}
+
+// Getting any annotations generated by the tool
+foreach (AIAnnotation annotation in result.Messages
+ .SelectMany(m => m.Contents)
+ .SelectMany(c => c.Annotations ?? []))
+{
+ Console.WriteLine($"Annotation: {annotation}");
+}
+```
+
+**Functional differences:**
+1. Code interpreter content is now available via MEAI abstractions - no breaking glass required
+2. Use `CodeInterpreterToolCallContent` to access code inputs (the generated code)
+3. Use `CodeInterpreterToolResultContent` to access code outputs (execution results)
+4. Annotations are accessible via `AIAnnotation` on content items
+
+
+#### Provider-Specific Options Configuration
+
+
+For advanced model settings not available in `ChatOptions`, use the `RawRepresentationFactory` property:
+
+```csharp
+var agentOptions = new ChatClientAgentRunOptions(new ChatOptions
+{
+ MaxOutputTokens = 8000,
+ // Breaking glass to access provider-specific options
+ RawRepresentationFactory = (_) => new OpenAI.Responses.CreateResponseOptions()
+ {
+ ReasoningOptions = new()
+ {
+ ReasoningEffortLevel = OpenAI.Responses.ResponseReasoningEffortLevel.High,
+ ReasoningSummaryVerbosity = OpenAI.Responses.ResponseReasoningSummaryVerbosity.Detailed
+ }
+ }
+});
+```
+
+**Use this pattern when:**
+1. Standard `ChatOptions` properties don't cover required model settings
+2. Provider-specific configuration is needed (e.g., reasoning effort level)
+3. Advanced SDK features need to be accessed
+
+
+#### Type-Safe Extension Methods
+
+
+Use provider-specific extension methods for safer breaking glass access:
+
+```csharp
+using OpenAI; // Brings in extension methods
+
+// Type-safe extraction of OpenAI ChatCompletion
+var chatCompletion = result.AsChatCompletion();
+
+// Access underlying OpenAI objects safely
+var openAIResponse = chatCompletion.GetRawResponse();
+```
+
+**Available extension methods:**
+- `result.AsChatCompletion()` for OpenAI providers
+- `result.GetRawResponse()` for accessing underlying SDK responses
+- Provider-specific extensions for type-safe casting
+
+
+
+
+### Common Migration Issues and Solutions
+
+
+**Issue: Missing Using Statements**
+- **Problem**: Compilation errors due to missing namespace imports
+- **Solution**: Add `using Microsoft.Agents.AI;` and remove `using Microsoft.SemanticKernel.Agents;`
+
+**Issue: Tool Function Signatures**
+- **Problem**: `[KernelFunction]` attributes cause compilation errors
+- **Solution**: Remove `[KernelFunction]` attributes, keep `[Description]` attributes
+
+**Issue: Thread Type Mismatches**
+- **Problem**: Provider-specific thread constructors not found
+- **Solution**: Replace all thread constructors with `agent.GetNewThread()`
+
+**Issue: Options Configuration**
+- **Problem**: `AgentInvokeOptions` type not found
+- **Solution**: Replace with `AgentRunOptions` or `ChatClientAgentRunOptions` containing `ChatOptions`
+
+**Issue: Dependency Injection**
+- **Problem**: `Kernel` service registration not found
+- **Solution**: Remove `services.AddKernel()`, use direct client registration
+
+
+### Migration Execution Steps
+
+
+1. **Update Package References**: Remove SK packages, add AF packages per provider
+2. **Update Namespaces**: Replace SK namespaces with AF namespaces
+3. **Update Agent Creation**: Remove Kernel, use direct client creation
+4. **Update Method Calls**: Replace `InvokeAsync` with `RunAsync`
+5. **Update Thread Creation**: Replace provider-specific constructors with `GetNewThread()`
+6. **Update Tool Registration**: Remove attributes, use `AIFunctionFactory.Create()`
+7. **Update Options**: Replace `AgentInvokeOptions` with provider-specific options
+8. **Test and Validate**: Compile and test all functionality
+
+
+## Provider-Specific Migration Patterns
+
+
+The following sections provide detailed migration patterns for each supported provider, covering package references, agent creation patterns, and provider-specific configurations.
+
+
+### 1. OpenAI Chat Completion Migration
+
+
+**Remove Semantic Kernel Packages:**
+```xml
+
+```
+
+**Add Agent Framework Packages:**
+```xml
+
+```
+
+
+**Before (Semantic Kernel):**
+```csharp
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Agents;
+
+Kernel kernel = Kernel.CreateBuilder()
+ .AddOpenAIChatClient(modelId, apiKey)
+ .Build();
+
+ChatCompletionAgent agent = new()
+{
+ Instructions = "You are a helpful assistant",
+ Kernel = kernel
+};
+
+AgentThread thread = new ChatHistoryAgentThread();
+```
+
+**After (Agent Framework):**
+```csharp
+using Microsoft.Agents.AI;
+using OpenAI;
+
+AIAgent agent = new OpenAIClient(apiKey)
+ .GetChatClient(modelId)
+ .CreateAIAgent(instructions: "You are a helpful assistant");
+
+AgentThread thread = agent.GetNewThread();
+```
+
+### 2. Azure OpenAI Chat Completion Migration
+
+
+**Remove Semantic Kernel Packages:**
+```xml
+
+
+
+```
+
+**Add Agent Framework Packages:**
+```xml
+
+
+
+```
+
+**Note**: If not using `AzureCliCredential`, you can use `ApiKeyCredential` instead without the `Azure.Identity` package.
+
+
+**Before (Semantic Kernel):**
+```csharp
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Agents;
+using Azure.Identity;
+
+Kernel kernel = Kernel.CreateBuilder()
+ .AddAzureOpenAIChatClient(deploymentName, endpoint, new AzureCliCredential())
+ .Build();
+
+ChatCompletionAgent agent = new()
+{
+ Instructions = "You are a helpful assistant",
+ Kernel = kernel
+};
+```
+
+**After (Agent Framework):**
+```csharp
+using Microsoft.Agents.AI;
+using Azure.AI.OpenAI;
+using Azure.Identity;
+
+AIAgent agent = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential())
+ .GetChatClient(deploymentName)
+ .CreateAIAgent(instructions: "You are a helpful assistant");
+```
+
+### 3. OpenAI Assistants Migration
+
+> ⚠️ **DEPRECATION WARNING**: The OpenAI Assistants API has been deprecated. The Agent Framework extension methods for Assistants are marked as `[Obsolete]`. **Please use the Responses API instead** (see Section 6: OpenAI Responses Migration).
+
+
+**Remove Semantic Kernel Packages:**
+```xml
+
+```
+
+**Add Agent Framework Packages:**
+```xml
+
+```
+
+
+
+**Replace this Semantic Kernel pattern:**
+```csharp
+using Microsoft.SemanticKernel.Agents.OpenAI;
+using OpenAI.Assistants;
+
+AssistantClient assistantClient = new(apiKey);
+Assistant assistant = await assistantClient.CreateAssistantAsync(
+ modelId,
+ instructions: "You are a helpful assistant");
+
+OpenAIAssistantAgent agent = new(assistant, assistantClient)
+{
+ Kernel = kernel
+};
+
+AgentThread thread = new OpenAIAssistantAgentThread(assistantClient);
+```
+
+**With this Agent Framework pattern:**
+
+**Creating a new assistant:**
+```csharp
+using Microsoft.Agents.AI;
+using OpenAI;
+
+AIAgent agent = new OpenAIClient(apiKey)
+ .GetAssistantClient()
+ .CreateAIAgent(modelId, instructions: "You are a helpful assistant");
+
+AgentThread thread = agent.GetNewThread();
+
+// Cleanup when needed
+await assistantClient.DeleteThreadAsync(thread.ConversationId);
+```
+
+**Retrieving an existing assistant:**
+```csharp
+using Microsoft.Agents.AI;
+using OpenAI;
+
+AIAgent agent = new OpenAIClient(apiKey)
+ .GetAssistantClient()
+ .GetAIAgent(assistantId); // Use existing assistant ID
+
+AgentThread thread = agent.GetNewThread();
+```
+
+
+### 4. Azure AI Foundry (AzureAIAgent) Migration
+
+
+**Remove Semantic Kernel Packages:**
+```xml
+
+
+```
+
+**Add Agent Framework Packages:**
+```xml
+
+
+```
+
+
+
+**Replace these Semantic Kernel patterns:**
+
+**Pattern 1: Direct AzureAIAgent creation**
+```csharp
+using Microsoft.SemanticKernel.Agents.AzureAI;
+using Azure.Identity;
+
+AzureAIAgent agent = new(
+ endpoint: new Uri(endpoint),
+ credential: new AzureCliCredential(),
+ projectId: projectId)
+{
+ Instructions = "You are a helpful assistant"
+};
+
+AgentThread thread = new AzureAIAgentThread(agent);
+```
+
+**Pattern 2: PersistentAgent definition creation**
+```csharp
+// Define the agent
+PersistentAgent definition = await client.Administration.CreateAgentAsync(
+ deploymentName,
+ tools: [new CodeInterpreterToolDefinition()]);
+
+AzureAIAgent agent = new(definition, client);
+
+// Create a thread for the agent conversation.
+AgentThread thread = new AzureAIAgentThread(client);
+```
+
+**With these Agent Framework patterns:**
+
+**Creating a new agent:**
+```csharp
+using Microsoft.Agents.AI;
+using Azure.AI.Agents.Persistent;
+using Azure.Identity;
+
+var client = new PersistentAgentsClient(endpoint, new AzureCliCredential());
+
+// Create a new AIAgent using Agent Framework
+AIAgent agent = client.CreateAIAgent(
+ model: deploymentName,
+ instructions: "You are a helpful assistant",
+ tools: [/* List of specialized Azure.AI.Agents.Persistent.ToolDefinition types */]);
+
+AgentThread thread = agent.GetNewThread();
+```
+
+**Retrieving an existing agent:**
+```csharp
+using Microsoft.Agents.AI;
+using Azure.AI.Agents.Persistent;
+using Azure.Identity;
+
+var client = new PersistentAgentsClient(endpoint, new AzureCliCredential());
+
+// Retrieve an existing AIAgent using its ID
+AIAgent agent = await client.GetAIAgentAsync(agentId);
+
+AgentThread thread = agent.GetNewThread();
+```
+
+
+### 5. A2A Migration
+
+
+**Remove Semantic Kernel Packages:**
+```xml
+
+```
+
+**Add Agent Framework Packages:**
+```xml
+
+```
+
+
+
+**Replace this Semantic Kernel pattern:**
+```csharp
+// Create an A2A agent instance
+using var httpClient = CreateHttpClient();
+var client = new A2AClient(url, httpClient);
+var cardResolver = new A2ACardResolver(url, httpClient);
+var agentCard = await cardResolver.GetAgentCardAsync();
+var agent = new A2AAgent(client, agentCard);
+```
+
+**With this Agent Framework pattern:**
+```csharp
+// Initialize an A2ACardResolver to get an A2A agent card.
+A2ACardResolver agentCardResolver = new(new Uri(a2aAgentHost));
+
+// Create an instance of the AIAgent for an existing A2A agent specified by the agent card.
+AIAgent agent = await agentCardResolver.GetAIAgentAsync();
+```
+
+
+### 6. OpenAI Responses Migration
+
+
+**Remove Semantic Kernel Packages:**
+```xml
+
+```
+
+**Add Agent Framework Packages:**
+```xml
+
+```
+
+
+
+**Replace this Semantic Kernel pattern:**
+
+The thread management is done manually with OpenAI Responses in Semantic Kernel, where the thread
+needs to be passed to the `InvokeAsync` method and updated with the `item.Thread` from the response.
+
+```csharp
+using Microsoft.SemanticKernel.Agents.OpenAI;
+
+// Define the agent
+OpenAIResponseAgent agent = new(new OpenAIClient(apiKey))
+{
+ Name = "ResponseAgent",
+ Instructions = "Answer all queries in English and French.",
+};
+
+// Initial thread can be null as it will be automatically created
+AgentThread? agentThread = null;
+
+var responseItems = agent.InvokeAsync(new ChatMessageContent(AuthorRole.User, "Input message."), agentThread);
+await foreach (AgentResponseItem responseItem in responseItems)
+{
+ // Update the thread to maintain the conversation for future interaction
+ agentThread = responseItem.Thread;
+
+ WriteAgentChatMessage(responseItem.Message);
+}
+```
+
+**With this Agent Framework pattern:**
+
+Agent Framework automatically manages the thread, so there's no need to manually update it.
+
+```csharp
+using Microsoft.Agents.AI.OpenAI;
+
+AIAgent agent = new OpenAIClient(apiKey)
+ .GetOpenAIResponseClient(modelId)
+ .CreateAIAgent(
+ name: "ResponseAgent",
+ instructions: "Answer all queries in English and French.",
+ tools: [/* AITools */]);
+
+AgentThread thread = agent.GetNewThread();
+
+var result = await agent.RunAsync(userInput, thread);
+
+// The thread will be automatically updated with the new response id from this point
+```
+
+
+### 7. Azure OpenAI Responses Migration
+
+
+**Remove Semantic Kernel Packages:**
+```xml
+
+
+```
+
+**Add Agent Framework Packages:**
+```xml
+
+
+```
+
+
+
+**Replace this Semantic Kernel pattern:**
+
+Azure OpenAI Responses uses `AzureOpenAIClient` instead of `OpenAIClient`. The thread management is done manually where the thread needs to be passed to the `InvokeAsync` method and updated with the `item.Thread` from the response.
+
+```csharp
+using Microsoft.SemanticKernel.Agents.OpenAI;
+using Azure.AI.OpenAI;
+
+// Define the agent
+OpenAIResponseAgent agent = new(new AzureOpenAIClient(endpoint, new AzureCliCredential()))
+{
+ Name = "ResponseAgent",
+ Instructions = "Answer all queries in English and French.",
+};
+
+// Initial thread can be null as it will be automatically created
+AgentThread? agentThread = null;
+
+var responseItems = agent.InvokeAsync(new ChatMessageContent(AuthorRole.User, "Input message."), agentThread);
+await foreach (AgentResponseItem responseItem in responseItems)
+{
+ // Update the thread to maintain the conversation for future interaction
+ agentThread = responseItem.Thread;
+
+ WriteAgentChatMessage(responseItem.Message);
+}
+```
+
+**With this Agent Framework pattern:**
+
+Agent Framework automatically manages the thread, so there's no need to manually update it.
+
+```csharp
+using Microsoft.Agents.AI.OpenAI;
+using Azure.AI.OpenAI;
+
+AIAgent agent = new AzureOpenAIClient(endpoint, new AzureCliCredential())
+ .GetOpenAIResponseClient(modelId)
+ .CreateAIAgent(
+ name: "ResponseAgent",
+ instructions: "Answer all queries in English and French.",
+ tools: [/* AITools */]);
+
+AgentThread thread = agent.GetNewThread();
+
+var result = await agent.RunAsync(userInput, thread);
+
+// The thread will be automatically updated with the new response id from this point
+```
+
+
+### 8. Unsupported Providers (Require Custom Implementation)
+
+
+#### BedrockAgent Migration
+
+**Status**: Hosted Agents is not directly supported in Agent Framework
+
+**Status**: Non-Hosted AI Model Agents supported via `ChatClientAgent`
+
+**Replace this Semantic Kernel pattern:**
+```csharp
+using Microsoft.SemanticKernel.Agents.Bedrock;
+
+// Create a new agent on the Bedrock Agent service and prepare it for use
+using var client = new AmazonBedrockAgentClient();
+using var runtimeClient = new AmazonBedrockAgentRuntimeClient();
+var agentModel = await client.CreateAndPrepareAgentAsync(new CreateAgentRequest()
+ {
+ AgentName = agentName,
+ Description = "AgentDescription",
+ Instruction = "You are a helpful assistant",
+ AgentResourceRoleArn = TestConfiguration.BedrockAgent.AgentResourceRoleArn,
+ FoundationModel = TestConfiguration.BedrockAgent.FoundationModel,
+ });
+
+// Create a new BedrockAgent instance with the agent model and the client
+// so that we can interact with the agent using Semantic Kernel contents.
+var agent = new BedrockAgent(agentModel, client, runtimeClient);
+```
+
+**With this Agent Framework workaround:**
+
+Currently there's no support for the Hosted Bedrock Agent service in Agent Framework.
+
+For providers like AWS Bedrock that have an `IChatClient` implementation available, use the `ChatClientAgent` directly by providing the `IChatClient` instance to the agent.
+
+_Those agents will be purely backed by the AI chat models behavior and will not store any state in the server._
+
+```csharp
+using Microsoft.Agents.AI;
+
+services.TryAddAWSService();
+var serviceProvider = services.BuildServiceProvider();
+IAmazonBedrockRuntime runtime = serviceProvider.GetRequiredService();
+
+using var bedrockChatClient = runtime.AsIChatClient();
+AIAgent agent = new ChatClientAgent(bedrockChatClient, instructions: "You are a helpful assistant");
+```
+
+
+### Unsupported Features that need workarounds
+
+
+The following Semantic Kernel Agents features currently don't have direct equivalents in Agent Framework:
+
+#### Plugins Migration
+
+**Problem**: Semantic Kernel plugins allowed multiple functions to be registered under a type or object instance
+
+**Semantic Kernel pattern**
+```csharp
+// Create plugin with multiple functions
+public class WeatherPlugin
+{
+ [KernelFunction, Description("Get current weather")]
+ public string GetCurrentWeather(string location)
+ => $"Weather in {location}: Sunny";
+
+ [KernelFunction, Description("Get weather forecast")]
+ public static Task GetForecastAsync(string location, int days)
+ => Task.FromResult($"Forecast for {location}: {days} days");
+}
+
+kernel.Plugins.AddFromType();
+// OR
+kernel.Plugins.AddFromObject(new WeatherPlugin());
+```
+
+**Agent Framework workaround:**
+
+```csharp
+// Create individual functions (no plugin grouping)
+public class WeatherFunctions
+{
+ [Description("Get current weather")]
+ public static string GetCurrentWeather(string location)
+ => $"Weather in {location}: Sunny";
+
+ [Description("Get weather forecast")]
+ public Task GetForecastAsync(string location, int days)
+ => Task.FromResult($"Forecast for {location}: {days} days");
+}
+
+var weatherService = new WeatherFunctions();
+
+// Register functions individually as tools
+AITool[] tools = [
+ AIFunctionFactory.Create(WeatherFunctions.GetCurrentWeather), // Get from type static method
+ AIFunctionFactory.Create(weatherService.GetForecastAsync) // Get from instance method
+];
+
+// OR Iterate over the type or instance if many functions are needed for registration
+AITool[] tools =
+[
+ .. typeof(WeatherFunctions)
+ .GetMethods(BindingFlags.Static | BindingFlags.Public)
+ .Select((m) => AIFunctionFactory.Create(m, target: null)), // Get from type static methods
+ .. weatherService.GetType()
+ .GetMethods(BindingFlags.Instance | BindingFlags.Public)
+ .Select((m) => AIFunctionFactory.Create(m, target: weatherService)) // Get from instance methods
+];
+
+AIAgent agent = new OpenAIClient(apiKey)
+ .GetChatClient(modelId)
+ .CreateAIAgent(
+ instructions: "You are a weather assistant",
+ tools: tools);
+```
+
+#### Prompt Template Migration
+
+**Problem**: Agent prompt templating is not yet supported in Agent Framework
+
+**Semantic Kernel pattern**
+```csharp
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Agents;
+
+var template = "Tell a story about {{$topic}} that is {{$length}} sentences long.";
+
+ChatCompletionAgent agent =
+ new(templateFactory: new KernelPromptTemplateFactory(),
+ templateConfig: new(template) { TemplateFormat = PromptTemplateConfig.SemanticKernelTemplateFormat })
+ {
+ Kernel = kernel,
+ Name = "StoryTeller",
+ Arguments = new KernelArguments()
+ {
+ { "topic", "Dog" },
+ { "length", "3" },
+ }
+ };
+```
+
+**Agent Framework workaround**
+
+```csharp
+using Microsoft.Agents.AI;
+using Microsoft.SemanticKernel;
+
+// Manually render template
+var template = "Tell a story about {{$topic}} that is {{$length}} sentences long.";
+
+var renderedTemplate = await new KernelPromptTemplateFactory()
+ .Create(new PromptTemplateConfig(template))
+ .RenderAsync(new Kernel(), new KernelArguments()
+ {
+ ["topic"] = "Dog",
+ ["length"] = "3"
+ });
+
+AIAgent agent = new OpenAIClient(apiKey)
+ .GetChatClient(modelId)
+ .CreateAIAgent(instructions: renderedTemplate);
+
+// No template variables in invocation - use plain string
+var result = await agent.RunAsync("What's the weather?", thread);
+Console.WriteLine(result);
+```
+
+
+### 9. Function Invocation Filtering
+
+**Invocation Context**
+
+Semantic Kernel's `IAutoFunctionInvocationFilter` provides a `AutoFunctionInvocationContext` where Agent Framework provides `FunctionInvocationContext`
+
+The property mapping guide from a `AutoFunctionInvocationContext` to a `FunctionInvocationContext` is as follows:
+
+| SK | AF |
+| --- | --- |
+| RequestSequenceIndex | Iteration |
+| FunctionSequenceIndex | FunctionCallIndex |
+| ToolCallId | CallContent.CallId |
+| ChatMessageContent | Messages[0] |
+| ExecutionSettings | Options |
+| ChatHistory | Messages |
+| Function | Function |
+| Kernel | N/A |
+| Result | Use `return` from the delegate |
+| Terminate | Terminate |
+| CancellationToken | provided via argument to middleware delegate |
+| Arguments | Arguments |
+
+#### Semantic Kernel
+
+```csharp
+// Filter specifically for functions calling
+public sealed class CustomAutoFunctionInvocationFilter : IAutoFunctionInvocationFilter
+{
+ public async Task OnAutoFunctionInvocationAsync(AutoFunctionInvocationContext context, Func next)
+ {
+ Console.WriteLine($"[SK Auto Filter] Auto-invoking function: {context.Function.Name}");
+
+ // Check if function should be auto-invoked
+ if (context.Function.Name.Contains("Dangerous"))
+ {
+ Console.WriteLine($"[SK Auto Filter] Skipping dangerous function: {context.Function.Name}");
+ context.Terminate = true;
+ return;
+ }
+
+ await next(context);
+
+ Console.WriteLine($"[SK Auto Filter] Auto-invocation completed for: {context.Function.Name}");
+ }
+}
+
+var builder = Kernel.CreateBuilder()
+ .AddOpenAIChatClient(modelId, apiKey);
+
+// via builder DI
+var builder = Kernel.CreateBuilder()
+ .AddOpenAIChatClient(modelId, apiKey)
+ .Services
+ .AddSingleton();
+
+// OR via DI
+services
+ .AddKernel()
+ .AddOpenAIChatClient(modelId, apiKey)
+ .AddSingleton();
+
+// OR register auto function filter directly with the kernel instance
+kernel.AutoFunctionInvocationFilters.Add(new CustomAutoFunctionInvocationFilter());
+
+// Create agent with filtered kernel
+ChatCompletionAgent agent = new()
+{
+ Instructions = "You are a helpful assistant",
+ Kernel = kernel
+};
+```
+
+#### Agent Framework
+
+Agent Framework provides function calling middleware that offers equivalent capabilities to Semantic Kernel's auto function invocation filters:
+
+```csharp
+// Function calling middleware equivalent to CustomAutoFunctionInvocationFilter
+async ValueTask CustomAutoFunctionMiddleware(
+ AIAgent agent,
+ FunctionInvocationContext context,
+ Func> next,
+ CancellationToken cancellationToken)
+{
+ Console.WriteLine($"[AF Middleware] Auto-invoking function: {context.Function.Name}");
+
+ // Check if function should be auto-invoked
+ if (context.Function.Name.Contains("Dangerous"))
+ {
+ Console.WriteLine($"[AF Middleware] Skipping dangerous function: {context.Function.Name}");
+ context.Terminate = true;
+ return "Function execution blocked for security reasons";
+ }
+
+ var result = await next(context, cancellationToken);
+
+ Console.WriteLine($"[AF Middleware] Auto-invocation completed for: {context.Function.Name}");
+ return result;
+}
+
+// Apply middleware to agent
+var filteredAgent = originalAgent
+ .AsBuilder()
+ .Use(CustomAutoFunctionMiddleware)
+ .Build();
+```
+
+
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
new file mode 100644
index 0000000..21d3aa2
--- /dev/null
+++ b/.github/workflows/codeql-analysis.yml
@@ -0,0 +1,69 @@
+# CodeQL is the code analysis engine developed by GitHub to automate security checks.
+# The results are shown as code scanning alerts in GitHub. For more details, visit:
+# https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/about-code-scanning-with-codeql
+
+name: "CodeQL"
+
+on:
+ workflow_dispatch:
+ push:
+ # TODO: Add "feature*" back in again, once we determine the cause of the ongoing CodeQL failures.
+ branches: ["main", "experimental*", "*-development"]
+ schedule:
+ - cron: "17 11 * * 2"
+
+jobs:
+ analyze:
+ name: Analyze
+ runs-on: ubuntu-latest
+ permissions:
+ actions: read
+ contents: read
+ security-events: write
+
+ strategy:
+ fail-fast: false
+ matrix:
+ language: ["csharp", "python"]
+ # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
+ # Use only 'java' to analyze code written in Java, Kotlin or both
+ # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both
+ # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v6
+ with:
+ persist-credentials: false
+
+ # Initializes the CodeQL tools for scanning.
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v4
+ with:
+ languages: ${{ matrix.language }}
+ # If you wish to specify custom queries, you can do so here or in a config file.
+ # By default, queries listed here will override any specified in a config file.
+ # Prefix the list here with "+" to use these queries and those in the config file.
+
+ # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
+ # queries: security-extended,security-and-quality
+
+ # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
+ # If this step fails, then you should remove it and run the build manually (see below)
+ - name: Autobuild
+ uses: github/codeql-action/autobuild@v4
+
+ # ℹ️ Command-line programs to run using the OS shell.
+ # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
+
+ # If the Autobuild fails above, remove it and uncomment the following three lines.
+ # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
+
+ # - run: |
+ # echo "Run, Build Application using script"
+ # ./location_of_script_within_repo/buildscript.sh
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v4
+ with:
+ category: "/language:${{matrix.language}}"
diff --git a/.github/workflows/dotnet-build-and-test.yml b/.github/workflows/dotnet-build-and-test.yml
new file mode 100644
index 0000000..31d1420
--- /dev/null
+++ b/.github/workflows/dotnet-build-and-test.yml
@@ -0,0 +1,285 @@
+#
+# This workflow will build all .slnx files in the dotnet folder, and run all unit tests and integration tests using dotnet docker containers,
+# each targeting a single version of the dotnet SDK.
+#
+
+name: dotnet-build-and-test
+
+on:
+ workflow_dispatch:
+ pull_request:
+ branches: ["main", "feature*"]
+ merge_group:
+ branches: ["main", "feature*"]
+ push:
+ branches: ["main", "feature*"]
+ schedule:
+ - cron: "0 0 * * *" # Run at midnight UTC daily
+
+env:
+ COVERAGE_THRESHOLD: 80
+ COVERAGE_FRAMEWORK: net10.0 # framework target for which we run/report code coverage
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
+permissions:
+ contents: read
+ id-token: "write"
+
+jobs:
+ paths-filter:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ pull-requests: read
+ outputs:
+ dotnetChanges: ${{ steps.filter.outputs.dotnet }}
+ cosmosDbChanges: ${{ steps.filter.outputs.cosmosdb }}
+ steps:
+ - uses: actions/checkout@v6
+ - uses: dorny/paths-filter@v3
+ id: filter
+ with:
+ filters: |
+ dotnet:
+ - 'dotnet/**'
+ cosmosdb:
+ - 'dotnet/src/Microsoft.Agents.AI.CosmosNoSql/**'
+ # run only if 'dotnet' files were changed
+ - name: dotnet tests
+ if: steps.filter.outputs.dotnet == 'true'
+ run: echo "Dotnet file"
+ - name: dotnet CosmosDB tests
+ if: steps.filter.outputs.cosmosdb == 'true'
+ run: echo "Dotnet CosmosDB changes"
+ # run only if not 'dotnet' files were changed
+ - name: not dotnet tests
+ if: steps.filter.outputs.dotnet != 'true'
+ run: echo "NOT dotnet file"
+
+ dotnet-build-and-test:
+ needs: paths-filter
+ if: needs.paths-filter.outputs.dotnetChanges == 'true'
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - { targetFramework: "net10.0", os: "ubuntu-latest", configuration: Release, integration-tests: true, environment: "integration" }
+ - { targetFramework: "net9.0", os: "windows-latest", configuration: Debug }
+ - { targetFramework: "net8.0", os: "ubuntu-latest", configuration: Release }
+ - { targetFramework: "net472", os: "windows-latest", configuration: Release, integration-tests: true, environment: "integration" }
+
+ runs-on: ${{ matrix.os }}
+ environment: ${{ matrix.environment }}
+ steps:
+ - uses: actions/checkout@v6
+ with:
+ persist-credentials: false
+ sparse-checkout: |
+ .
+ .github
+ 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)) }}
+ shell: pwsh
+ run: |
+ Write-Host "Launching Azure Cosmos DB Emulator"
+ Import-Module "$env:ProgramFiles\Azure Cosmos DB Emulator\PSModules\Microsoft.Azure.CosmosDB.Emulator"
+ Start-CosmosDbEmulator -NoUI -Key "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="
+ echo "COSMOS_EMULATOR_AVAILABLE=true" >> $env:GITHUB_ENV
+
+ - name: Setup dotnet
+ uses: actions/setup-dotnet@v5.1.0
+ with:
+ global-json-file: ${{ github.workspace }}/dotnet/global.json
+ - name: Build dotnet solutions
+ shell: bash
+ run: |
+ export SOLUTIONS=$(find ./dotnet/ -type f -name "*.slnx" | tr '\n' ' ')
+ for solution in $SOLUTIONS; do
+ dotnet build $solution -c ${{ matrix.configuration }} --warnaserror
+ done
+ - name: Package install check
+ shell: bash
+ # All frameworks are only built for the release configuration, so we only run this step for the release configuration
+ # and dotnet new doesn't support net472
+ if: matrix.configuration == 'Release' && matrix.targetFramework != 'net472'
+ run: |
+ TEMP_DIR=$(mktemp -d)
+
+ export SOLUTIONS=$(find ./dotnet/ -type f -name "*.slnx" | tr '\n' ' ')
+ for solution in $SOLUTIONS; do
+ dotnet pack $solution /property:TargetFrameworks=${{ matrix.targetFramework }} -c ${{ matrix.configuration }} --no-build --no-restore --output "$TEMP_DIR/artifacts"
+ done
+
+ pushd "$TEMP_DIR"
+
+ # Create a new console app to test the package installation
+ dotnet new console -f ${{ matrix.targetFramework }} --name packcheck --output consoleapp
+
+ # Create minimal nuget.config and use only dotnet nuget commands
+ echo ' ' > consoleapp/nuget.config
+
+ # Add sources with local first using dotnet nuget commands
+ dotnet nuget add source ../artifacts --name local --configfile consoleapp/nuget.config
+ dotnet nuget add source https://api.nuget.org/v3/index.json --name nuget.org --configfile consoleapp/nuget.config
+
+ # Change to project directory to ensure local nuget.config is used
+ pushd consoleapp
+ dotnet add packcheck.csproj package Microsoft.Agents.AI --prerelease
+ dotnet build -f ${{ matrix.targetFramework }} -c ${{ matrix.configuration }} packcheck.csproj
+
+ # Clean up
+ popd
+ popd
+ rm -rf "$TEMP_DIR"
+
+ - name: Run Unit Tests
+ shell: bash
+ run: |
+ export UT_PROJECTS=$(find ./dotnet -type f -name "*.UnitTests.csproj" | tr '\n' ' ')
+ 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
+ dotnet test -f ${{ matrix.targetFramework }} -c ${{ matrix.configuration }} $project --no-build -v Normal --logger trx --collect:"XPlat Code Coverage" --results-directory:"TestResults/Coverage/" -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.ExcludeByAttribute=GeneratedCodeAttribute,CompilerGeneratedAttribute,ExcludeFromCodeCoverageAttribute
+ else
+ dotnet test -f ${{ matrix.targetFramework }} -c ${{ matrix.configuration }} $project --no-build -v Normal --logger trx
+ fi
+ else
+ echo "Skipping $project - does not support target framework ${{ matrix.targetFramework }} (supports: $target_frameworks)"
+ fi
+ done
+ env:
+ # Cosmos DB Emulator connection settings
+ COSMOSDB_ENDPOINT: https://localhost:8081
+ 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 }}"
+
+ - name: Azure CLI Login
+ if: github.event_name != 'pull_request' && matrix.integration-tests
+ uses: azure/login@v2
+ with:
+ client-id: ${{ secrets.AZURE_CLIENT_ID }}
+ tenant-id: ${{ secrets.AZURE_TENANT_ID }}
+ subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+
+ # This setup action is required for both Durable Task and Azure Functions integration tests.
+ # We only run it on Ubuntu since the Durable Task and Azure Functions features are not available
+ # on .NET Framework (net472) which is what we use the Windows runner for.
+ - name: Set up Durable Task and Azure Functions Integration Test Emulators
+ if: github.event_name != 'pull_request' && matrix.integration-tests && matrix.os == 'ubuntu-latest'
+ uses: ./.github/actions/azure-functions-integration-setup
+ id: azure-functions-setup
+
+ - name: Run Integration Tests
+ shell: bash
+ if: github.event_name != 'pull_request' && matrix.integration-tests
+ run: |
+ export INTEGRATION_TEST_PROJECTS=$(find ./dotnet -type f -name "*IntegrationTests.csproj" | tr '\n' ' ')
+ 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
+ else
+ echo "Skipping $project - does not support target framework ${{ matrix.targetFramework }} (supports: $target_frameworks)"
+ fi
+ done
+ env:
+ # Cosmos DB Emulator connection settings
+ COSMOSDB_ENDPOINT: https://localhost:8081
+ COSMOSDB_KEY: C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==
+ # OpenAI Models
+ OpenAI__ApiKey: ${{ secrets.OPENAI__APIKEY }}
+ OpenAI__ChatModelId: ${{ vars.OPENAI__CHATMODELID }}
+ OpenAI__ChatReasoningModelId: ${{ vars.OPENAI__CHATREASONINGMODELID }}
+ # Azure OpenAI Models
+ AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
+ AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
+ # Azure AI Foundry
+ AzureAI__Endpoint: ${{ secrets.AZUREAI__ENDPOINT }}
+ AzureAI__DeploymentName: ${{ vars.AZUREAI__DEPLOYMENTNAME }}
+ AzureAI__BingConnectionId: ${{ vars.AZUREAI__BINGCONECTIONID }}
+ FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
+ FOUNDRY_MEDIA_DEPLOYMENT_NAME: ${{ vars.FOUNDRY_MEDIA_DEPLOYMENT_NAME }}
+ FOUNDRY_MODEL_DEPLOYMENT_NAME: ${{ vars.FOUNDRY_MODEL_DEPLOYMENT_NAME }}
+ FOUNDRY_CONNECTION_GROUNDING_TOOL: ${{ vars.FOUNDRY_CONNECTION_GROUNDING_TOOL }}
+
+ # Generate test reports and check coverage
+ - name: Generate test reports
+ if: matrix.targetFramework == env.COVERAGE_FRAMEWORK
+ uses: danielpalme/ReportGenerator-GitHub-Action@5.5.1
+ with:
+ reports: "./TestResults/Coverage/**/coverage.cobertura.xml"
+ targetdir: "./TestResults/Reports"
+ reporttypes: "HtmlInline;JsonSummary"
+
+ - name: Upload coverage report artifact
+ if: matrix.targetFramework == env.COVERAGE_FRAMEWORK
+ uses: actions/upload-artifact@v6
+ with:
+ name: CoverageReport-${{ matrix.os }}-${{ matrix.targetFramework }}-${{ matrix.configuration }} # Artifact name
+ path: ./TestResults/Reports # Directory containing files to upload
+
+ - name: Check coverage
+ if: matrix.targetFramework == env.COVERAGE_FRAMEWORK
+ shell: pwsh
+ run: .github/workflows/dotnet-check-coverage.ps1 -JsonReportPath "TestResults/Reports/Summary.json" -CoverageThreshold $env:COVERAGE_THRESHOLD
+
+ # This final job is required to satisfy the merge queue. It must only run (or succeed) if no tests failed
+ dotnet-build-and-test-check:
+ if: always()
+ runs-on: ubuntu-latest
+ needs: [dotnet-build-and-test]
+ steps:
+ - name: Get Date
+ shell: bash
+ run: |
+ echo "date=$(date +'%m/%d/%Y %H:%M:%S')" >> "$GITHUB_ENV"
+
+ - name: Run Type is Daily
+ if: ${{ github.event_name == 'schedule' }}
+ shell: bash
+ run: |
+ echo "run_type=Daily" >> "$GITHUB_ENV"
+
+ - name: Run Type is Manual
+ if: ${{ github.event_name == 'workflow_dispatch' }}
+ shell: bash
+ run: |
+ echo "run_type=Manual" >> "$GITHUB_ENV"
+
+ - name: Run Type is ${{ github.event_name }}
+ if: ${{ github.event_name != 'schedule' && github.event_name != 'workflow_dispatch'}}
+ shell: bash
+ run: |
+ echo "run_type=${{ github.event_name }}" >> "$GITHUB_ENV"
+
+ - name: Fail workflow if tests failed
+ id: check_tests_failed
+ if: contains(join(needs.*.result, ','), 'failure')
+ uses: actions/github-script@v8
+ with:
+ script: core.setFailed('Integration Tests Failed!')
+
+ - name: Fail workflow if tests cancelled
+ id: check_tests_cancelled
+ if: contains(join(needs.*.result, ','), 'cancelled')
+ uses: actions/github-script@v8
+ with:
+ script: core.setFailed('Integration Tests Cancelled!')
diff --git a/.github/workflows/dotnet-check-coverage.ps1 b/.github/workflows/dotnet-check-coverage.ps1
new file mode 100644
index 0000000..d2154f8
--- /dev/null
+++ b/.github/workflows/dotnet-check-coverage.ps1
@@ -0,0 +1,82 @@
+param (
+ [string]$JsonReportPath,
+ [double]$CoverageThreshold
+)
+
+$jsonContent = Get-Content $JsonReportPath -Raw | ConvertFrom-Json
+$coverageBelowThreshold = $false
+
+$nonExperimentalAssemblies = [System.Collections.Generic.HashSet[string]]::new()
+
+$assembliesCollection = @(
+ 'Microsoft.Agents.AI.Abstractions'
+ 'Microsoft.Agents.AI'
+)
+
+foreach ($assembly in $assembliesCollection) {
+ $nonExperimentalAssemblies.Add($assembly)
+}
+
+function Get-FormattedValue {
+ param (
+ [float]$Coverage,
+ [bool]$UseIcon = $false
+ )
+ $formattedNumber = "{0:N1}" -f $Coverage
+ $icon = if (-not $UseIcon) { "" } elseif ($Coverage -ge $CoverageThreshold) { '✅' } else { '❌' }
+
+ return "$formattedNumber% $icon"
+}
+
+$totallines = $jsonContent.summary.totallines
+$totalbranches = $jsonContent.summary.totalbranches
+$lineCoverage = $jsonContent.summary.linecoverage
+$branchCoverage = $jsonContent.summary.branchcoverage
+
+$totalTableData = [PSCustomObject]@{
+ 'Metric' = 'Total Coverage'
+ 'Total Lines' = $totallines
+ 'Total Branches' = $totalbranches
+ 'Line Coverage' = Get-FormattedValue -Coverage $lineCoverage
+ 'Branch Coverage' = Get-FormattedValue -Coverage $branchCoverage
+}
+
+$totalTableData | Format-Table -AutoSize
+
+$assemblyTableData = @()
+
+foreach ($assembly in $jsonContent.coverage.assemblies) {
+ $assemblyName = $assembly.name
+ $assemblyTotallines = $assembly.totallines
+ $assemblyTotalbranches = $assembly.totalbranches
+ $assemblyLineCoverage = $assembly.coverage
+ $assemblyBranchCoverage = $assembly.branchcoverage
+
+ $isNonExperimentalAssembly = $nonExperimentalAssemblies -contains $assemblyName
+
+ $lineCoverageFailed = $assemblyLineCoverage -lt $CoverageThreshold -and $assemblyTotallines -gt 0
+ $branchCoverageFailed = $assemblyBranchCoverage -lt $CoverageThreshold -and $assemblyTotalbranches -gt 0
+
+ if ($isNonExperimentalAssembly -and ($lineCoverageFailed -or $branchCoverageFailed)) {
+ $coverageBelowThreshold = $true
+ }
+
+ $assemblyTableData += [PSCustomObject]@{
+ 'Assembly Name' = $assemblyName
+ 'Total Lines' = $assemblyTotallines
+ 'Total Branches' = $assemblyTotalbranches
+ 'Line Coverage' = Get-FormattedValue -Coverage $assemblyLineCoverage -UseIcon $isNonExperimentalAssembly
+ 'Branch Coverage' = Get-FormattedValue -Coverage $assemblyBranchCoverage -UseIcon $isNonExperimentalAssembly
+ }
+}
+
+$sortedTable = $assemblyTableData | Sort-Object {
+ $nonExperimentalAssemblies -contains $_.'Assembly Name'
+} -Descending
+
+$sortedTable | Format-Table -AutoSize
+
+if ($coverageBelowThreshold) {
+ Write-Host "Code coverage is lower than defined threshold: $CoverageThreshold. Stopping the task."
+ exit 1
+}
diff --git a/.github/workflows/dotnet-format.yml b/.github/workflows/dotnet-format.yml
new file mode 100644
index 0000000..8d7c9fe
--- /dev/null
+++ b/.github/workflows/dotnet-format.yml
@@ -0,0 +1,96 @@
+#
+# This workflow runs the dotnet formatter on all c-sharp code.
+#
+
+name: dotnet-format
+
+on:
+ workflow_dispatch:
+ pull_request:
+ branches: ["main", "feature*"]
+ paths:
+ - dotnet/**
+ - '.github/workflows/dotnet-format.yml'
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ check-format:
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - { dotnet: "10.0", configuration: Release, os: ubuntu-latest }
+
+ runs-on: ${{ matrix.os }}
+ env:
+ NUGET_CERT_REVOCATION_MODE: offline
+
+ steps:
+ - name: Check out code
+ uses: actions/checkout@v6
+ with:
+ fetch-depth: 0
+ persist-credentials: false
+ sparse-checkout: |
+ .
+ .github
+ dotnet
+
+ - name: Get changed files
+ id: changed-files
+ if: github.event_name == 'pull_request'
+ uses: jitterbit/get-changed-files@v1
+ continue-on-error: true
+
+ - name: No C# files changed
+ id: no-csharp
+ if: github.event_name == 'pull_request' && steps.changed-files.outputs.added_modified == ''
+ run: echo "No C# files changed"
+
+ # This step will loop over the changed files and find the nearest .csproj file for each one, then store the unique csproj files in a variable
+ - name: Find csproj files
+ id: find-csproj
+ if: github.event_name != 'pull_request' || steps.changed-files.outputs.added_modified != '' || steps.changed-files.outcome == 'failure'
+ run: |
+ csproj_files=()
+ exclude_files=("Experimental.Orchestration.Flow.csproj" "Experimental.Orchestration.Flow.UnitTests.csproj" "Experimental.Orchestration.Flow.IntegrationTests.csproj")
+ if [[ ${{ steps.changed-files.outcome }} == 'success' ]]; then
+ for file in ${{ steps.changed-files.outputs.added_modified }}; do
+ echo "$file was changed"
+ dir="./$file"
+ while [[ $dir != "." && $dir != "/" && $dir != $GITHUB_WORKSPACE ]]; do
+ if find "$dir" -maxdepth 1 -name "*.csproj" -print -quit | grep -q .; then
+ csproj_path="$(find "$dir" -maxdepth 1 -name "*.csproj" -print -quit)"
+ if [[ ! "${exclude_files[@]}" =~ "${csproj_path##*/}" ]]; then
+ csproj_files+=("$csproj_path")
+ fi
+ break
+ fi
+
+ dir=$(echo ${dir%/*})
+ done
+ done
+ else
+ # if the changed-files step failed, run dotnet on the whole slnx instead of specific projects
+ csproj_files=$(find ./ -type f -name "*.slnx" | tr '\n' ' ');
+ fi
+ csproj_files=($(printf "%s\n" "${csproj_files[@]}" | sort -u))
+ echo "Found ${#csproj_files[@]} unique csproj/slnx files: ${csproj_files[*]}"
+ echo "csproj_files=${csproj_files[*]}" >> $GITHUB_OUTPUT
+
+ - name: Pull container dotnet/sdk:${{ matrix.dotnet }}
+ if: steps.find-csproj.outputs.csproj_files != ''
+ run: docker pull mcr.microsoft.com/dotnet/sdk:${{ matrix.dotnet }}
+
+ # This step will run dotnet format on each of the unique csproj files and fail if any changes are made
+ # exclude-diagnostics should be removed after fixes for IL2026 and IL3050 are out: https://github.com/dotnet/sdk/issues/51136
+ - name: Run dotnet format
+ if: steps.find-csproj.outputs.csproj_files != ''
+ run: |
+ for csproj in ${{ steps.find-csproj.outputs.csproj_files }}; do
+ echo "Running dotnet format on $csproj"
+ docker run --rm -v $(pwd):/app -w /app mcr.microsoft.com/dotnet/sdk:${{ matrix.dotnet }} /bin/sh -c "dotnet format $csproj --verify-no-changes --verbosity diagnostic --exclude-diagnostics IL2026 IL3050"
+ done
diff --git a/.github/workflows/label-issues.yml b/.github/workflows/label-issues.yml
new file mode 100644
index 0000000..111c63e
--- /dev/null
+++ b/.github/workflows/label-issues.yml
@@ -0,0 +1,112 @@
+name: Label issues
+on:
+ issues:
+ types:
+ - reopened
+ - opened
+
+jobs:
+ label_issues:
+ name: "Issue: add labels"
+ if: ${{ github.event.action == 'opened' || github.event.action == 'reopened' }}
+ runs-on: ubuntu-latest
+ permissions:
+ issues: write
+ steps:
+ - uses: actions/github-script@v8
+ with:
+ github-token: ${{ secrets.GH_ACTIONS_PR_WRITE }}
+ script: |
+ // Get the issue body and title
+ const body = context.payload.issue.body
+ let title = context.payload.issue.title
+
+ // Define the labels array
+ let labels = []
+
+ // Check if the issue author is in the agentframework-developers team
+ let isTeamMember = false
+ try {
+ const teamMembership = await github.rest.teams.getMembershipForUserInOrg({
+ org: context.repo.owner,
+ team_slug: process.env.TEAM_NAME,
+ username: context.payload.issue.user.login
+ })
+ console.log("Team Membership Data:", teamMembership);
+ isTeamMember = teamMembership.data.state === 'active'
+ } catch (error) {
+ // User is not in the team or team doesn't exist
+ console.error("Error fetching team membership:", error);
+ isTeamMember = false
+ }
+
+ // Only add triage label if the author is not in the team
+ if (!isTeamMember) {
+ labels.push("triage")
+ }
+
+ // Helper function to extract field value from issue form body
+ // Issue forms format fields as: ### Field Name\n\nValue
+ function getFormFieldValue(body, fieldName) {
+ if (!body) return null
+ const regex = new RegExp(`###\\s*${fieldName}\\s*\\n\\n([^\\n#]+)`, 'i')
+ const match = body.match(regex)
+ return match ? match[1].trim() : null
+ }
+
+ // Check for language from issue form dropdown first
+ const languageField = getFormFieldValue(body, 'Language')
+ let languageLabelAdded = false
+
+ if (languageField) {
+ if (languageField === 'Python') {
+ labels.push("python")
+ languageLabelAdded = true
+ } else if (languageField === '.NET') {
+ labels.push(".NET")
+ languageLabelAdded = true
+ }
+ // 'None / Not Applicable' - don't add any language label
+ }
+
+ // Fallback: Check if the body or the title contains the word 'python' (case-insensitive)
+ // Only if language wasn't already determined from the form field
+ if (!languageLabelAdded) {
+ if ((body != null && body.match(/python/i)) || (title != null && title.match(/python/i))) {
+ // Add the 'python' label to the array
+ labels.push("python")
+ }
+
+ // Check if the body or the title contains the words 'dotnet', '.net', 'c#' or 'csharp' (case-insensitive)
+ if ((body != null && body.match(/\.net/i)) || (title != null && title.match(/\.net/i)) ||
+ (body != null && body.match(/dotnet/i)) || (title != null && title.match(/dotnet/i)) ||
+ (body != null && body.match(/C#/i)) || (title != null && title.match(/C#/i)) ||
+ (body != null && body.match(/csharp/i)) || (title != null && title.match(/csharp/i))) {
+ // Add the '.NET' label to the array
+ labels.push(".NET")
+ }
+ }
+
+ // Check for issue type from issue form dropdown
+ const issueTypeField = getFormFieldValue(body, 'Type of Issue')
+ if (issueTypeField) {
+ if (issueTypeField === 'Bug') {
+ labels.push("bug")
+ } else if (issueTypeField === 'Feature Request') {
+ labels.push("enhancement")
+ } else if (issueTypeField === 'Question') {
+ labels.push("question")
+ }
+ }
+
+ // Add the labels to the issue (only if there are labels to add)
+ if (labels.length > 0) {
+ github.rest.issues.addLabels({
+ issue_number: context.issue.number,
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ labels: labels
+ });
+ }
+ env:
+ TEAM_NAME: ${{ secrets.DEVELOPER_TEAM }}
diff --git a/.github/workflows/label-pr.yml b/.github/workflows/label-pr.yml
new file mode 100644
index 0000000..4aea432
--- /dev/null
+++ b/.github/workflows/label-pr.yml
@@ -0,0 +1,21 @@
+# This workflow will triage pull requests and apply a label based on the
+# paths that are modified in the pull request.
+#
+# To use this workflow, you will need to set up a .github/labeler.yml
+# file with configuration. For more information, see:
+# https://github.com/actions/labeler
+
+name: Label pull request
+on: [pull_request_target]
+
+jobs:
+ add_label:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ pull-requests: write
+
+ steps:
+ - uses: actions/labeler@v6
+ with:
+ repo-token: "${{ secrets.GH_ACTIONS_PR_WRITE }}"
diff --git a/.github/workflows/label-title-prefix.yml b/.github/workflows/label-title-prefix.yml
new file mode 100644
index 0000000..b8d5b76
--- /dev/null
+++ b/.github/workflows/label-title-prefix.yml
@@ -0,0 +1,72 @@
+name: Label title prefix
+on:
+ issues:
+ types: [labeled]
+ pull_request_target:
+ types: [labeled]
+
+jobs:
+ add_title_prefix:
+ name: "Issue/PR: add title prefix"
+ continue-on-error: true
+ runs-on: ubuntu-latest
+ permissions:
+ issues: write
+ pull-requests: write
+
+ steps:
+ - uses: actions/github-script@v8
+ name: "Issue/PR: update title"
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ let prefixLabels = {
+ "python": "Python",
+ ".NET": ".NET"
+ };
+
+ function addTitlePrefix(title, prefix)
+ {
+ // Update the title based on the label and prefix
+ // Check if the title starts with the prefix (case-sensitive)
+ if (!title.startsWith(prefix + ": ")) {
+ // If not, check if the first word is the label (case-insensitive)
+ if (title.match(new RegExp(`^${prefix}`, 'i'))) {
+ // If yes, replace it with the prefix (case-sensitive)
+ title = title.replace(new RegExp(`^${prefix}`, 'i'), prefix);
+ } else {
+ // If not, prepend the prefix to the title
+ title = prefix + ": " + title;
+ }
+ }
+
+ return title;
+ }
+
+ labelAdded = context.payload.label.name
+
+ // Check if the issue or PR has the label
+ if (labelAdded in prefixLabels) {
+ let prefix = prefixLabels[labelAdded];
+ switch(context.eventName) {
+ case 'issues':
+ github.rest.issues.update({
+ issue_number: context.issue.number,
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ title: addTitlePrefix(context.payload.issue.title, prefix)
+ });
+ break
+
+ case 'pull_request_target':
+ github.rest.pulls.update({
+ pull_number: context.issue.number,
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ title: addTitlePrefix(context.payload.pull_request.title, prefix)
+ });
+ break
+ default:
+ core.setFailed('Unrecognited eventName: ' + context.eventName);
+ }
+ }
diff --git a/.github/workflows/markdown-link-check.yml b/.github/workflows/markdown-link-check.yml
new file mode 100644
index 0000000..5c984c5
--- /dev/null
+++ b/.github/workflows/markdown-link-check.yml
@@ -0,0 +1,33 @@
+name: Check .md links
+
+on:
+ workflow_dispatch:
+ pull_request:
+ branches: ["main"]
+ paths:
+ - '**.md'
+ - '.github/workflows/markdown-link-check.yml'
+ - '.github/.linkspector.yml'
+ schedule:
+ - cron: "0 0 * * *" # Run at midnight UTC daily
+
+permissions:
+ contents: read
+
+jobs:
+ markdown-link-check:
+ runs-on: ubuntu-22.04
+ # check out the latest version of the code
+ steps:
+ - uses: actions/checkout@v6
+ with:
+ persist-credentials: false
+
+ # Checks the status of hyperlinks in all files
+ - name: Run linkspector
+ uses: umbrelladocs/action-linkspector@v1
+ with:
+ reporter: local
+ filter_mode: nofilter
+ fail_on_error: true
+ config_file: ".github/.linkspector.yml"
diff --git a/.github/workflows/merge-gatekeeper.yml b/.github/workflows/merge-gatekeeper.yml
new file mode 100644
index 0000000..de1a68a
--- /dev/null
+++ b/.github/workflows/merge-gatekeeper.yml
@@ -0,0 +1,32 @@
+name: Merge Gatekeeper
+
+on:
+ pull_request:
+ branches: [ "main", "feature*" ]
+ merge_group:
+ branches: ["main"]
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ merge-gatekeeper:
+ runs-on: ubuntu-latest
+ # Restrict permissions of the GITHUB_TOKEN.
+ # Docs: https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs
+ permissions:
+ checks: read
+ statuses: read
+ steps:
+ - name: Run Merge Gatekeeper
+ # NOTE: v1 is updated to reflect the latest v1.x.y. Please use any tag/branch that suits your needs:
+ # https://github.com/upsidr/merge-gatekeeper/tags
+ # https://github.com/upsidr/merge-gatekeeper/branches
+ uses: upsidr/merge-gatekeeper@v1
+ if: github.event_name == 'pull_request'
+ with:
+ token: ${{ secrets.GITHUB_TOKEN }}
+ timeout: 3600
+ interval: 30
+ ignored: CodeQL,CodeQL analysis (csharp)
diff --git a/.github/workflows/python-code-quality.yml b/.github/workflows/python-code-quality.yml
new file mode 100644
index 0000000..4139d47
--- /dev/null
+++ b/.github/workflows/python-code-quality.yml
@@ -0,0 +1,53 @@
+name: Python - Code Quality
+on:
+ merge_group:
+ workflow_dispatch:
+ pull_request:
+ branches: ["main"]
+ paths:
+ - "python/**"
+
+env:
+ # Configure a constant location for the uv cache
+ UV_CACHE_DIR: /tmp/.uv-cache
+
+jobs:
+ pre-commit:
+ name: Checks
+ if: "!cancelled()"
+ strategy:
+ fail-fast: false
+ matrix:
+ python-version: ["3.10", "3.14"]
+ runs-on: ubuntu-latest
+ continue-on-error: true
+ defaults:
+ run:
+ working-directory: ./python
+ env:
+ UV_PYTHON: ${{ matrix.python-version }}
+ steps:
+ - uses: actions/checkout@v6
+ with:
+ fetch-depth: 0
+ - name: Set up python and install the project
+ id: python-setup
+ uses: ./.github/actions/python-setup
+ with:
+ python-version: ${{ matrix.python-version }}
+ os: ${{ runner.os }}
+ env:
+ # Configure a constant location for the uv cache
+ UV_CACHE_DIR: /tmp/.uv-cache
+ - uses: actions/cache@v5
+ with:
+ path: ~/.cache/pre-commit
+ key: pre-commit|${{ matrix.python-version }}|${{ hashFiles('python/.pre-commit-config.yaml') }}
+ - uses: pre-commit/action@v3.0.1
+ name: Run Pre-Commit Hooks
+ with:
+ extra_args: --config python/.pre-commit-config.yaml --all-files
+ - name: Run Mypy
+ env:
+ GITHUB_BASE_REF: ${{ github.event.pull_request.base.ref || github.base_ref || 'main' }}
+ run: uv run poe ci-mypy
diff --git a/.github/workflows/python-docs.yml b/.github/workflows/python-docs.yml
new file mode 100644
index 0000000..f962ec3
--- /dev/null
+++ b/.github/workflows/python-docs.yml
@@ -0,0 +1,39 @@
+name: Python - Create Docs
+
+on:
+ workflow_dispatch:
+ release:
+ types: [published]
+
+permissions:
+ contents: write
+ id-token: write
+env:
+ # Configure a constant location for the uv cache
+ UV_CACHE_DIR: /tmp/.uv-cache
+
+jobs:
+ python-build-docs:
+ if: github.event_name == 'release' && startsWith(github.event.release.tag_name, 'python-')
+ name: Python Build Docs
+ runs-on: ubuntu-latest
+ environment: "integration"
+ env:
+ UV_PYTHON: "3.11"
+ defaults:
+ run:
+ working-directory: python
+ steps:
+ - uses: actions/checkout@v6
+ - name: Set up uv
+ uses: astral-sh/setup-uv@v7
+ with:
+ version-file: "python/pyproject.toml"
+ enable-cache: true
+ cache-suffix: ${{ runner.os }}-${{ env.UV_PYTHON }}
+ cache-dependency-glob: "**/uv.lock"
+ - name: Install dependencies
+ run: uv sync --all-packages --dev --docs
+ - name: Build the docs
+ run: uv run poe docs-full
+ # Upload docs to learn gh
diff --git a/.github/workflows/python-lab-tests.yml b/.github/workflows/python-lab-tests.yml
new file mode 100644
index 0000000..f5cb504
--- /dev/null
+++ b/.github/workflows/python-lab-tests.yml
@@ -0,0 +1,99 @@
+name: Python - Lab Tests
+
+on:
+ workflow_dispatch:
+ pull_request:
+ branches: ["main"]
+ paths:
+ - "python/packages/lab/**"
+ merge_group:
+ branches: ["main"]
+ schedule:
+ - cron: "0 0 * * *" # Run at midnight UTC daily
+
+env:
+ # Configure a constant location for the uv cache
+ UV_CACHE_DIR: /tmp/.uv-cache
+
+jobs:
+ paths-filter:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ pull-requests: read
+ outputs:
+ pythonChanges: ${{ steps.filter.outputs.python}}
+ steps:
+ - uses: actions/checkout@v6
+ - uses: dorny/paths-filter@v3
+ id: filter
+ with:
+ filters: |
+ python:
+ - 'python/**'
+ # run only if 'python' files were changed
+ - name: python tests
+ if: steps.filter.outputs.python == 'true'
+ run: echo "Python file"
+ # run only if not 'python' files were changed
+ - name: not python tests
+ if: steps.filter.outputs.python != 'true'
+ run: echo "NOT python file"
+
+ python-lab-tests:
+ name: Python Lab Tests
+ needs: paths-filter
+ if: needs.paths-filter.outputs.pythonChanges == 'true'
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: true
+ matrix:
+ python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
+ # TODO(ekzhu): re-enable macos-latest when this is fixed: https://github.com/actions/runner-images/issues/11881
+ os: [ubuntu-latest, windows-latest]
+ env:
+ UV_PYTHON: ${{ matrix.python-version }}
+ permissions:
+ contents: read
+ defaults:
+ run:
+ working-directory: python
+ steps:
+ - uses: actions/checkout@v6
+
+ - name: Set up python and install the project
+ id: python-setup
+ uses: ./.github/actions/python-setup
+ with:
+ python-version: ${{ matrix.python-version }}
+ os: ${{ runner.os }}
+ env:
+ # Configure a constant location for the uv cache
+ UV_CACHE_DIR: /tmp/.uv-cache
+
+ # Lab specific tests
+ - name: Run lab tests
+ run: cd packages/lab && uv run poe test
+
+ - name: Run lab lint
+ run: cd packages/lab && uv run poe lint
+
+ - name: Run lab format check
+ run: cd packages/lab && uv run poe fmt --check
+
+ - name: Run lab type checking
+ run: cd packages/lab && uv run poe pyright
+
+ - name: Run lab mypy
+ run: cd packages/lab && uv run poe mypy
+
+ # Surface failing tests
+ - name: Surface failing tests
+ if: always()
+ uses: pmeier/pytest-results-action@v0.7.2
+ with:
+ path: ./python/packages/lab/**.xml
+ summary: true
+ display-options: fEX
+ fail-on-empty: false
+ title: Lab Test Results
diff --git a/.github/workflows/python-merge-tests.yml b/.github/workflows/python-merge-tests.yml
new file mode 100644
index 0000000..0dafc12
--- /dev/null
+++ b/.github/workflows/python-merge-tests.yml
@@ -0,0 +1,198 @@
+name: Python - Merge - Tests
+
+on:
+ workflow_dispatch:
+ pull_request:
+ branches: ["main"]
+ merge_group:
+ branches: ["main"]
+ schedule:
+ - cron: "0 0 * * *" # Run at midnight UTC daily
+
+permissions:
+ contents: write
+ id-token: write
+
+env:
+ # Configure a constant location for the uv cache
+ UV_CACHE_DIR: /tmp/.uv-cache
+ RUN_INTEGRATION_TESTS: "true"
+ RUN_SAMPLES_TESTS: ${{ vars.RUN_SAMPLES_TESTS }}
+
+jobs:
+ paths-filter:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ pull-requests: read
+ outputs:
+ pythonChanges: ${{ steps.filter.outputs.python}}
+ steps:
+ - uses: actions/checkout@v6
+ - uses: dorny/paths-filter@v3
+ id: filter
+ with:
+ filters: |
+ python:
+ - 'python/**'
+ # run only if 'python' files were changed
+ - name: python tests
+ if: steps.filter.outputs.python == 'true'
+ run: echo "Python file"
+ # run only if not 'python' files were changed
+ - name: not python tests
+ if: steps.filter.outputs.python != 'true'
+ run: echo "NOT python file"
+ python-tests-core:
+ name: Python Tests - Core
+ needs: paths-filter
+ if: github.event_name != 'pull_request' && needs.paths-filter.outputs.pythonChanges == 'true'
+ runs-on: ${{ matrix.os }}
+ environment: ${{ matrix.environment }}
+ strategy:
+ fail-fast: true
+ matrix:
+ python-version: ["3.10"]
+ os: [ubuntu-latest]
+ environment: ["integration"]
+ env:
+ UV_PYTHON: ${{ matrix.python-version }}
+ OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }}
+ OPENAI_RESPONSES_MODEL_ID: ${{ vars.OPENAI__RESPONSESMODELID }}
+ OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ ANTHROPIC_CHAT_MODEL_ID: ${{ vars.ANTHROPIC_CHAT_MODEL_ID }}
+ AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
+ AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
+ AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
+ LOCAL_MCP_URL: ${{ vars.LOCAL_MCP__URL }}
+ # For Azure Functions integration tests
+ FUNCTIONS_WORKER_RUNTIME: "python"
+ DURABLE_TASK_SCHEDULER_CONNECTION_STRING: "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None"
+ AzureWebJobsStorage: "UseDevelopmentStorage=true"
+
+ defaults:
+ run:
+ working-directory: python
+ steps:
+ - uses: actions/checkout@v6
+ - name: Set up python and install the project
+ id: python-setup
+ uses: ./.github/actions/python-setup
+ with:
+ python-version: ${{ matrix.python-version }}
+ os: ${{ runner.os }}
+ env:
+ # Configure a constant location for the uv cache
+ UV_CACHE_DIR: /tmp/.uv-cache
+ - name: Azure CLI Login
+ if: github.event_name != 'pull_request'
+ uses: azure/login@v2
+ with:
+ client-id: ${{ secrets.AZURE_CLIENT_ID }}
+ tenant-id: ${{ secrets.AZURE_TENANT_ID }}
+ subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+ - name: Set up Azure Functions Integration Test Emulators
+ uses: ./.github/actions/azure-functions-integration-setup
+ id: azure-functions-setup
+ - name: Test with pytest
+ timeout-minutes: 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
+ if: env.RUN_SAMPLES_TESTS == 'true'
+ run: uv run pytest tests/samples/ -m "openai" -m "azure"
+ working-directory: ./python
+ - name: Surface failing tests
+ if: always()
+ uses: pmeier/pytest-results-action@v0.7.2
+ with:
+ path: ./python/**.xml
+ summary: true
+ display-options: fEX
+ fail-on-empty: false
+ title: Test results
+
+ python-tests-azure-ai:
+ name: Python Tests - Azure AI
+ needs: paths-filter
+ if: github.event_name != 'pull_request' && needs.paths-filter.outputs.pythonChanges == 'true'
+ runs-on: ${{ matrix.os }}
+ environment: ${{ matrix.environment }}
+ strategy:
+ fail-fast: true
+ matrix:
+ python-version: ["3.10"]
+ os: [ubuntu-latest]
+ environment: ["integration"]
+ env:
+ UV_PYTHON: ${{ matrix.python-version }}
+ AZURE_AI_PROJECT_ENDPOINT: ${{ secrets.AZUREAI__ENDPOINT }}
+ AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.AZUREAI__DEPLOYMENTNAME }}
+ LOCAL_MCP_URL: ${{ vars.LOCAL_MCP__URL }}
+ defaults:
+ run:
+ working-directory: python
+ steps:
+ - uses: actions/checkout@v6
+ - name: Set up python and install the project
+ id: python-setup
+ uses: ./.github/actions/python-setup
+ with:
+ python-version: ${{ matrix.python-version }}
+ os: ${{ runner.os }}
+ env:
+ # Configure a constant location for the uv cache
+ UV_CACHE_DIR: /tmp/.uv-cache
+ - name: Azure CLI Login
+ if: github.event_name != 'pull_request'
+ uses: azure/login@v2
+ with:
+ client-id: ${{ secrets.AZURE_CLIENT_ID }}
+ tenant-id: ${{ secrets.AZURE_TENANT_ID }}
+ subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+ - name: Test with pytest
+ timeout-minutes: 10
+ run: uv run --directory packages/azure-ai poe integration-tests -n logical --dist loadfile --dist worksteal --timeout 300 --retries 3 --retry-delay 10
+ working-directory: ./python
+ - name: Test Azure AI samples
+ timeout-minutes: 10
+ if: env.RUN_SAMPLES_TESTS == 'true'
+ run: uv run pytest tests/samples/ -m "azure-ai"
+ working-directory: ./python
+ - name: Surface failing tests
+ if: always()
+ uses: pmeier/pytest-results-action@v0.7.2
+ with:
+ path: ./python/**.xml
+ summary: true
+ display-options: fEX
+ fail-on-empty: false
+ title: Test results
+
+ # TODO: Add python-tests-lab
+
+ python-integration-tests-check:
+ if: always()
+ runs-on: ubuntu-latest
+ needs:
+ [
+ python-tests-core,
+ python-tests-azure-ai
+ ]
+ steps:
+
+ - name: Fail workflow if tests failed
+ id: check_tests_failed
+ if: contains(join(needs.*.result, ','), 'failure')
+ uses: actions/github-script@v8
+ with:
+ script: core.setFailed('Integration Tests Failed!')
+
+ - name: Fail workflow if tests cancelled
+ id: check_tests_cancelled
+ if: contains(join(needs.*.result, ','), 'cancelled')
+ uses: actions/github-script@v8
+ with:
+ script: core.setFailed('Integration Tests Cancelled!')
diff --git a/.github/workflows/python-release.yml b/.github/workflows/python-release.yml
new file mode 100644
index 0000000..ba6e368
--- /dev/null
+++ b/.github/workflows/python-release.yml
@@ -0,0 +1,62 @@
+name: Python - Build Release Assets
+
+on:
+ release:
+ types: [published]
+
+permissions:
+ contents: write
+ id-token: write
+env:
+ # Configure a constant location for the uv cache
+ UV_CACHE_DIR: /tmp/.uv-cache
+
+jobs:
+ python-build-assets:
+ if: github.event_name == 'release' && startsWith(github.event.release.tag_name, 'python-')
+ name: Python Build Assets and add to Release
+ runs-on: ubuntu-latest
+ environment: "integration"
+ env:
+ UV_PYTHON: "3.13"
+ defaults:
+ run:
+ working-directory: python
+ steps:
+ - uses: actions/checkout@v6
+ - name: Set up python and install the project
+ id: python-setup
+ uses: ./.github/actions/python-setup
+ with:
+ python-version: ${{ matrix.python-version }}
+ os: ${{ runner.os }}
+ env:
+ # Configure a constant location for the uv cache
+ UV_CACHE_DIR: /tmp/.uv-cache
+ - name: Set environment variables
+ run: |
+ # Extract package name from tag (format: python--)
+ TAG="${{ github.event.release.tag_name }}"
+ PACKAGE=$(echo "$TAG" | sed 's/^python-\([^-]*\)-.*$/\1/')
+
+ # Validate package exists
+ if [[ ! -d "packages/$PACKAGE" ]]; then
+ echo "Error: Package '$PACKAGE' not found in packages/ directory"
+ echo "Available packages: $(ls packages/)"
+ exit 1
+ fi
+
+ echo "PACKAGE=$PACKAGE" >> $GITHUB_ENV
+ echo "Building package: $PACKAGE"
+
+ - name: Check version
+ run: |
+ echo "Building and uploading Python package version: ${{ github.event.release.tag_name }}"
+ echo "Package directory: packages/${{ env.PACKAGE }}"
+ - name: Build the package
+ run: uv run poe --directory packages/${{ env.PACKAGE }} build
+ - name: Release
+ uses: softprops/action-gh-release@v2
+ with:
+ files: |
+ python/dist/*
diff --git a/.github/workflows/python-test-coverage-report.yml b/.github/workflows/python-test-coverage-report.yml
new file mode 100644
index 0000000..92e13f9
--- /dev/null
+++ b/.github/workflows/python-test-coverage-report.yml
@@ -0,0 +1,59 @@
+name: Python - Test Coverage Report
+
+on:
+ workflow_run:
+ workflows: ["Python - Test Coverage"]
+ types:
+ - completed
+
+permissions:
+ contents: read
+ pull-requests: write
+
+jobs:
+ python-test-coverage-report:
+ runs-on: ubuntu-latest
+ if: github.event.workflow_run.conclusion == 'success'
+ continue-on-error: false
+ defaults:
+ run:
+ working-directory: python
+ steps:
+ - uses: actions/checkout@v6
+ - name: Download coverage report
+ uses: actions/download-artifact@v7
+ with:
+ github-token: ${{ secrets.GH_ACTIONS_PR_WRITE }}
+ run-id: ${{ github.event.workflow_run.id }}
+ path: ./python
+ merge-multiple: true
+ - name: Display structure of downloaded files
+ run: ls
+ - name: Read and set PR number
+ # Need to read the PR number from the file saved in the previous workflow
+ # because the workflow_run event does not have access to the PR number
+ # The PR number is needed to post the comment on the PR
+ run: |
+ if [ ! -s pr_number ]; then
+ echo "PR number file 'pr_number' is missing or empty"
+ exit 1
+ fi
+ PR_NUMBER=$(head -1 pr_number | tr -dc '0-9')
+ if [ -z "$PR_NUMBER" ]; then
+ echo "PR number file 'pr_number' does not contain a valid PR number"
+ exit 1
+ fi
+ echo "PR_NUMBER=$PR_NUMBER" >> "$GITHUB_ENV"
+ - name: Pytest coverage comment
+ id: coverageComment
+ uses: MishaKav/pytest-coverage-comment@v1.2.0
+ with:
+ github-token: ${{ secrets.GH_ACTIONS_PR_WRITE }}
+ issue-number: ${{ env.PR_NUMBER }}
+ pytest-xml-coverage-path: python/python-coverage.xml
+ title: "Python Test Coverage Report"
+ badge-title: "Python Test Coverage"
+ junitxml-title: "Python Unit Test Overview"
+ junitxml-path: python/pytest.xml
+ default-branch: "main"
+ report-only-changed-files: true
diff --git a/.github/workflows/python-test-coverage.yml b/.github/workflows/python-test-coverage.yml
new file mode 100644
index 0000000..03cca20
--- /dev/null
+++ b/.github/workflows/python-test-coverage.yml
@@ -0,0 +1,49 @@
+name: Python - Test Coverage
+
+on:
+ pull_request:
+ branches: ["main", "feature*"]
+ paths:
+ - "python/packages/**"
+ - "python/tests/unit/**"
+env:
+ # Configure a constant location for the uv cache
+ UV_CACHE_DIR: /tmp/.uv-cache
+
+jobs:
+ python-tests-coverage:
+ runs-on: ubuntu-latest
+ continue-on-error: false
+ defaults:
+ run:
+ working-directory: python
+ env:
+ UV_PYTHON: "3.10"
+ steps:
+ - uses: actions/checkout@v6
+ # Save the PR number to a file since the workflow_run event
+ # in the coverage report workflow does not have access to it
+ - name: Save PR number
+ run: |
+ echo ${{ github.event.number }} > ./pr_number
+ - name: Set up python and install the project
+ id: python-setup
+ uses: ./.github/actions/python-setup
+ with:
+ python-version: ${{ matrix.python-version }}
+ os: ${{ runner.os }}
+ env:
+ # Configure a constant location for the uv cache
+ 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: Upload coverage report
+ uses: actions/upload-artifact@v6
+ with:
+ path: |
+ python/python-coverage.xml
+ python/pytest.xml
+ python/pr_number
+ overwrite: true
+ retention-days: 1
+ if-no-files-found: error
diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml
new file mode 100644
index 0000000..07b9200
--- /dev/null
+++ b/.github/workflows/python-tests.yml
@@ -0,0 +1,54 @@
+name: Python - Tests
+
+on:
+ pull_request:
+ branches: ["main", "feature*"]
+ paths:
+ - "python/**"
+env:
+ # Configure a constant location for the uv cache
+ UV_CACHE_DIR: /tmp/.uv-cache
+
+jobs:
+ python-tests:
+ name: Python Tests
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: true
+ matrix:
+ python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
+ # todo: add macos-latest when problems are resolved
+ os: [ubuntu-latest, windows-latest]
+ env:
+ UV_PYTHON: ${{ matrix.python-version }}
+ permissions:
+ contents: write
+ defaults:
+ run:
+ working-directory: python
+ steps:
+ - uses: actions/checkout@v6
+ - name: Set up python and install the project
+ id: python-setup
+ uses: ./.github/actions/python-setup
+ with:
+ python-version: ${{ matrix.python-version }}
+ os: ${{ runner.os }}
+ env:
+ # Configure a constant location for the uv cache
+ UV_CACHE_DIR: /tmp/.uv-cache
+ # Unit tests
+ - name: Run all tests
+ run: uv run poe all-tests
+ working-directory: ./python
+
+ # Surface failing tests
+ - name: Surface failing tests
+ if: always()
+ uses: pmeier/pytest-results-action@v0.7.2
+ with:
+ path: ./python/**.xml
+ summary: true
+ display-options: fEX
+ fail-on-empty: false
+ title: Test results
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..f9ba8cf
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,9 @@
+# Microsoft Open Source Code of Conduct
+
+This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
+
+Resources:
+
+- [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/)
+- [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/)
+- Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns
diff --git a/COMMUNITY.md b/COMMUNITY.md
new file mode 100644
index 0000000..bebf35b
--- /dev/null
+++ b/COMMUNITY.md
@@ -0,0 +1,22 @@
+# Welcome to the Agent Framework Community
+
+Below are some ways that you can get involved in the Agent Framework Community.
+
+## Engage on GitHub
+
+- [Discussions](https://github.com/microsoft/agent-framework/discussions): Ask questions, provide feedback and ideas to what you'd like to see from the Agent Framework.
+- [Issues](https://github.com/microsoft/agent-framework/issues) - If you find a bug, unexpected behavior or have a feature request, please open an issue.
+- [Pull Requests](https://github.com/microsoft/agent-framework/pulls) - We welcome contributions! Please see our [Contributing Guide](https://github.com/microsoft/agent-framework/blob/main/CONTRIBUTING.md)
+
+We do our best to respond to each submission.
+
+## Public Community Office Hours
+
+We regularly have Community Office Hours that are open to the **public** to join.
+
+Add Agent Framework events to your calendar. We are running two community calls to accommodate different time zones for Q&A Office Hours:
+
+- **Americas & EMEA timezone:** Every Wednesday at 8:00 AM Pacific Time/17:00 CET. Adjusted for daylight savings. Join here: [AF-AG-SK-Americas-Europe-OfficeHours](https://aka.ms/sk-officehours).
+- **Asia Pacific timezone:** The second Wednesday of every month at 4:00 PM Pacific Time Wednesday. In much of Asia this occurs on Thursday local time. Adjusted for daylight savings. Join here: [AF-AG-SK-APAC-OfficeHours](https://aka.ms/sk-apac-officehours).
+
+If you are unable to make it live, all meetings will be recorded and posted online.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..3c0e6dc
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,119 @@
+# Contributing to Agent Framework
+
+You can contribute to Agent Framework with issues and pull requests (PRs). Simply
+filing issues for problems you encounter is a great way to contribute. Contributing
+code is greatly appreciated.
+
+## Reporting Issues
+
+We always welcome bug reports, API proposals and overall feedback. Here are a few
+tips on how you can make reporting your issue as effective as possible.
+
+### Where to Report
+
+New issues can be reported in our [list of issues](https://github.com/microsoft/agent-framework/issues).
+
+Before filing a new issue, please search the list of issues to make sure it does
+not already exist.
+
+If you do find an existing issue for what you wanted to report, please include
+your own feedback in the discussion. Do consider upvoting (👍 reaction) the original
+post, as this helps us prioritize popular issues in our backlog.
+
+### Writing a Good Bug Report
+
+Good bug reports make it easier for maintainers to verify and root cause the
+underlying problem.
+The better a bug report, the faster the problem will be resolved. Ideally, a bug
+report should contain the following information:
+
+- A high-level description of the problem.
+- A _minimal reproduction_, i.e. the smallest size of code/configuration required
+ to reproduce the wrong behavior.
+- A description of the _expected behavior_, contrasted with the _actual behavior_ observed.
+- Information on the environment: OS/distribution, CPU architecture, SDK version, etc.
+- Additional information, e.g. Is it a regression from previous versions? Are there
+ any known workarounds?
+
+## Contributing Changes
+
+Project maintainers will merge accepted code changes from contributors.
+
+### DOs and DON'Ts
+
+DO's:
+
+- **DO** follow the standard coding conventions
+
+ - [.NET](https://learn.microsoft.com/dotnet/csharp/fundamentals/coding-style/coding-conventions)
+ - [Python](https://pypi.org/project/black/)
+
+- **DO** give priority to the current style of the project or file you're changing
+ if it diverges from the general guidelines.
+- **DO** use the pre-commit hooks for python to ensure proper formatting.
+- **DO** include tests when adding new features. When fixing bugs, start with
+ adding a test that highlights how the current behavior is broken.
+- **DO** keep the discussions focused. When a new or related topic comes up
+ it's often better to create new issue than to side track the discussion.
+- **DO** clearly state on an issue that you are going to take on implementing it.
+- **DO** blog and tweet (or whatever) about your contributions, frequently!
+
+DON'Ts:
+
+- **DON'T** surprise us with big pull requests. Instead, file an issue and start
+ a discussion so we can agree on a direction before you invest a large amount of time.
+- **DON'T** commit code that you didn't write. If you find code that you think is a good
+ fit to add to Agent Framework, file an issue and start a discussion before proceeding.
+- **DON'T** submit PRs that alter licensing related files or headers. If you believe
+ there's a problem with them, file an issue and we'll be happy to discuss it.
+- **DON'T** make new APIs without filing an issue and discussing with us first.
+
+### Breaking Changes
+
+Contributions must maintain API signature and behavioral compatibility. Contributions
+that include breaking changes will be rejected. Please file an issue to discuss
+your idea or change if you believe that a breaking change is warranted.
+
+### Suggested Workflow
+
+We use and recommend the following workflow:
+
+1. Create an issue for your work.
+ - You can skip this step for trivial changes.
+ - Reuse an existing issue on the topic, if there is one.
+ - Get agreement from the team and the community that your proposed change is
+ a good one.
+ - Clearly state that you are going to take on implementing it, if that's the case.
+ You can request that the issue be assigned to you. Note: The issue filer and
+ the implementer don't have to be the same person.
+2. Create a personal fork of the repository on GitHub (if you don't already have one).
+3. In your fork, create a branch off of main (`git checkout -b mybranch`).
+ - Name the branch so that it clearly communicates your intentions, such as
+ "issue-123" or "githubhandle-issue".
+4. Make and commit your changes to your branch.
+5. Add new tests corresponding to your change, if applicable.
+6. Run the relevant scripts in [the section below](#development-scripts) to ensure that your build is clean and all tests are passing.
+7. Create a PR against the repository's **main** branch.
+ - State in the description what issue or improvement your change is addressing.
+ - Verify that all the Continuous Integration checks are passing.
+8. Wait for feedback or approval of your changes from the code maintainers.
+9. When area owners have signed off, and all checks are green, your PR will be merged.
+
+### Development scripts
+
+The scripts below are used to build, test, and lint within the project.
+
+- Python: see [python/DEV_SETUP.md](./python/DEV_SETUP.md).
+- .NET:
+ - Build: `dotnet build`
+ - Test: `dotnet test`
+ - Linting (auto-fix): `dotnet format`
+
+### PR - CI Process
+
+The continuous integration (CI) system will automatically perform the required
+builds and run tests (including the ones you are expected to run) for PRs. Builds
+and test runs must be clean.
+
+If the CI build fails for any reason, the PR issue will be updated with a link
+that can be used to determine the cause of the failure.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..9e841e7
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+ MIT License
+
+ Copyright (c) Microsoft Corporation.
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..b3c89ef
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,41 @@
+
+
+## Security
+
+Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet) and [Xamarin](https://github.com/xamarin).
+
+If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/security.md/definition), please report it to us as described below.
+
+## Reporting Security Issues
+
+**Please do not report security vulnerabilities through public GitHub issues.**
+
+Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/security.md/msrc/create-report).
+
+If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/security.md/msrc/pgp).
+
+You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc).
+
+Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:
+
+ * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
+ * Full paths of source file(s) related to the manifestation of the issue
+ * The location of the affected source code (tag/branch/commit or direct URL)
+ * Any special configuration required to reproduce the issue
+ * Step-by-step instructions to reproduce the issue
+ * Proof-of-concept or exploit code (if possible)
+ * Impact of the issue, including how an attacker might exploit the issue
+
+This information will help us triage your report more quickly.
+
+If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/security.md/msrc/bounty) page for more details about our active programs.
+
+## Preferred Languages
+
+We prefer all communications to be in English.
+
+## Policy
+
+Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/security.md/cvd).
+
+
diff --git a/SUPPORT.md b/SUPPORT.md
new file mode 100644
index 0000000..1ed5d44
--- /dev/null
+++ b/SUPPORT.md
@@ -0,0 +1,17 @@
+# Support
+
+## How to file issues and get help
+
+This project uses GitHub Issues to track bugs and feature requests. Please search the existing
+issues before filing new issues to avoid duplicates. For new issues, file your bug or
+feature request as a new Issue.
+
+For help and questions about using this project, please create a GitHub issue.
+
+AI Support team will support Microsoft Agent Framework issues for customers under a **Unified support agreement when the issue arises from usage of Azure AI services** (Foundry Models, Foundry Agents etc.) in conjunction with the SDK. Conversely, if customer has any other / non unified support agreement and/or Agent Framework SDK is used in a way **not involving an Azure service**, it is treated as a purely open-source tool – Microsoft’s support organization will not handle it, and users should use GitHub or forums for assistance
+
+For Copilot Studio SDK implementation issues, customers should use GitHub Issues for assistance, as outlined above. Conversely, for prerequisites managed within the Copilot Studio portal, customers can rely on the standard Microsoft Copilot Studio support channels.
+
+## Microsoft Support Policy
+
+Support for this **PROJECT or PRODUCT** is limited to the resources listed above.
diff --git a/TRANSPARENCY_FAQ.md b/TRANSPARENCY_FAQ.md
new file mode 100644
index 0000000..3a09f19
--- /dev/null
+++ b/TRANSPARENCY_FAQ.md
@@ -0,0 +1,141 @@
+# Responsible AI Transparency FAQs
+
+**What is Microsoft Agent Framework?**
+
+Microsoft Agent Framework is a comprehensive multi-language (C#/.NET and Python) framework for building, orchestrating, and deploying AI agents and multi-agent workflows. The system takes user instructions and conversation inputs and produces intelligent responses through AI agents that can integrate with various LLM providers (OpenAI, Azure OpenAI, Azure AI Foundry). It provides both simple chat agents and complex multi-agent workflows with graph-based orchestration.
+
+**What can Microsoft Agent Framework do?**
+
+The framework offers:
+
+- **Agent Creation**: Build AI agents with custom instructions and tools
+- **Multi-Agent Orchestration**: Group chat, sequential, concurrent, and handoff patterns
+- **Graph-based Workflows**: Connect agents and deterministic functions using data flows with streaming, checkpointing, time-travel, and Human-in-the-loop
+- **Extensibility Framework**: Extend with native functions, A2A, Model Context Protocol (MCP)
+- **LLM Integration**: Support for OpenAI, Azure OpenAI, Azure AI Foundry, and other providers
+- **Runtime Support**: Both in-process and distributed agent execution
+
+**What is/are Microsoft Agent Framework's intended use(s)?**
+
+Intended uses include:
+
+- **Enterprise AI Applications**: Building AI-powered business applications with multiple specialized agents
+- **Multi-Agent Collaboration**: Coordinating multiple AI agents for complex tasks (e.g., content creation with writer/reviewer agents)
+- **Workflow Automation**: Orchestrating AI agents and deterministic functions in business processes
+
+**How was Microsoft Agent Framework evaluated? What metrics are used to measure performance?**
+
+Microsoft Agent Framework is a development framework rather than a deployed AI system. The framework undergoes engineering testing for component functionality, integration testing for multi-agent scenarios, and conformance testing across .NET and Python implementations. However, AI performance metrics such as accuracy, helpfulness, and safety are dependent on the underlying LLM providers and specific application implementations. Developers using the framework should conduct application-specific evaluation including performance, safety, and accuracy testing appropriate to their chosen LLM providers, deployment contexts, and use cases.
+
+**What are the limitations of Microsoft Agent Framework? How can users minimize the impact of Microsoft Agent Framework's limitations when using the system?**
+
+Microsoft Agent Framework relies on existing LLMs. Using the framework retains common limitations of large language models, including:
+
+**LLM-Inherited Limitations**:
+
+- **Data Biases**: Large language models, trained on extensive data, can inadvertently carry biases present in the source data. Consequently, the models may generate outputs that could be potentially biased or unfair.
+- **Lack of Contextual Understanding**: Despite their impressive capabilities in language understanding and generation, these models exhibit limited real-world understanding, resulting in potential inaccuracies or nonsensical responses.
+- **Lack of Transparency**: Due to the complexity and size, large language models can act as 'black boxes,' making it difficult to comprehend the rationale behind specific outputs or decisions.
+- **Content Harms**: There are various types of content harms that large language models can cause. It is important to be aware of them when using these models, and to take actions to prevent them. It is recommended to leverage various content moderation services provided by different companies and institutions.
+- **Inaccurate or ungrounded content**: It is important to be aware and cautious not to entirely rely on a given language model for critical decisions or information that might have deep impact as it is not obvious how to prevent these models to fabricate content without high authority input sources.
+- **Potential for Misuse**: Without suitable safeguards, there is a risk that these models could be maliciously used for generating disinformation or harmful content.
+
+**Framework-Specific Limitations**:
+
+- **Platform Requirements**: Python 3.10+ required, specific .NET versions (.NET 8.0, 9.0, 10.0, netstandard2.0, net472)
+- **API Dependencies**: Requires proper configuration of LLM provider keys and endpoints
+- **Orchestration Features**: Advanced orchestration patterns including GroupChat, Sequential, and Concurrent workflows are now available in both Python and .NET implementations. See the respective language documentation for examples.
+- **Privacy and Data Protection**: The framework allows for human participation in conversations between agents. It is important to ensure that user data and conversations are protected and that developers use appropriate measures to safeguard privacy.
+- **Accountability and Transparency**: The framework involves multiple agents conversing and collaborating, it is important to establish clear accountability and transparency mechanisms. Users should be able to understand and trace the decision-making process of the agents involved in order to ensure accountability and address any potential issues or biases.
+- **Security & unintended consequences**: The use of multi-agent conversations and automation in complex tasks may have unintended consequences. Especially, allowing agents to make changes in external environments through tool calls or function execution could pose significant risks. Developers should carefully consider the potential risks and ensure that appropriate safeguards are in place to prevent harm or negative outcomes, including keeping a human in the loop for decision making.
+
+**Mitigation Steps**:
+
+- Follow setup guides for proper API key configuration
+- Use provided samples as starting points to avoid configuration issues
+- Monitor the GitHub repository for feature releases and updates
+- Implement content moderation and safety measures when deploying agents
+- Maintain human oversight for critical decisions and actions
+- Use appropriate security measures to protect user data and conversations
+
+**What operational factors and settings allow for effective and responsible use of Microsoft Agent Framework?**
+
+**Configuration Requirements**:
+
+- **API Keys**: Proper configuration of your LLM provider credentials and endpoints
+
+- **Model Selection**: Choose appropriate deployment models for specific use cases
+
+- **Tool Integration**: Careful selection and validation of external tools and MCP servers
+
+- **Type Safety**: Strong typing and compatibility validation between agents and threads
+
+
+
+**Responsible Development Practices**:
+
+- **Human Oversight**: Microsoft Agent Framework prioritizes human involvement in multi-agent conversations. Users should maintain oversight and can step in to provide feedback to agents and steer them in the correct direction. In critical applications, users should confirm actions before they are executed.
+
+- **Agent Modularity**: Modularity allows agents to have different levels of information access. Additional agents can assume roles that help keep other agents in check. For example, one can easily add a dedicated agent to play the role of safeguard.
+
+- **LLM Selection**: Users can choose the LLM that is optimized for responsible use. We encourage developers to review and follow LLM providers’ policies. Developers should add content moderation and/or use safety metaprompts when using agents, like they would do when using LLMs directly.
+
+- **Security Measures**: Implement appropriate security measures for tool execution and external system integrations. Consider using containerization or sandboxing for code execution scenarios to prevent unintended system changes.
+
+- **Testing and Validation**: Use provided testing frameworks (unit, integration, conformance tests) to validate agent behavior and ensure reliability.
+
+- **Monitoring and Observability**: Implement proper error handling, logging, and use OpenTelemetry for observability to track agent behavior and identify potential issues.
+
+
+
+**How do I provide feedback on Microsoft Agent Framework?**
+
+- **Bug Reports**: File issues at https://github.com/microsoft/agent-framework/issues
+
+**What are external services and how does Microsoft Agent Framework use them?**
+
+The framework supports multiple external service types:
+
+- **Native Functions**: Custom Python/C# functions that agents can invoke
+- **A2A (Agent2Agent)Integration**: Agent-to-agent communication and coordination
+- **Model Context Protocol (MCP)**: External tools and data sources through MCP servers
+- **Tools & External Capabilities**: Agent-invokable external services
+
+External service development is open to developers who can create custom functions and integrate external APIs. Users have control over which tools are provided to agents during agent creation.
+
+**What data can Microsoft Agent Framework provide to external services? What permissions do Microsoft Agent Framework external services have?**
+
+Microsoft Agent Framework is an open-source framework that allows integration with various types of external services. The data access and permissions depend on how you configure and implement these integrations:
+
+**Data Access by Service Type**:
+
+- **Native Functions**: Custom functions you develop have access to whatever data you explicitly pass to them as parameters
+- **A2A (Agent2Agent)**: External agents can access conversation history, messages, and any data you configure to share through the communication interface
+- **Model Context Protocol (MCP) Servers**: External MCP servers can access data according to the specific MCP server implementation and your configuration
+- **External Tools**: Third-party tools and APIs have access to data you explicitly send to them through function calls
+
+**Important Security Considerations**:
+
+- **Community and Third-Party Services**: Microsoft Agent Framework is an open-source project. When using community-developed tools or services from third-party providers, it is your responsibility to evaluate and ensure their safety, security, and compliance with your data protection requirements.
+- **Data Boundary Considerations**: When connecting Azure-hosted agents to external agents or services, data may leave the Azure boundary and Microsoft's security perimeter. You should verify the data handling practices, security measures, and compliance certifications of external providers before sharing sensitive or regulated data.
+- **Provider Due Diligence**: Before integrating any external service, you should review their privacy policies, security practices, data retention policies, and terms of service to ensure they meet your organization's requirements and regulatory obligations.
+- **Data Minimization**: Only provide external services with the minimum data necessary for their function. Avoid sharing sensitive, personal, or confidential information unless absolutely required and properly secured.
+
+**Recommendation**: Consult with your organization's security, privacy, and legal teams before integrating external services, especially in production environments handling sensitive data.
+
+**What kinds of issues may arise when using Microsoft Agent Framework enabled with external services?**
+
+**Potential Issues**:
+
+- **API Key Security**: Risk of exposing API keys in configuration or logs
+- **Tool Reliability**: External tool failures or unavailability affecting agent performance
+- **Type Safety**: Mismatched message types between agents and handlers
+- **Provider Dependencies**: Reliance on external LLM provider availability and rate limits
+
+**Mitigation Mechanisms**:
+
+- Follow security best practices for API key management
+- Implement proper error handling for tool failures
+- Use strong typing and compatibility validation
+- Monitor external service health and implement fallback strategies
+- Regular repository updates during preview period for bug fixes
diff --git a/agent-samples/README.md b/agent-samples/README.md
new file mode 100644
index 0000000..ea5c8b0
--- /dev/null
+++ b/agent-samples/README.md
@@ -0,0 +1,3 @@
+# Declarative Agents
+
+This folder contains sample agent definitions that can be run using the declarative agent support, for python see the [declarative agent python sample folder](../python/samples/getting_started/declarative/).
diff --git a/agent-samples/azure/AzureOpenAI.yaml b/agent-samples/azure/AzureOpenAI.yaml
new file mode 100644
index 0000000..2f43d9a
--- /dev/null
+++ b/agent-samples/azure/AzureOpenAI.yaml
@@ -0,0 +1,25 @@
+kind: Prompt
+name: Assistant
+description: Helpful assistant
+instructions: You are a helpful assistant. You answer questions is the language specified by the user. You return your answers in a JSON format. You must include Chat as the type in your response.
+model:
+ id: =Env.AZURE_OPENAI_DEPLOYMENT_NAME
+ provider: AzureOpenAI
+ apiType: Chat
+ options:
+ temperature: 0.9
+ topP: 0.95
+outputSchema:
+ properties:
+ language:
+ kind: string
+ required: true
+ description: The language of the answer.
+ answer:
+ kind: string
+ required: true
+ description: The answer text.
+ type:
+ kind: string
+ required: true
+ description: The type of the response.
diff --git a/agent-samples/azure/AzureOpenAIAssistants.yaml b/agent-samples/azure/AzureOpenAIAssistants.yaml
new file mode 100644
index 0000000..f973d05
--- /dev/null
+++ b/agent-samples/azure/AzureOpenAIAssistants.yaml
@@ -0,0 +1,25 @@
+kind: Prompt
+name: Assistant
+description: Helpful assistant
+instructions: You are a helpful assistant. You answer questions in the language specified by the user. You return your answers in a JSON format. You must include Assistants as the type in your response.
+model:
+ id: gpt-4o-mini
+ provider: AzureOpenAI
+ apiType: Assistants
+ options:
+ temperature: 0.9
+ topP: 0.95
+outputSchema:
+ properties:
+ language:
+ type: string
+ required: true
+ description: The language of the answer.
+ answer:
+ type: string
+ required: true
+ description: The answer text.
+ type:
+ type: string
+ required: true
+ description: The type of the response.
diff --git a/agent-samples/azure/AzureOpenAIChat.yaml b/agent-samples/azure/AzureOpenAIChat.yaml
new file mode 100644
index 0000000..d02e0c6
--- /dev/null
+++ b/agent-samples/azure/AzureOpenAIChat.yaml
@@ -0,0 +1,25 @@
+kind: Prompt
+name: Assistant
+description: Helpful assistant
+instructions: You are a helpful assistant. You answer questions in the language specified by the user. You return your answers in a JSON format. You must include Chat as the type in your response.
+model:
+ id: gpt-4o-mini
+ provider: AzureOpenAI
+ apiType: Chat
+ options:
+ temperature: 0.9
+ topP: 0.95
+outputSchema:
+ properties:
+ language:
+ type: string
+ required: true
+ description: The language of the answer.
+ answer:
+ type: string
+ required: true
+ description: The answer text.
+ type:
+ type: string
+ required: true
+ description: The type of the response.
diff --git a/agent-samples/azure/AzureOpenAIResponses.yaml b/agent-samples/azure/AzureOpenAIResponses.yaml
new file mode 100644
index 0000000..006c147
--- /dev/null
+++ b/agent-samples/azure/AzureOpenAIResponses.yaml
@@ -0,0 +1,25 @@
+kind: Prompt
+name: Assistant
+description: Helpful assistant
+instructions: You are a helpful assistant. You answer questions in the language specified by the user. You return your answers in a JSON format. You must include Responses as the type in your response.
+model:
+ id: gpt-4o-mini
+ provider: AzureOpenAI
+ apiType: Responses
+ options:
+ temperature: 0.9
+ topP: 0.95
+outputSchema:
+ properties:
+ language:
+ type: string
+ required: true
+ description: The language of the answer.
+ answer:
+ type: string
+ required: true
+ description: The answer text.
+ type:
+ type: string
+ required: true
+ description: The type of the response.
diff --git a/agent-samples/chatclient/Assistant.yaml b/agent-samples/chatclient/Assistant.yaml
new file mode 100644
index 0000000..3332d54
--- /dev/null
+++ b/agent-samples/chatclient/Assistant.yaml
@@ -0,0 +1,18 @@
+kind: Prompt
+name: Assistant
+description: Helpful assistant
+instructions: You are a helpful assistant. You answer questions in the language specified by the user. You return your answers in a JSON format.
+model:
+ options:
+ temperature: 0.9
+ topP: 0.95
+outputSchema:
+ properties:
+ language:
+ type: string
+ required: true
+ description: The language of the answer.
+ answer:
+ type: string
+ required: true
+ description: The answer text.
diff --git a/agent-samples/chatclient/GetWeather.yaml b/agent-samples/chatclient/GetWeather.yaml
new file mode 100644
index 0000000..f32411b
--- /dev/null
+++ b/agent-samples/chatclient/GetWeather.yaml
@@ -0,0 +1,29 @@
+kind: Prompt
+name: Assistant
+description: Helpful assistant
+instructions: You are a helpful assistant. You answer questions using the tools provided.
+model:
+ options:
+ temperature: 0.9
+ topP: 0.95
+ allowMultipleToolCalls: true
+ chatToolMode: auto
+tools:
+ - kind: function
+ name: GetWeather
+ description: Get the weather for a given location.
+ bindings:
+ get_weather: get_weather
+ parameters:
+ properties:
+ location:
+ kind: string
+ description: The city and state, e.g. San Francisco, CA
+ required: true
+ unit:
+ kind: string
+ description: The unit of temperature. Possible values are 'celsius' and 'fahrenheit'.
+ required: false
+ enum:
+ - celsius
+ - fahrenheit
diff --git a/agent-samples/foundry/FoundryAgent.yaml b/agent-samples/foundry/FoundryAgent.yaml
new file mode 100644
index 0000000..2de2ea0
--- /dev/null
+++ b/agent-samples/foundry/FoundryAgent.yaml
@@ -0,0 +1,22 @@
+kind: Prompt
+name: Assistant
+description: Helpful assistant
+instructions: You are a helpful assistant. You answer questions in the language specified by the user. You return your answers in a JSON format.
+model:
+ id: gpt-4.1-mini
+ options:
+ temperature: 0.9
+ topP: 0.95
+ connection:
+ kind: Remote
+ endpoint: =Env.AZURE_FOUNDRY_PROJECT_ENDPOINT
+outputSchema:
+ properties:
+ language:
+ type: string
+ required: true
+ description: The language of the answer.
+ answer:
+ type: string
+ required: true
+ description: The answer text.
diff --git a/agent-samples/foundry/MicrosoftLearnAgent.yaml b/agent-samples/foundry/MicrosoftLearnAgent.yaml
new file mode 100644
index 0000000..8e15340
--- /dev/null
+++ b/agent-samples/foundry/MicrosoftLearnAgent.yaml
@@ -0,0 +1,21 @@
+kind: Prompt
+name: MicrosoftLearnAgent
+description: Microsoft Learn Agent
+instructions: You answer questions by searching the Microsoft Learn content only.
+model:
+ id: =Env.AZURE_FOUNDRY_PROJECT_MODEL_ID
+ options:
+ temperature: 0.9
+ topP: 0.95
+ connection:
+ kind: remote
+ endpoint: =Env.AZURE_FOUNDRY_PROJECT_ENDPOINT
+tools:
+ - kind: mcp
+ name: microsoft_learn
+ description: Get information from Microsoft Learn.
+ url: https://learn.microsoft.com/api/mcp
+ approvalMode:
+ kind: never
+ allowedTools:
+ - microsoft_docs_search
diff --git a/agent-samples/foundry/PersistentAgent.yaml b/agent-samples/foundry/PersistentAgent.yaml
new file mode 100644
index 0000000..298ded2
--- /dev/null
+++ b/agent-samples/foundry/PersistentAgent.yaml
@@ -0,0 +1,22 @@
+kind: Prompt
+name: Assistant
+description: Helpful assistant
+instructions: You are a helpful assistant. You answer questions is the language specified by the user. You return your answers in a JSON format.
+model:
+ id: =Env.AZURE_FOUNDRY_PROJECT_MODEL_ID
+ options:
+ temperature: 0.9
+ topP: 0.95
+ connection:
+ kind: remote
+ endpoint: =Env.AZURE_FOUNDRY_PROJECT_ENDPOINT
+outputSchema:
+ properties:
+ language:
+ kind: string
+ required: true
+ description: The language of the answer.
+ answer:
+ kind: string
+ required: true
+ description: The answer text.
diff --git a/agent-samples/openai/OpenAI.yaml b/agent-samples/openai/OpenAI.yaml
new file mode 100644
index 0000000..0e70188
--- /dev/null
+++ b/agent-samples/openai/OpenAI.yaml
@@ -0,0 +1,28 @@
+kind: Prompt
+name: Assistant
+description: Helpful assistant
+instructions: You are a helpful assistant. You answer questions is the language specified by the user. You return your answers in a JSON format. You must include Chat as the type in your response.
+model:
+ id: =Env.OPENAI_MODEL
+ provider: OpenAI
+ apiType: Chat
+ options:
+ temperature: 0.9
+ topP: 0.95
+ connection:
+ kind: key
+ key: =Env.OPENAI_API_KEY
+outputSchema:
+ properties:
+ language:
+ kind: string
+ required: true
+ description: The language of the answer.
+ answer:
+ kind: string
+ required: true
+ description: The answer text.
+ type:
+ kind: string
+ required: true
+ description: The type of the response.
diff --git a/agent-samples/openai/OpenAIAssistants.yaml b/agent-samples/openai/OpenAIAssistants.yaml
new file mode 100644
index 0000000..1318051
--- /dev/null
+++ b/agent-samples/openai/OpenAIAssistants.yaml
@@ -0,0 +1,28 @@
+kind: Prompt
+name: Assistant
+description: Helpful assistant
+instructions: You are a helpful assistant. You answer questions in the language specified by the user. You return your answers in a JSON format. You must include Assistants as the type in your response.
+model:
+ id: gpt-4.1-mini
+ provider: OpenAI
+ apiType: Assistants
+ options:
+ temperature: 0.9
+ topP: 0.95
+ connection:
+ kind: ApiKey
+ key: =Env.OPENAI_API_KEY
+outputSchema:
+ properties:
+ language:
+ type: string
+ required: true
+ description: The language of the answer.
+ answer:
+ type: string
+ required: true
+ description: The answer text.
+ type:
+ type: string
+ required: true
+ description: The type of the response.
diff --git a/agent-samples/openai/OpenAIChat.yaml b/agent-samples/openai/OpenAIChat.yaml
new file mode 100644
index 0000000..78286ae
--- /dev/null
+++ b/agent-samples/openai/OpenAIChat.yaml
@@ -0,0 +1,28 @@
+kind: Prompt
+name: Assistant
+description: Helpful assistant
+instructions: You are a helpful assistant. You answer questions in the language specified by the user. You return your answers in a JSON format. You must include Chat as the type in your response.
+model:
+ id: gpt-4.1-mini
+ provider: OpenAI
+ apiType: Chat
+ options:
+ temperature: 0.9
+ topP: 0.95
+ connection:
+ kind: ApiKey
+ key: =Env.OPENAI_API_KEY
+outputSchema:
+ properties:
+ language:
+ type: string
+ required: true
+ description: The language of the answer.
+ answer:
+ type: string
+ required: true
+ description: The answer text.
+ type:
+ type: string
+ required: true
+ description: The type of the response.
diff --git a/agent-samples/openai/OpenAIResponses.yaml b/agent-samples/openai/OpenAIResponses.yaml
new file mode 100644
index 0000000..bdc04d4
--- /dev/null
+++ b/agent-samples/openai/OpenAIResponses.yaml
@@ -0,0 +1,28 @@
+kind: Prompt
+name: Assistant
+description: Helpful assistant
+instructions: You are a helpful assistant. You answer questions in the language specified by the user. You return your answers in a JSON format. You must include Responses as the type in your response.
+model:
+ id: gpt-4.1-mini
+ provider: OpenAI
+ apiType: Responses
+ options:
+ temperature: 0.9
+ topP: 0.95
+ connection:
+ kind: key
+ apiKey: =Env.OPENAI_APIKEY
+outputSchema:
+ properties:
+ language:
+ kind: string
+ required: true
+ description: The language of the answer.
+ answer:
+ kind: string
+ required: true
+ description: The answer text.
+ type:
+ kind: string
+ required: true
+ description: The type of the response.
diff --git a/docs/FAQS.md b/docs/FAQS.md
new file mode 100644
index 0000000..3ecd551
--- /dev/null
+++ b/docs/FAQS.md
@@ -0,0 +1,54 @@
+# Frequently Asked Questions
+
+### How do I get access to nightly builds?
+
+Nightly builds of the Agent Framework are available [here](https://github.com/orgs/microsoft/packages?repo_name=agent-framework).
+
+To download nightly builds follow the following steps:
+
+1. You will need a GitHub account to complete these steps.
+1. Create a GitHub Personal Access Token with the `read:packages` scope using these [instructions](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-personal-access-token-classic).
+1. If your account is part of the Microsoft organization then you must authorize the `Microsoft` organization as a single sign-on organization.
+ 1. Click the "Configure SSO" next to the Personal Access Token you just created and then authorize `Microsoft`.
+1. Use the following command to add the Microsoft GitHub Packages source to your NuGet configuration:
+
+ ```powershell
+ dotnet nuget add source --username GITHUBUSERNAME --password GITHUBPERSONALACCESSTOKEN --store-password-in-clear-text --name GitHubMicrosoft "https://nuget.pkg.github.com/microsoft/index.json"
+ ```
+
+1. Or you can manually create a `NuGet.Config` file.
+
+ ```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ```
+
+ * If you place this file in your project folder make sure to have Git (or whatever source control you use) ignore it.
+ * For more information on where to store this file go [here](https://learn.microsoft.com/en-us/nuget/reference/nuget-config-file).
+1. You can now add packages from the nightly build to your project.
+ * E.g. use this command `dotnet add package Microsoft.Agents.AI --version 0.0.1-nightly-250731.6-alpha`
+1. And the latest package release can be referenced in the project like this:
+ * ` `
+
+For more information see:
diff --git a/docs/assets/Agentic-framework_high-res.png b/docs/assets/Agentic-framework_high-res.png
new file mode 100644
index 0000000..cdb53b1
Binary files /dev/null and b/docs/assets/Agentic-framework_high-res.png differ
diff --git a/docs/assets/readme-banner.png b/docs/assets/readme-banner.png
new file mode 100644
index 0000000..defc110
Binary files /dev/null and b/docs/assets/readme-banner.png differ
diff --git a/docs/decisions/0001-agent-run-response.md b/docs/decisions/0001-agent-run-response.md
new file mode 100644
index 0000000..6f3385e
--- /dev/null
+++ b/docs/decisions/0001-agent-run-response.md
@@ -0,0 +1,515 @@
+---
+# These are optional elements. Feel free to remove any of them.
+status: accepted
+contact: westey-m
+date: 2025-07-10 {YYYY-MM-DD when the decision was last updated}
+deciders: sergeymenshykh, markwallace, rbarreto, dmytrostruk, westey-m, eavanvalkenburg, stephentoub
+consulted:
+informed:
+---
+
+# Agent Run Responses Design
+
+## Context and Problem Statement
+
+Agents may produce lots of output during a run including
+
+1. **[Primary]** General response messages to the caller (this may be in the form of text, including structured output, images, sound, etc.)
+2. **[Primary]** Structured confirmation requests to the caller
+3. **[Secondary]** Tool invocation activities executed (both local and remote). For information only.
+4. Reasoning/Thinking output.
+ 1. **[Primary]** In some cases an LLM may return reasoning output intermixed with as part of the answer to the caller, since the caller's prompt asked for this detail in some way. This should be considered a specialization of 1.
+ 1. **[Secondary]** Reasonining models optionally produce reasoning output separate from the answer to the caller's question, and this should be considered secondary content.
+5. **[Secondary]** Handoffs / transitions from agent to agent where an agent contains sub agents.
+6. **[Secondary]** An indication that the agent is responding (i.e. typing) as if it's a real human.
+7. Complete messages in addition to updates, when streaming
+8. Id for long running process that is launched
+9. and more
+
+We need to ensure that with this diverse list of output, we are able to
+
+- Support all with abstractions where needed
+- Provide a simple getting started experience that doesn't overwhelm developers
+
+### Agent response data types
+
+When comparing various agent SDKs and protocols, agent output is often divided into two categories:
+
+1. **Result**: A response from the agent that communicates the result of the agent's work to the caller in natural language (or images/sound/etc.). Let's call this **Primary** output.
+ 1. Includes cases where the agent finished because it requires more input from the user.
+2. **Progress**: Updates while the agent is running, which are informational only, typically showing what the agent is doing, and does not allow any actions to be taken by the caller that modify the behavior of the agent before completing the run. Let's call this **Secondary** output.
+
+A potential third category is:
+
+3. **Long Running**: A response that does not contain a Primary response or Secondary updates, but rather a reference to a long running task.
+
+### Different use cases for Primary and Secondary output
+
+To solve complex problems, many agents must be used together. These agents typically have their own capabilities and responsibilities and communicate via input messages and final responses/handoff calls, while the internal workings of each agent is not of interest to the other agents participating in solving the problem.
+
+When an agent is in conversation with one or more humans, the information that may be displayed to the user(s) can vary. E.g. When an agent is part of a conversation with multiple humans it may be asked to perform tasks by the humans, and they may not want a stream of distracting updates posted to the conversation, but rather just a final response. On the other hand, if an agent is being used by a single human to perform a task, the human may be waiting for the agent to complete the task. Therefore, they may be interested in getting updates of what the agent is doing.
+
+Where agents are nested, consumers would also likely want to constrain the amount of data from an agent that bubbles up into higher level conversations to avoid exceeding the context window, therefore limiting it to the Primary response only.
+
+### Comparison with other SDKs / Protocols
+
+Approaches observed from the compared SDKs:
+
+1. Response object with separate properties for Primary and Secondary
+2. Response stream that contains Primary and Secondary entries and callers need to filter.
+3. Response containing just Primary.
+
+| SDK | Non-Streaming | Streaming |
+|-|-|-|
+| AutoGen | **Approach 1** Separates messages into Agent-Agent (maps to Primary) and Internal (maps to Secondary) and these are returned as separate properties on the agent response object. See [types of messages](https://microsoft.github.io/autogen/stable/user-guide/agentchat-user-guide/tutorial/messages.html#types-of-messages) and [Response](https://microsoft.github.io/autogen/stable/reference/python/autogen_agentchat.base.html#autogen_agentchat.base.Response) | **Approach 2** Returns a stream of internal events and the last item is a Response object. See [ChatAgent.on_messages_stream](https://microsoft.github.io/autogen/stable/reference/python/autogen_agentchat.base.html#autogen_agentchat.base.ChatAgent.on_messages_stream) |
+| OpenAI Agent SDK | **Approach 1** Separates new_items (Primary+Secondary) from final output (Primary) as separate properties on the [RunResult](https://github.com/openai/openai-agents-python/blob/main/src/agents/result.py#L39) | **Approach 1** Similar to non-streaming, has a way of streaming updates via a method on the response object which includes all data, and then a separate final output property on the response object which is populated only when the run is complete. See [RunResultStreaming](https://github.com/openai/openai-agents-python/blob/main/src/agents/result.py#L136) |
+| Google ADK | **Approach 2** [Emits events](https://google.github.io/adk-docs/runtime/#step-by-step-breakdown) with [FinalResponse](https://github.com/google/adk-java/blob/main/core/src/main/java/com/google/adk/events/Event.java#L232) true (Primary) / false (Secondary) and callers have to filter out those with false to get just the final response message | **Approach 2** Similar to non-streaming except [events](https://google.github.io/adk-docs/runtime/#streaming-vs-non-streaming-output-partialtrue) are emitted with [Partial](https://github.com/google/adk-java/blob/main/core/src/main/java/com/google/adk/events/Event.java#L133) true to indicate that they are streaming messages. A final non partial event is also emitted. |
+| AWS (Strands) | **Approach 3** Returns an [AgentResult](https://strandsagents.com/latest/documentation/docs/api-reference/python/agent/agent_result/) (Primary) with messages and a reason for the run's completion. | **Approach 2** [Streams events](https://strandsagents.com/latest/documentation/docs/api-reference/python/agent/agent/#strands.agent.agent.Agent.stream_async) (Primary+Secondary) including, response text, current_tool_use, even data from "callbacks" (strands plugins) |
+| LangGraph | **Approach 2** A mixed list of all [messages](https://langchain-ai.github.io/langgraph/agents/run_agents/#output-format) | **Approach 2** A mixed list of all [messages](https://langchain-ai.github.io/langgraph/agents/run_agents/#output-format) |
+| Agno | **Combination of various approaches** Returns a [RunResponse](https://docs.agno.com/reference/agents/run-response) object with text content, messages (essentially chat history including inputs and instructions), reasoning and thinking text properties. Secondary events could potentially be extracted from messages. | **Approach 2** Returns [RunResponseEvent](https://docs.agno.com/reference/agents/run-response#runresponseevent-types-and-attributes) objects including tool call, memory update, etc, information, where the [RunResponseCompletedEvent](https://docs.agno.com/reference/agents/run-response#runresponsecompletedevent) has similar properties to RunResponse|
+| A2A | **Approach 3** Returns a [Task or Message](https://a2aproject.github.io/A2A/latest/specification/#71-messagesend) where the message is the final result (Primary) and task is a reference to a long running process. | **Approach 2** Returns a [stream](https://a2aproject.github.io/A2A/latest/specification/#72-messagestream) that contains task updates (Secondary) and a final message (Primary) |
+| Protocol Activity | **Approach 2** Single stream of responses including secondary events and final response messages (Primary). | No separate behavior for streaming. |
+
+## Decision Drivers
+
+- Solutions provides an easy to use experience for users who are getting started and just want the answer to a question.
+- Solution must be extensible to future requirements, e.g. long running agent processes.
+- Experience is in line or better than the best in class experience from other SDKs
+
+## Response Type Options
+
+- **Option 1** Run: Messages List contains mix of Primary and Secondary content, RunStreaming: Stream of Primary + Secondary
+ - **Option 1.1** Secondary content do not use `TextContent`
+ - **Option 1.2** Presence of Secondary Content is determined by a runtime parameter
+ - **Option 1.3** Use ChatClient response types
+ - **Option 1.4** Return derived ChatClient response types
+- **Option 2** Run: Container with Primary and Secondary Properties, RunStreaming: Stream of Primary + Secondary
+ - **Option 2.1** Response types extend MEAI types
+ - **Option 2.2** New Response types
+- **Option 3** Run: Primary-only, RunStreaming: Stream of Primary + Secondary
+- **Option 4** Remove Run API and retain RunStreaming API only, which returns a Stream of Primary + Secondary.
+
+Since the suggested options vary only for the non-streaming case, the following detailed explanations for each
+focuses on the non-streaming case.
+
+### Option 1 Run: Messages List contains mix of Primary and Secondary content, RunStreaming: Stream of Primary + Secondary
+
+Run returns a `Task` and RunStreaming returns a `IAsyncEnumerable`.
+For Run, the returned `ChatResponse.Messages` contains an ordered list of messages that contain both the Primary and Secondary content.
+
+`ChatResponse.Text` automatically aggregates all text from any `TextContent` items in all `ChatMessage` items in the response.
+If we can ensure that no updates ever contain `TextContent`, this will mean that `ChatResponse.Text` will always contain
+the Primary response text. See option 1.1.
+If we cannot ensure this, either the solution or usage becomes more complex, see 1.3 and 1.4.
+
+#### Option 1.1 `TextContent`, `DataContent` and `UriContent` means Primary content
+
+`ChatResponse.Text` aggregates all `TextContent` values, and no secondary updates use `TextContent`
+so `ChatResponse.Text` will always contain the Primary content.
+
+```csharp
+// Since the Text property contains the primary content, it's a simple getting started experience.
+var response = await agent.RunAsync("Do Something");
+Console.WriteLine(response.Text);
+
+// Callers can still get access to all updates too.
+foreach (var update in response.Messages)
+{
+ Console.WriteLine(update.Contents.FirstOrDefault()?.GetType().Name);
+}
+
+// For streaming, it's possible to output the primary content by also using the Text property on each update.
+await foreach (var update in agent.RunStreamingAsync("Do Something"))
+{
+ Console.Writeline(update.Text)
+}
+```
+
+- **PROS**: Easy and familiar user experience, reuse response types from IChatClient. Similar experience for both streaming and non streaming.
+- **CONS**: The agent response types cannot evolve separately from MEAI if needed.
+
+#### Option 1.1a `TextContent`, `DataContent` and `UriContent` means Primary content, with custom Agent response types
+
+Same as 1.1 but with custom Agent Framework response types.
+The response types should preferably resemble ChatResponse types closely, to ensure user's have a fimilar experience when moving between the two.
+Therefore something like `AgentResponse.Text` which also aggregates all `TextContent` values similar to 1.1 makes sense.
+
+- **PROS**: Easy getting started experience, and response types can be customized for the Agent Framework where needed.
+- **CONS**: More work to define custom response types.
+
+#### Option 1.2 Presence of Secondary Content is determined by a runtime parameter
+
+We can allow callers to choose whether to include secondary content in the list of reponse messages.
+Open Question: Do we allow secondary content to use `TextContent` types?
+
+```csharp
+// By default the response only has the primary content, so text
+// contains the primary content, and it's a good starting experience.
+var response = await agent.RunAsync("Do Something");
+Console.WriteLine(response.Text);
+
+// we can also optionally include updates via an option.
+var response = await agent.RunAsync("Do Something", options: new() { IncludeUpdates = true });
+// Callers can now access all updates.
+foreach (var update in response.Messages)
+{
+ Console.WriteLine(update.Contents.FirstOrDefault()?.GetType().Name);
+}
+```
+
+- **PROS**: Easy getting started experience, reuse response types from IChatClient.
+- **CONS**: Since the basic experience is the same as 1.1, and when you look at individual messages, you most likely want all anyway, it seems arbitrarily limiting compared to 1.1.
+
+### Option 2 Run: Container with Primary and Secondary Properties, RunStreaming: Stream of Primary + Secondary
+
+Run returns a new response type that has separate properties for the Primary Content and the Secondary Updates leading up to it.
+The Primary content is available in the `AgentResponse.Messages` property while Secondary updates are in a new `AgentResponse.Updates` property.
+`AgentResponse.Text` returns the Primary content text.
+
+Since streaming would still need to return an `IAsyncEnumerable` of updates, the design would differ from non-streaming.
+With non-streaming Primary and Secondary content is split into separate lists, while with streaming it's combined in one stream.
+
+```csharp
+// Since text contains the primary content, it's a good getting started experience.
+var response = await agent.RunAsync("Do Something");
+Console.WriteLine(response.Text);
+
+// Callers can still get access to all updates too.
+foreach (var update in response.Updates)
+{
+ Console.WriteLine(update.Contents.FirstOrDefault()?.GetType().Name);
+}
+```
+
+- **PROS**: Primary content and Secondary Updates are categorised for non-streaming and therefore easy to distinguish and this design matches popular SDKs like AutoGen and OpenAI SDK.
+- **CONS**: Requires custom response types and design would differ between streaming and non-streaming.
+
+### Option 3 Run: Primary-only, RunStreaming: Stream of Primary + Secondary
+
+Run returns a `Task` and RunStreaming returns a `IAsyncEnumerable`.
+For Run, the returned `ChatResponse.Messages` contains only the Primary content messages.
+`ChatResponse.Text` will contain the aggregate text of `ChatResponse.Messages` and therefore the primary content messages text.
+
+```csharp
+// Since text contains the primary content response, it's a good getting started experience.
+var response = await agent.RunAsync("Do Something");
+Console.WriteLine(response.Text);
+
+// Callers cannot get access to all updates, since only the primary content is in messages.
+var primaryContentOnly = response.Messages.FirstOrDefault();
+```
+
+- **PROS**: Simple getting started experience, Reusing IChatClient response types.
+- **CONS**: Intermediate updates are only availble in streaming mode.
+
+### Option 4: Remove Run API and retain RunStreaming API only, which returns a Stream of Primary + Secondary
+
+With this option, we remove the `RunAsync` method and only retain the `RunStreamingAsync` method, but
+we add helpers to process the streaming responses and extract information from it.
+
+```csharp
+// User can get the primary content through an extension method on the async enumerable stream.
+var responses = agent.RunStreamingAsync("Do Something");
+// E.g. an extension method that builds the primary content text.
+Console.WriteLine(await responses.AggregateFinalResult());
+// Or an extention method that builds complete messages from the updates.
+Console.WriteLine(await responses.BuildMessage().Text);
+
+// Callers can also iterate through all updates if needed
+await foreach (var update in responses)
+{
+ Console.WriteLine(update.Contents.FirstOrDefault()?.GetType().Name);
+}
+```
+
+- **PROS**: Single API for streaming/non-streaming
+- **CONS**: More complex to for inexperienced users.
+
+## Custom Response Type Design Options
+
+### Option 1 Response types extend MEAI types
+
+```csharp
+class Agent
+{
+ public abstract Task RunAsync(
+ IReadOnlyCollection messages,
+ AgentThread? thread = null,
+ AgentRunOptions? options = null,
+ CancellationToken cancellationToken = default);
+
+ public abstract IAsyncEnumerable RunStreamingAsync(
+ IReadOnlyCollection messages,
+ AgentThread? thread = null,
+ AgentRunOptions? options = null,
+ CancellationToken cancellationToken = default);
+}
+
+class AgentResponse : ChatResponse
+{
+}
+
+public class AgentResponseUpdate : ChatResponseUpdate
+{
+}
+```
+
+- **PROS**: Fimilar response types for anyone already using MEAI.
+- **CONS**: Agent response types cannot evolve separately.
+
+### Option 2 New Response types
+
+We could create new response types for Agents.
+The new types could also exclude properties that make less sense for agents, like ConversationId, which is abstracted away by AgentThread, or ModelId, where an agent might use multiple models.
+
+```csharp
+class Agent
+{
+ public abstract Task RunAsync(
+ IReadOnlyCollection messages,
+ AgentThread? thread = null,
+ AgentRunOptions? options = null,
+ CancellationToken cancellationToken = default);
+
+ public abstract IAsyncEnumerable RunStreamingAsync(
+ IReadOnlyCollection messages,
+ AgentThread? thread = null,
+ AgentRunOptions? options = null,
+ CancellationToken cancellationToken = default);
+}
+
+class AgentResponse // Compare with ChatResponse
+{
+ public string Text { get; } // Aggregation of TextContent from messages.
+
+ public IList Messages { get; set; }
+
+ public string? ResponseId { get; set; }
+
+ // Metadata
+ public string? AuthorName { get; set; }
+ public DateTimeOffset? CreatedAt { get; set; }
+ public object? RawRepresentation { get; set; }
+ public UsageDetails? Usage { get; set; }
+ public AdditionalPropertiesDictionary? AdditionalProperties { get; set; }
+}
+
+// Not Included in AgentResponse compared to ChatResponse
+public ChatFinishReason? FinishReason { get; set; }
+public string? ConversationId { get; set; }
+public string? ModelId { get; set; }
+
+public class AgentResponseUpdate // Compare with ChatResponseUpdate
+{
+ public string Text { get; } // Aggregation of TextContent from Contents.
+
+ public IList Contents { get; set; }
+
+ public string? ResponseId { get; set; }
+ public string? MessageId { get; set; }
+
+ // Metadata
+ public ChatRole? Role { get; set; }
+ public string? AuthorName { get; set; }
+ public DateTimeOffset? CreatedAt { get; set; }
+ public UsageDetails? Usage { get; set; }
+ public object? RawRepresentation { get; set; }
+ public AdditionalPropertiesDictionary? AdditionalProperties { get; set; }
+}
+
+// Not Included in AgentResponseUpdate compared to ChatResponseUpdate
+public ChatFinishReason? FinishReason { get; set; }
+public string? ConversationId { get; set; }
+public string? ModelId { get; set; }
+```
+
+- **PROS**: Agent response types can evolve separately. Types can still resemble MEAI response types to ensure a fimilar experience for developers.
+- **CONS**: No automatic inheritence of new properties from MEAI. (this might also be a pro)
+
+## Long Running Processes Options
+
+Some agent protocols, like A2A, support long running agentic processes. When invoking the agent
+in the non-streaming case, the agent may respond with an id of a process that was launched.
+
+The caller is then expected to poll the service to get status updates using the id.
+The caller may also subscribe to updates from the process using the id.
+
+We therefore need to be able to support providing this type of response to agent callers.
+
+- **Option 1** Add a new `AIContent` type and `ChatFinishReason` for long running processes.
+- **Option 2** Add another property on a custom response type.
+
+### Option 1: Add another AIContent type and ChatFinishReason for long running processes
+
+```csharp
+public class AgentRunContent : AIContent
+{
+ public string AgentRunId { get; set; }
+}
+
+// Add a new long running chat finish reason.
+public class ChatFinishReason
+{
+ public static ChatFinishReason LongRunning { get; } = new ChatFinishReason("long_running");
+}
+```
+
+- **PROS**: Fits well into existing `ChatResponse` design.
+- **CONS**: More complex for users to extract the required long running result (can be mitigated with extenion methods)
+
+### Option 2: Add another property on responses for AgentRun
+
+```csharp
+class AgentResponse
+{
+ ...
+ public AgentRun RunReference { get; set; } // Reference to long running process
+ ...
+}
+
+
+public class AgentResponseUpdate
+{
+ ...
+ public AgentRun RunReference { get; set; } // Reference to long running process
+ ...
+}
+
+// Add a new long running chat finish reason.
+public class ChatFinishReason
+{
+ ...
+ public static ChatFinishReason LongRunning { get; } = new ChatFinishReason("long_running");
+ ...
+}
+
+// Can be added in future: Class representing long running processing by the agent
+// that can be used to check for updates and status of the processing.
+public class AgentRun
+{
+ public string AgentRunId { get; set; }
+}
+```
+
+- **PROS**: Easy access to long running result values
+- **CONS**: Requires custom response types.
+
+## Structured user input options (Work in progress)
+
+Some agent services may ask end users a question while also providing a list of options that the user can pick from or a template for the input required.
+We need to decide whether to maintain an abstraction for these, so that similar types of structured input from different agents can be used by callers without
+needing to break out of the abstraction.
+
+## Tool result options (Work in progress)
+
+We need to consider abstractions for `AIContent` derived types for tool call results for common tool types beyond Function calls, e.g. CodeInterpreter, WebSearch, etc.
+
+## StructuredOutputs
+
+Structured outputs is a valueable aspect of any Agent system, since it forces an Agent to produce output in a required format, and may include required fields. This allows turning unstructured data into structured data easily using a general purpose language model.
+
+Not all agent types necessarily support this or necessarily support this in the same way.
+Requesting a specific output schema at invocation time is widely supported by inference services though, and therefore inference based agents would support this well.
+Custom agents on the other hand may not necessarily want to support this, and forcing all custom Agent implementations to have a final structured output step to produce this complicates implementations.
+Custom agents may also have a built in output schema, that they always produce.
+
+Options:
+
+1. Support configuring the preferred structured output schema at agent construction time for those agents that support structured outputs.
+2. Support configuring the preferred structured output schema at invocation time, and ignore/throw if not supported (similar to IChatClient)
+3. Support both options with the invocation time schema overriding the construction time (or built in) schema if both are supported.
+
+Note that where an agent doesn't support structured output, it may also be possible to use a decorator to produce structured output from the agent's unstructured response, thereby turning an agent that doesn't support this into one that does.
+
+See [Structured Outputs Support](#structured-outputs-support) for a comparison on what other agent frameworks and protocols support.
+
+To support a good user experience for structured outputs, I'm proposing that we follow the pattern used by MEAI.
+We would add a generic version of `AgentResponse`, that allows us to get the agent result already deserialized into our preferred type.
+This would be coupled with generic overload extension methods for Run that automatically builds a schema from the supplied type and updates
+the run options.
+
+If we support requesting a schema at invocation time the following would be the preferred approach:
+
+```csharp
+class Movie
+{
+ public string Title { get; set; }
+ public string DirectorFullName { get; set; }
+ public int ReleaseYear { get; set; }
+}
+
+AgentResponse response = agent.RunAsync("What are the top 3 children's movies of the 80s.");
+Movie[] movies = response.Result
+```
+
+If we only support requesting a schema at agent creation time or where an agent has a built in schema, the following would be the preferred approach:
+
+```csharp
+AgentResponse response = agent.RunAsync("What are the top 3 children's movies of the 80s.");
+Movie[] movies = response.TryParseStructuredOutput();
+```
+
+## Decision Outcome
+
+### Response Type Options Decision
+
+Option 1.1 with the caveate that we cannot control the output of all agents. However, as far as possible we should have appropriate AIContext derived types for
+progress updates so that TextContent is not used for these.
+
+### Custom Response Type Design Options Decision
+
+Option 2 chosen so that we can vary Agent responses independently of Chat Client.
+
+### StructuredOutputs Decision
+
+We will not support structured output per run request, but individual agents are free to allow this on the concrete implementation or at construction time.
+We will however add support for easily extracting a structured output type from the `AgentResponse`.
+
+## Addendum 1: AIContext Derived Types for different response types / Gap Analysis (Work in progress)
+
+We need to decide what AIContent types, each agent response type will be mapped to.
+
+| Number | DataType | AIContent Type |
+|-|-|-|
+| 1. | General response messages to the user | TextContent + DataContent + UriContent |
+| 2. | Structured confirmation requests to the user | ? |
+| 3. | Function invocation activities executed (both local and remote). For information only. | FunctionCallContent + FunctionResultContent |
+| 4. | Tool invocation activities executed (both local and remote). For information only. | FunctionCallContent/FunctionResultContent/Custom ? |
+| 5. | Reasoning/Thinking output. For information only. | TextReasoningContent |
+| 6. | Handoffs / transitions from agent to agent. | ? |
+| 7. | An indication that the agent is responding (i.e. typing) as if it's a real human. | ? |
+| 8. | Complete messages in addition to updates, when streaming | TextContent |
+| 9. | Id for long running process that is launched | ? |
+| 10. | Memory storage / lookups (are these just traces?) | ? |
+| 11. | RAG indexing / lookups (are these just traces?) | ? |
+| 12. | General status updates for human consumption / Tracing | ? |
+| 13. | Unknown Type | AIContent |
+
+## Addendum 2: Other SDK feature comparison
+
+### Structured Outputs Support
+
+1. Configure Schema on Agent at Agent construction
+2. Pass schema at Agent invocation
+
+| SDK | Structured Outputs support |
+|-|-|
+| AutoGen | **Approach 1** Supports [configuring an agent](https://microsoft.github.io/autogen/stable/user-guide/agentchat-user-guide/tutorial/agents.html#structured-output) at agent creation. |
+| Google ADK | **Approach 1** Both [input and output schemas can be specified for LLM Agents](https://google.github.io/adk-docs/agents/llm-agents/#structuring-data-input_schema-output_schema-output_key) at construction time. This option is specific to this agent type and other agent types do not necessarily support |
+| AWS (Strands) | **Approach 2** Supports a special invocation method called [structured_output](https://strandsagents.com/latest/documentation/docs/api-reference/python/agent/agent/#strands.agent.agent.Agent.structured_output) |
+| LangGraph | **Approach 1** Supports [configuring an agent](https://langchain-ai.github.io/langgraph/agents/agents/?h=structured#6-configure-structured-output) at agent construction time, and a [structured response](https://langchain-ai.github.io/langgraph/agents/run_agents/#output-format) can be retrieved as a special property on the agent response |
+| Agno | **Approach 1** Supports [configuring an agent](https://docs.agno.com/examples/getting-started/structured-output) at agent construction time |
+| A2A | **Informal Approach 2** Doesn't formally support schema negotiation, but [hints can be provided via metadata](https://a2a-protocol.org/latest/specification/#97-structured-data-exchange-requesting-and-providing-json) at invocation time |
+| Protocol Activity | Supports returning [Complex types](https://github.com/microsoft/Agents/blob/main/specs/activity/protocol-activity.md#complex-types) but no support for requesting a type |
+
+### Response Reason Support
+
+| SDK | Response Reason support |
+|-|-|
+| AutoGen | Supports a [stop reason](https://microsoft.github.io/autogen/stable/reference/python/autogen_agentchat.base.html#autogen_agentchat.base.TaskResult.stop_reason) which is a freeform text string |
+| Google ADK | [No equivalent present](https://github.com/google/adk-python/blob/main/src/google/adk/events/event.py) |
+| AWS (Strands) | Exposes a [stop_reason](https://strandsagents.com/latest/documentation/docs/api-reference/python/types/event_loop/#strands.types.event_loop.StopReason) property on the [AgentResult](https://strandsagents.com/latest/documentation/docs/api-reference/python/agent/agent_result/) class with options that are tied closely to LLM operations. |
+| LangGraph | No equivalent present, output contains only [messages](https://langchain-ai.github.io/langgraph/agents/run_agents/#output-format) |
+| Agno | [No equivalent present](https://docs.agno.com/reference/agents/run-response) |
+| A2A | No equivalent present, response only contains a [message](https://a2a-protocol.org/latest/specification/#64-message-object) or [task](https://a2a-protocol.org/latest/specification/#61-task-object). |
+| Protocol Activity | [No equivalent present.](https://github.com/microsoft/Agents/blob/main/specs/activity/protocol-activity.md) |
diff --git a/docs/decisions/0002-agent-tools.md b/docs/decisions/0002-agent-tools.md
new file mode 100644
index 0000000..e08ff21
--- /dev/null
+++ b/docs/decisions/0002-agent-tools.md
@@ -0,0 +1,1896 @@
+---
+# These are optional elements. Feel free to remove any of them.
+status: {proposed}
+contact: {dmytrostruk}
+date: {2025-06-23}
+deciders: {stephentoub, markwallace-microsoft, RogerBarreto, westey-m}
+consulted: {}
+informed: {}
+---
+
+# Agent Tools
+
+## Context and Problem Statement
+
+AI agents increasingly rely on diverse tools like function calling, file search, and computer use, but integrating each tool often requires custom, inconsistent implementations. A unified abstraction for tool usage is essential to simplify development, ensure consistency, and enable scalable, reliable agent performance across varied tasks.
+
+## Decision Drivers
+
+- The abstraction must provide a consistent API for all tools to reduce complexity and improve developer experience.
+- The design should allow seamless integration of new tools without significant changes to existing implementations.
+- Robust mechanisms for managing tool-specific errors and timeouts are required for reliability.
+- The abstraction should support a fallback approach to directly use unsupported or custom tools, bypassing standard abstractions when necessary.
+
+## Considered Options
+
+### Option 1: Use ChatOptions.RawRepresentationFactory for Provider-Specific Tools
+
+#### Description
+
+Utilize the existing `ChatOptions.RawRepresentationFactory` to inject provider-specific tools (e.g., for an AI provider like Foundry) without extending the `AITool` abstract class from `Microsoft.Extensions.AI`.
+
+```csharp
+ChatOptions options = new()
+{
+ RawRepresentationFactory = _ => new ResponseCreationOptions()
+ {
+ Tools = { ... }, // backend-specific tools
+ },
+};
+```
+
+#### Pros
+
+- No development work needed; leverages existing `Microsoft.Extensions.AI` functionality.
+- Flexible for integrating tools from any AI provider without modifying the `AITool`.
+- Minimal codebase changes, reducing the risk of introducing errors.
+
+#### Cons
+
+- Requires a separate mechanism to register tools, complicating the developer experience.
+- Developers must know the specific AI provider (via `IChatClient`) to configure tools, reducing abstraction.
+- Inconsistent with the `AITool` abstraction, leading to fragmented tool usage patterns.
+- Poor tool discoverability, as they are not integrated into the `AITool` ecosystem.
+
+### Option 2: Add Provider-Specific AITool-Derived Types in Provider Packages
+
+#### Description
+
+Create provider-specific tool types that inherit from the `AITool` abstract class within each AI provider’s package (e.g., a Foundry package could include Foundry-specific tools). The provider’s `IChatClient` implementation would natively recognize and process these `AITool`-derived types, eliminating the need for a separate registration mechanism.
+
+#### Pros
+
+- Integrates with the `AITool` abstract class, providing a consistent developer experience within the `Microsoft.Extensions.AI`.
+- Eliminates the need for a special registration mechanism like `RawRepresentationFactory`.
+- Enhances type safety and discoverability for provider-specific tools.
+- Aligns with the standardized interface driver by leveraging `AITool` as the base class.
+
+#### Cons
+
+- Developers must know they are targeting a specific AI provider to select the appropriate `AITool`-derived types.
+- Increases maintenance overhead for each provider’s package to support and update these tool types.
+- Leads to fragmentation, as each provider requires its own set of `AITool`-derived types.
+- Potential for duplication if multiple providers implement similar tools with different `AITool` derivatives.
+
+### Option 3: Create Generic AITool-Derived Abstractions in M.E.AI.Abstractions
+
+#### Description
+
+Develop generic tool abstractions that inherit from the `AITool` abstract class in the `M.E.AI.Abstractions` package (e.g., `HostedCodeInterpreterTool`, `HostedWebSearchTool`). These abstractions map to common tool concepts across multiple AI providers, with provider-specific implementations handled internally.
+
+#### Pros
+
+- Provides a standardized `AITool`-based interface across AI providers, improving consistency and developer experience.
+- Reduces the need for provider-specific knowledge by abstracting tool implementations.
+- Highly extensible, supporting new `AITool`-derived types for common tool concepts (e.g., server-side MCP tools).
+
+#### Cons
+
+- Complex mapping logic needed to support diverse provider implementations.
+- May not cover niche or provider-specific tools, necessitating a fallback mechanism.
+
+### Option 4: Hybrid Approach Combining Options 1, 2, and 3
+
+#### Description
+
+Implement a hybrid strategy where common tools use generic `AITool`-derived abstractions in `M.E.AI.Abstractions` (Option 3), provider-specific tools (e.g., for Foundry) are implemented as `AITool`-derived types in their respective provider packages (Option 2), and rare or unsupported tools fall back to `ChatOptions.RawRepresentationFactory` (Option 1).
+
+#### Pros
+
+- Balances developer experience and flexibility by using the best `AITool`-based approach for each tool type.
+- Supports standardized `AITool` interfaces for common tools while allowing provider-specific and breakglass mechanisms.
+- Extensible and scalable, accommodating both current and future tool requirements across AI providers.
+- Addresses ancillary and intermediate content (e.g., MCP permissions) with generic types.
+
+#### Cons
+
+- Increases complexity by managing multiple `AITool` integration approaches within the same system.
+- Requires clear documentation to guide developers on when to use each option.
+- Potential for inconsistency if boundaries between approaches are not well-defined.
+- Higher maintenance burden to support and test multiple tool integration paths.
+
+## More information
+
+### AI Agent Tool Types Availability
+
+Tool Type | Azure AI Foundry Agent Service | OpenAI Assistant API | OpenAI ChatCompletion API | OpenAI Responses API | Amazon Bedrock Agents | Google | Anthropic | Description
+-- | -- | -- | -- | -- | -- | -- | -- | --
+Function Calling | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | Enables custom, stateless functions to define specific agent behaviors.
+Code Interpreter | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | Allows agents to execute code for tasks like data analysis or problem-solving.
+Search and Retrieval | ✅ (File Search, Azure AI Search) | ✅ (File Search) | ❌ | ✅ (File Search) | ✅ (Knowledge Bases) | ✅ (Vertex AI Search) | ❌ | Enables agents to search and retrieve information from files, knowledge bases, or enterprise search systems.
+Web Search | ✅ (Bing Search) | ❌ | ✅ | ✅ | ❌ | ✅ (Google Search) | ✅ | Provides real-time access to internet-based content using search engines or web APIs for dynamic, up-to-date information.
+Remote MCP Servers | ✅ | ❌ | ❌ | ✅ | ❌ | ✅ | ✅ | Gives the model access to new capabilities via Model Context Protocol servers.
+Computer Use | ❌ | ❌ | ❌ | ✅ | ✅ (ANTHROPIC.Computer) | ❌ | ✅ | Creates agentic workflows that enable a model to control a computer interface.
+OpenAPI Spec Tool | ✅ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | Integrates existing OpenAPI specifications for service APIs.
+Stateful Functions | ✅ (Azure Functions) | ❌ | ❌ | ❌ | ✅ (AWS Lambda) | ❌ | ❌ | Supports custom, stateful functions for complex agent actions.
+Text Editor | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | Allows agents to view and modify text files for debugging or editing purposes.
+Azure Logic Apps | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | Low-code/no-code solution to add workflows to AI agents.
+Microsoft Fabric | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | Enables agents to interact with data in Microsoft Fabric for insights.
+Image Generation | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | Generates or edits images using GPT image.
+
+### API Comparison
+
+#### Function Calling
+
+ Azure AI Foundry Agent Service
+ Source: https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/tools/function-calling?pivots=rest
+
+ Message Request:
+ ```json
+ {
+ "tools": [
+ {
+ "type": "function",
+ "function": {
+ "description": "{string}",
+ "name": "{string}",
+ "parameters": "{JSON Schema object}"
+ }
+ }
+ ]
+ }
+ ```
+
+ Tool Call Response:
+ ```json
+ {
+ "tool_calls": [
+ {
+ "id": "{string}",
+ "type": "function",
+ "function": {
+ "name": "{string}",
+ "arguments": "{JSON object}",
+ }
+ }
+ ]
+ }
+ ```
+
+
+ OpenAI Assistant API
+ Source: https://platform.openai.com/docs/assistants/tools/function-calling
+
+ Message Request:
+ ```json
+ {
+ "tools": [
+ {
+ "type": "function",
+ "function": {
+ "description": "{string}",
+ "name": "{string}",
+ "parameters": "{JSON Schema object}"
+ }
+ }
+ ]
+ }
+ ```
+
+ Tool Call Response:
+ ```json
+ {
+ "tool_calls": [
+ {
+ "id": "{string}",
+ "type": "function",
+ "function": {
+ "name": "{string}",
+ "arguments": "{JSON object}",
+ }
+ }
+ ]
+ }
+ ```
+
+
+ OpenAI ChatCompletion API
+ Source: https://platform.openai.com/docs/guides/function-calling?api-mode=chat
+
+ Message Request:
+ ```json
+ {
+ "tools": [
+ {
+ "type": "function",
+ "function": {
+ "description": "{string}",
+ "name": "{string}",
+ "parameters": "{JSON Schema object}"
+ }
+ }
+ ]
+ }
+ ```
+
+ Tool Call Response:
+ ```json
+ [
+ {
+ "id": "{string}",
+ "type": "function",
+ "function": {
+ "name": "{string}",
+ "arguments": "{JSON object}",
+ }
+ }
+ ]
+ ```
+
+
+ OpenAI Responses API
+ Source: https://platform.openai.com/docs/guides/function-calling?api-mode=responses
+
+ Message Request:
+ ```json
+ {
+ "tools": [
+ {
+ "type": "function",
+ "description": "{string}",
+ "name": "{string}",
+ "parameters": "{JSON Schema object}"
+ }
+ ]
+ }
+ ```
+
+ Tool Call Response:
+ ```json
+ [
+ {
+ "id": "{string}",
+ "call_id": "{string}",
+ "type": "function_call",
+ "name": "{string}",
+ "arguments": "{JSON object}"
+ }
+ ]
+ ```
+
+
+ Amazon Bedrock Agents
+ Source: https://docs.aws.amazon.com/bedrock/latest/APIReference/API_agent_CreateAgentActionGroup.html#API_agent_CreateAgentActionGroup_RequestSyntax
+
+ CreateAgentActionGroup Request:
+ ```json
+ {
+ "functionSchema": {
+ "name": "{string}",
+ "description": "{string}",
+ "parameters": {
+ "type": "{string | number | integer | boolean | array}",
+ "description": "{string}",
+ "required": "{boolean}"
+ }
+ }
+ }
+ ```
+
+ Tool Call Response:
+ ```json
+ {
+ "invocationInputs": [
+ {
+ "functionInvocationInput": {
+ "actionGroup": "{string}",
+ "function": "{string}",
+ "parameters": [
+ {
+ "name": "{string}",
+ "type": "{string | number | integer | boolean | array}",
+ "value": {}
+ }
+ ]
+ }
+ }
+ ]
+ }
+ ```
+
+
+ Google
+ Source: https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/function-calling#rest
+
+ Message Request:
+ ```json
+ {
+ "tools": [
+ {
+ "functionDeclarations": [
+ {
+ "name": "{string}",
+ "description": "{string}",
+ "parameters": "{JSON Schema object}"
+ }
+ ]
+ }
+ ]
+ }
+ ```
+
+ Tool Call Response:
+ ```json
+ {
+ "content": {
+ "role": "model",
+ "parts": [
+ {
+ "functionCall": {
+ "name": "{string}",
+ "args": {
+ "{argument_name}": {}
+ }
+ }
+ }
+ ]
+ }
+ }
+ ```
+
+
+ Anthropic
+ Source: https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/overview
+
+ Message Request:
+ ```json
+ {
+ "tools": [
+ {
+ "name": "{string}",
+ "description": "{string}",
+ "input_schema": "{JSON Schema object}"
+ }
+ ]
+ }
+ ```
+
+ Tool Call Response:
+ ```json
+ {
+ "id": "{string}",
+ "model": "{string}",
+ "stop_reason": "tool_use",
+ "role": "assistant",
+ "content": [
+ {
+ "type": "text",
+ "text": "{string}"
+ },
+ {
+ "type": "tool_use",
+ "id": "{string}",
+ "name": "{string}",
+ "input": {
+ "argument_name": {}
+ }
+ }
+ ]
+ }
+ ```
+
+
+#### Commonalities
+
+- **Standardized Tool Definition**: All providers use a JSON-based structure for defining tools, including a `type` field (commonly "function") and a `function` object with `name`, `description`, and `parameters` (often following JSON Schema).
+- **Tool Call Response Structure**: Responses typically include a list of tool calls with an `id`, `type`, and details about the function called (e.g., `name` and `arguments`), enabling consistent handling of function invocations.
+- **JSON Schema for Parameters**: Parameters for functions are defined using JSON Schema objects across most providers, facilitating a unified approach to parameter validation and processing.
+- **Extensibility**: The structure allows for additional metadata or fields (e.g., `call_id`, `actionGroup`), suggesting potential for abstraction to support provider-specific extensions while maintaining core compatibility.
+
+
+
+#### Code Interpreter
+
+ Azure AI Foundry Agent Service
+ Source: https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/tools/code-interpreter-samples?pivots=rest-api
+
+ .NET Support: ✅
+
+ Message Request:
+ ```json
+ {
+ "tools": [
+ {
+ "type": "code_interpreter"
+ }
+ ],
+ "tool_resources": {
+ "code_interpreter": {
+ "file_ids": ["{string}"],
+ "data_sources": [
+ {
+ "type": {
+ "id_asset": "{string}",
+ "uri_asset": "{string}"
+ },
+ "uri": "{string}"
+ }
+ ]
+ }
+ }
+ }
+ ```
+
+ Tool Call Response:
+ ```json
+ {
+ "tool_calls": [
+ {
+ "id": "{string}",
+ "type": "code_interpreter",
+ "code_interpreter": {
+ "input": "{string}",
+ "outputs": [
+ {
+ "type": "image",
+ "file_id": "{string}"
+ },
+ {
+ "type": "logs",
+ "logs": "{string}"
+ }
+ ]
+ }
+ }
+ ]
+ }
+ ```
+
+
+ OpenAI Assistant API
+ Source: https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/tools/code-interpreter-samples?pivots=rest-api
+
+ .NET Support: ✅
+
+ Message Request:
+ ```json
+ {
+ "tools": [
+ {
+ "type": "code_interpreter"
+ }
+ ],
+ "tool_resources": {
+ "code_interpreter": {
+ "file_ids": ["{string}"]
+ }
+ }
+ }
+ ```
+
+ Tool Call Response:
+ ```json
+ {
+ "tool_calls": [
+ {
+ "id": "{string}",
+ "type": "code",
+ "code": {
+ "input": "{string}",
+ "outputs": [
+ {
+ "type": "logs",
+ "logs": "{string}"
+ }
+ ]
+ }
+ }
+ ]
+ }
+ ```
+
+
+ OpenAI Responses API
+ Source: https://platform.openai.com/docs/guides/tools-code-interpreter
+
+ .NET Support: ❌ (currently in development: GitHub issue )
+
+ Message Request:
+ ```json
+ {
+ "tools": [
+ {
+ "type": "code_interpreter",
+ "container": { "type": "auto" }
+ }
+ ]
+ }
+ ```
+
+ Tool Call Response:
+ ```json
+ [
+ {
+ "id": "{string}",
+ "code": "{string}",
+ "type": "code_interpreter_call",
+ "status": "{string}",
+ "container_id": "{string}",
+ "results": [
+ {
+ "type": "logs",
+ "logs": "{string}"
+ },
+ {
+ "type": "files",
+ "files": [
+ {
+ "file_id": "{string}",
+ "mime_type": "{string}"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ ```
+
+
+ Amazon Bedrock Agents
+ Source: https://docs.aws.amazon.com/bedrock/latest/userguide/agents-enable-code-interpretation.html
+
+ .NET Support: ❌ (Amazon SDK has IChatClient implementation but lacks ChatOptions.RawRepresentationFactory)
+
+ CreateAgentActionGroup Request:
+ ```json
+ {
+ "actionGroupName": "{string}",
+ "parentActionGroupSignature": "AMAZON.CodeInterpreter",
+ "actionGroupState": "ENABLED"
+ }
+ ```
+
+ Tool Call Response:
+ ```json
+ {
+ "trace": {
+ "orchestrationTrace": {
+ "invocationInput": {
+ "invocationType": "ACTION_GROUP_CODE_INTERPRETER",
+ "codeInterpreterInvocationInput": {
+ "code": "{string}",
+ "files": ["{string}"]
+ }
+ },
+ "observation": {
+ "codeInterpreterInvocationOutput": {
+ "executionError": "{string}",
+ "executionOutput": "{string}",
+ "executionTimeout": "{boolean}",
+ "files": ["{string}"],
+ "metadata": {
+ "clientRequestId": "{string}",
+ "endTime": "{timestamp}",
+ "operationTotalTimeMs": "{long}",
+ "startTime": "{timestamp}",
+ "totalTimeMs": "{long}",
+ "usage": {
+ "inputTokens": "{integer}",
+ "outputTokens": "{integer}"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ ```
+
+
+ Google
+ Source: https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/code-execution#googlegenaisdk_tools_code_exec_with_txt-drest
+
+ .NET Support: ❌ (official SDK lacks IChatClient implementation.)
+
+ Message Request:
+ ```json
+ {
+ "contents": {
+ "role": "{string}",
+ "parts": {
+ "text": "{string}"
+ }
+ },
+ "tools": [
+ {
+ "codeExecution": {}
+ }
+ ]
+ }
+ ```
+
+ Tool Call Response:
+ ```json
+ {
+ "content": {
+ "role": "model",
+ "parts": [
+ {
+ "executableCode": {
+ "language": "{string}",
+ "code": "{string}"
+ }
+ },
+ {
+ "codeExecutionResult": {
+ "outcome": "{string}",
+ "output": "{string}"
+ }
+ }
+ ]
+ }
+ }
+ ```
+
+
+ Anthropic
+ Source: https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/code-execution-tool
+
+
+ .NET Support: ❌
+
+ Anthropic.SDK - uses `code_interpreter` instead of `code_execution` and lacks a possibility to specify file id.
+ Anthropic by tryAGI - has `code_execution` implementation, but it's in beta and can't be used as a tool.
+
+
+
+ Message Request:
+ ```json
+ {
+ "tools": [
+ {
+ "name": "code_execution",
+ "type": "code_execution_20250522"
+ }
+ ]
+ }
+ ```
+
+ Tool Call Response:
+ ```json
+ {
+ "role": "assistant",
+ "container": {
+ "id": "{string}",
+ "expires_at": "{timestamp}"
+ },
+ "content": [
+ {
+ "type": "server_tool_use",
+ "id": "{string}",
+ "name": "code_execution",
+ "input": {
+ "code": "{string}"
+ }
+ },
+ {
+ "type": "code_execution_tool_result",
+ "tool_use_id": "{string}",
+ "content": {
+ "type": "code_execution_result",
+ "stdout": "{string}",
+ "stderr": "{string}",
+ "return_code": "{integer}"
+ }
+ }
+ ]
+ }
+ ```
+
+
+#### Commonalities
+
+- **Tool Type Specification**: Providers consistently define a `code_interpreter` tool type within the `tools` array, indicating support for code execution capabilities.
+- **Input and Output Handling**: Requests include mechanisms to specify code input (e.g., `input` or `code` fields), and responses return execution outputs, such as logs or files, in a structured format.
+- **File Resource Support**: Most providers allow associating files with the code interpreter (e.g., via `file_ids` or `files`), enabling data input/output for code execution.
+- **Execution Metadata**: Responses often include metadata about the execution process (e.g., `status`, `logs`, or `executionError`), which can be abstracted for standardized error handling and result processing.
+
+
+
+#### Search and Retrieval
+
+ Azure AI Foundry Agent Service
+ Source: https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/tools/file-search-upload-files?pivots=rest
+
+ File Search Request:
+ ```json
+ {
+ "tools": [
+ {
+ "type": "file_search"
+ }
+ ],
+ "tool_resources": {
+ "file_search": {
+ "vector_store_ids": ["{string}"],
+ "vector_stores": [
+ {
+ "name": "{string}",
+ "configuration": {
+ "data_sources": [
+ {
+ "type": {
+ "id_asset": "{string}",
+ "uri_asset": "{string}"
+ },
+ "uri": "{string}"
+ }
+ ]
+ }
+ }
+ ]
+ }
+ }
+ }
+ ```
+
+ File Search Tool Call Response:
+ ```json
+ {
+ "tool_calls": [
+ {
+ "id": "{string}",
+ "type": "file_search",
+ "file_search": {
+ "ranking_options": {
+ "ranker": "{string}",
+ "score_threshold": "{float}"
+ },
+ "results": [
+ {
+ "file_id": "{string}",
+ "file_name": "{string}",
+ "score": "{float}",
+ "content": [
+ {
+ "text": "{string}",
+ "type": "{string}"
+ }
+ ]
+ }
+ ]
+ }
+ }
+ ]
+ }
+ ```
+
+ Azure AI Search Request:
+ ```json
+ {
+ "tools": [
+ {
+ "type": "azure_ai_search"
+ }
+ ],
+ "tool_resources": {
+ "azure_ai_search": {
+ "indexes": [
+ {
+ "index_connection_id": "{string}",
+ "index_name": "{string}",
+ "query_type": "{string}"
+ }
+ ]
+ }
+ }
+ }
+ ```
+
+ Azure AI Search Tool Call Response:
+ ```json
+ {
+ "tool_calls": [
+ {
+ "id": "{string}",
+ "type": "azure_ai_search",
+ "azure_ai_search": {} // From documentation: Reserved for future use
+ }
+ ]
+ }
+ ```
+
+
+ OpenAI Assistant API
+ Source: https://platform.openai.com/docs/assistants/tools/file-search
+
+ Message Request:
+ ```json
+ {
+ "tools": [
+ {
+ "type": "file_search"
+ }
+ ],
+ "tool_resources": {
+ "file_search": {
+ "vector_store_ids": ["string"]
+ }
+ }
+ }
+ ```
+
+ Tool Call Response:
+ ```json
+ {
+ "tool_calls": [
+ {
+ "id": "{string}",
+ "type": "file_search",
+ "file_search": {
+ "ranking_options": {
+ "ranker": "{string}",
+ "score_threshold": "{float}"
+ },
+ "results": [
+ {
+ "file_id": "{string}",
+ "file_name": "{string}",
+ "score": "{float}",
+ "content": [
+ {
+ "text": "{string}",
+ "type": "{string}"
+ }
+ ]
+ }
+ ]
+ }
+ }
+ ]
+ }
+ ```
+
+
+ OpenAI Responses API
+ Source: https://platform.openai.com/docs/api-reference/responses/create
+
+ Message Request:
+ ```json
+ {
+ "tools": [
+ {
+ "type": "file_search"
+ }
+ ],
+ "tool_resources": {
+ "file_search": {
+ "vector_store_ids": ["string"]
+ }
+ }
+ }
+ ```
+
+ Tool Call Response:
+ ```json
+ {
+ "output": [
+ {
+ "id": "{string}",
+ "queries": ["{string}"],
+ "status": "{in_progress | searching | incomplete | failed | completed}",
+ "type": "file_search_call",
+ "results": [
+ {
+ "attributes": {},
+ "file_id": "{string}",
+ "filename": "{string}",
+ "score": "{float}",
+ "text": "{string}"
+ }
+ ]
+ }
+ ]
+ }
+ ```
+
+
+ Amazon Bedrock Agents
+ Source: https://docs.aws.amazon.com/bedrock/latest/APIReference/API_agent-runtime_InvokeAgent.html
+
+ Message Request:
+ ```json
+ {
+ "sessionState": {
+ "knowledgeBaseConfigurations": [
+ {
+ "knowledgeBaseId": "{string}",
+ "retrievalConfiguration": {
+ "vectorSearchConfiguration": {
+ "filter": {},
+ "implicitFilterConfiguration": {
+ "metadataAttributes": [
+ {
+ "description": "{string}",
+ "key": "{string}",
+ "type": "{string}"
+ }
+ ],
+ "modelArn": "{string}"
+ },
+ "numberOfResults": "{number}",
+ "overrideSearchType": "{string}",
+ "rerankingConfiguration": {
+ "bedrockRerankingConfiguration": {
+ "metadataConfiguration": {
+ "selectionMode": "{string}",
+ "selectiveModeConfiguration": {}
+ },
+ "modelConfiguration": {
+ "additionalModelRequestFields": {
+ "string" : "{JSON string}"
+ },
+ "modelArn": "{string}"
+ },
+ "numberOfRerankedResults": "{number}"
+ },
+ "type": "{string}"
+ }
+ }
+ }
+ }
+ ]
+ }
+ }
+ ```
+
+ Tool Call Response:
+ ```json
+ {
+ "trace": {
+ "orchestrationTrace": {
+ "invocationInput": {
+ "invocationType": "KNOWLEDGE_BASE",
+ "knowledgeBaseLookupInput": {
+ "knowledgeBaseId": "{string}",
+ "text": "{string}"
+ }
+ },
+ "observation": {
+ "type": "KNOWLEDGE_BASE",
+ "knowledgeBaseLookupOutput": {
+ "retrievedReferences": [
+ {
+ "metadata": {},
+ "content": {
+ "byteContent": "{string}",
+ "row": [
+ {
+ "columnName": "{string}",
+ "columnValue": "{string}",
+ "type": "{BLOB | BOOLEAN | DOUBLE | NULL | LONG | STRING}"
+ }
+ ],
+ "text": "{string}",
+ "type": "{TEXT | IMAGE | ROW}"
+ }
+ }
+ ],
+ "metadata": {
+ "clientRequestId": "{string}",
+ "endTime": "{timestamp}",
+ "operationTotalTimeMs": "{long}",
+ "startTime": "{timestamp}",
+ "totalTimeMs": "{long}",
+ "usage": {
+ "inputTokens": "{integer}",
+ "outputTokens": "{integer}"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ ```
+
+
+ Google
+ Source: https://cloud.google.com/vertex-ai/generative-ai/docs/grounding/grounding-with-vertex-ai-search
+
+ Message Request:
+ ```json
+ {
+ "contents": [
+ {
+ "role": "user",
+ "parts": [
+ {
+ "text": "{string}"
+ }
+ ]
+ }
+ ],
+ "tools": [
+ {
+ "retrieval": {
+ "vertexAiSearch": {
+ "datastore": "{string}"
+ }
+ }
+ }
+ ]
+ }
+ ```
+
+ Tool Call Response:
+ ```json
+ {
+ "content": {
+ "role": "model",
+ "parts": [
+ {
+ "text": "{string}"
+ }
+ ]
+ },
+ "groundingMetadata": {
+ "retrievalQueries": [
+ "{string}"
+ ],
+ "groundingChunks": [
+ {
+ "retrievedContext": {
+ "uri": "{string}",
+ "title": "{string}"
+ }
+ }
+ ],
+ "groundingSupport": [
+ {
+ "segment": {
+ "startIndex": "{number}",
+ "endIndex": "{number}"
+ },
+ "segment_text": "{string}",
+ "supportChunkIndices": ["{number}"],
+ "confidenceScore": ["{number}"]
+ }
+ ]
+ }
+ }
+ ```
+
+
+#### Commonalities
+
+- **Vector Store Integration**: Providers like Azure and OpenAI use `vector_store_ids` or similar constructs to reference vector stores for file search, suggesting a common approach to retrieval-augmented generation.
+- **Search Configuration**: Requests include configurations for search (e.g., `vectorSearchConfiguration`, `ranking_options`), allowing customization of retrieval parameters like result count or ranking.
+- **Result Structure**: Responses contain a list of search results with fields like `file_id`, `score`, and `content` or `text`, enabling consistent processing of retrieved data.
+- **Metadata Inclusion**: Search responses often include metadata (e.g., `score`, `timestamp`, `usage`), which can be abstracted for unified analytics and performance tracking.
+
+
+
+#### Web Search
+
+ Azure AI Foundry Agent Service
+ Source: https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/tools/bing-code-samples?pivots=rest
+
+ Bing Search Message Request:
+ ```json
+ {
+ "tools": [
+ {
+ "type": "bing_grounding",
+ "bing_grounding": {
+ "search_configurations": [
+ {
+ "connection_id": "{string}",
+ "count": "{number}",
+ "market": "{string}",
+ "set_lang": "{string}",
+ "freshness": "{string}",
+ }
+ ]
+ }
+ }
+ ]
+ }
+ ```
+
+ Bing Search Tool Call Response:
+ ```json
+ {
+ "tool_calls": [
+ {
+ "id": "{string}",
+ "type": "function",
+ "bing_grounding": {} // From documentation: Reserved for future use
+ }
+ ]
+ }
+ ```
+
+
+ OpenAI ChatCompletion API
+ Source: https://platform.openai.com/docs/guides/tools-web-search?api-mode=chat
+
+ Message Request:
+ ```json
+ {
+ "web_search_options": {},
+ "messages": [
+ {
+ "role": "user",
+ "content": "{string}"
+ }
+ ]
+ }
+ ```
+
+ Tool Call Response:
+ ```json
+ [
+ {
+ "index": 0,
+ "message": {
+ "role": "assistant",
+ "content": "{string}",
+ "annotations": [
+ {
+ "type": "url_citation",
+ "url_citation": {
+ "end_index": "{number}",
+ "start_index": "{number}",
+ "title": "{string}",
+ "url": "{string}"
+ }
+ }
+ ]
+ }
+ }
+ ]
+ ```
+
+
+ OpenAI Responses API
+ Source: https://platform.openai.com/docs/guides/tools-web-search?api-mode=responses
+
+ Message Request:
+ ```json
+ {
+ "tools": [
+ {
+ "type": "web_search_preview"
+ }
+ ],
+ "input": "{string}"
+ }
+ ```
+
+ Tool Call Response:
+ ```json
+ {
+ "output": [
+ {
+ "type": "web_search_call",
+ "id": "{string}",
+ "status": "{string}"
+ },
+ {
+ "id": "{string}",
+ "type": "message",
+ "status": "{string}",
+ "role": "assistant",
+ "content": [
+ {
+ "type": "output_text",
+ "text": "{string}",
+ "annotations": [
+ {
+ "type": "url_citation",
+ "start_index": "{number}",
+ "end_index": "{string}",
+ "url": "{string}",
+ "title": "{string}"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ```
+
+
+ Google
+ Source: https://cloud.google.com/vertex-ai/generative-ai/docs/grounding/grounding-with-google-search
+
+ Message Request:
+ ```json
+ {
+ "contents": [
+ {
+ "role": "user",
+ "parts": [
+ {
+ "text": "{string}"
+ }
+ ]
+ }
+ ],
+ "tools": [
+ {
+ "googleSearch": {}
+ }
+ ]
+ }
+ ```
+
+ Tool Call Response:
+ ```json
+ {
+ "content": {
+ "role": "model",
+ "parts": [
+ {
+ "text": "{string}"
+ }
+ ]
+ },
+ "groundingMetadata": {
+ "webSearchQueries": [
+ "{string}"
+ ],
+ "searchEntryPoint": {
+ "renderedContent": "{string}"
+ },
+ "groundingChunks": [
+ {
+ "web": {
+ "uri": "{string}",
+ "title": "{string}",
+ "domain": "{string}"
+ }
+ }
+ ],
+ "groundingSupports": [
+ {
+ "segment": {
+ "startIndex": "{number}",
+ "endIndex": "{number}",
+ "text": "{string}"
+ },
+ "groundingChunkIndices": [
+ "{number}"
+ ],
+ "confidenceScores": [
+ "{number}"
+ ]
+ }
+ ],
+ "retrievalMetadata": {}
+ }
+ }
+ ```
+
+
+ Anthropic
+ Source: https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/web-search-tool
+
+ Message Request:
+ ```json
+ {
+ "tools": [
+ {
+ "name": "web_search",
+ "type": "web_search_20250305",
+ "max_uses": "{number}",
+ "allowed_domains": ["{string}"],
+ "blocked_domains": ["{string}"],
+ "user_location": {
+ "type": "approximate",
+ "city": "{string}",
+ "region": "{string}",
+ "country": "{string}",
+ "timezone": "{string}"
+ }
+ }
+ ]
+ }
+ ```
+
+ Tool Call Response:
+ ```json
+ {
+ "role": "assistant",
+ "content": [
+ {
+ "type": "server_tool_use",
+ "id": "{string}",
+ "name": "web_search",
+ "input": {
+ "query": "{string}"
+ }
+ },
+ {
+ "type": "web_search_tool_result",
+ "tool_use_id": "{string}",
+ "content": [
+ {
+ "type": "web_search_result",
+ "url": "{string}",
+ "title": "{string}",
+ "encrypted_content": "{string}",
+ "page_age": "{string}"
+ }
+ ]
+ },
+ {
+ "text": "{string}",
+ "type": "text",
+ "citations": [
+ {
+ "type": "web_search_result_location",
+ "url": "{string}",
+ "title": "{string}",
+ "encrypted_index": "{string}",
+ "cited_text": "{string}"
+ }
+ ]
+ }
+ ]
+ }
+ ```
+
+
+#### Commonalities
+
+- **Tool-Based Activation**: Providers define web search as a tool (e.g., `web_search`, `bing_grounding`, `googleSearch`), typically within a `tools` array, allowing standardized activation of search capabilities.
+- **Query Input**: Requests support passing a search query (e.g., via `input`, `content`, or `query`), enabling a unified interface for initiating searches.
+- **Result Annotations**: Responses include search results with metadata like `url`, `title`, and sometimes `confidenceScores` or `citations`, which can be abstracted for consistent result presentation.
+- **Grounding Metadata**: Most providers include grounding metadata (e.g., `groundingMetadata`, `annotations`), facilitating traceability and validation of search results.
+
+
+
+#### Remote MCP Servers
+
+ OpenAI Responses API
+ Source: https://platform.openai.com/docs/guides/tools-remote-mcp
+
+ Message Request:
+ ```json
+ {
+ "tools": [
+ {
+ "type": "mcp",
+ "server_label": "{string}",
+ "server_url": "{string}",
+ "require_approval": "{string}"
+ }
+ ]
+ }
+ ```
+
+ Tool Call Response:
+ ```json
+ {
+ "output": [
+ {
+ "id": "{string}",
+ "type": "mcp_list_tools",
+ "server_label": "{string}",
+ "tools": [
+ {
+ "name": "{string}",
+ "input_schema": "{JSON Schema object}"
+ }
+ ]
+ },
+ {
+ "id": "{string}",
+ "type": "mcp_call",
+ "approval_request_id": "{string}",
+ "arguments": "{JSON string}",
+ "error": "{string}",
+ "name": "{string}",
+ "output": "{string}",
+ "server_label": "{string}"
+ }
+ ]
+ }
+ ```
+
+
+ Google
+ Source: https://google.github.io/adk-docs/tools/mcp-tools/#using-mcp-tools-in-your-own-agent-out-of-adk-web
+
+ ```python
+ async def get_agent_async():
+ toolset = MCPToolset(
+ tool_filter=['read_file', 'list_directory'] # Optional: filter specific tools
+ connection_params=SseServerParams(url="http://remote-server:port/path", headers={...})
+ )
+
+ # Use in an agent
+ root_agent = LlmAgent(
+ model='model', # Adjust model name if needed based on availability
+ name='agent_name',
+ instruction='agent_instructions',
+ tools=[toolset], # Provide the MCP tools to the ADK agent
+ )
+ return root_agent, toolset
+ ```
+
+
+ Anthropic
+ Source: https://docs.anthropic.com/en/docs/agents-and-tools/mcp-connector
+
+ Message Request:
+ ```json
+ {
+ "messages": [
+ {
+ "role": "user",
+ "content": "{string}"
+ }
+ ],
+ "mcp_servers": [
+ {
+ "type": "url",
+ "url": "{string}",
+ "name": "{string}",
+ "tool_configuration": {
+ "enabled": true,
+ "allowed_tools": ["{string}"]
+ },
+ "authorization_token": "{string}"
+ }
+ ]
+ }
+ ```
+
+ Tool Use Response:
+ ```json
+ {
+ "type": "mcp_tool_use",
+ "id": "{string}",
+ "name": "{string}",
+ "server_name": "{string}",
+ "input": { "param1": "{object}", "param2": "{object}" }
+ }
+ ```
+
+ Tool Result Response:
+ ```json
+ {
+ "type": "mcp_tool_result",
+ "tool_use_id": "{string}",
+ "is_error": "{boolean}",
+ "content": [
+ {
+ "type": "text",
+ "text": "{string}"
+ }
+ ]
+ }
+ ```
+
+
+#### Commonalities
+
+- **Server Configuration**: Providers specify remote servers via URL and metadata (e.g., `server_url`, `url`, `name`), enabling a standardized way to connect to external MCP services.
+- **Tool Integration**: MCP tools are integrated into the `tools` or `mcp_servers` array, allowing agents to interact with remote tools in a consistent manner.
+- **Input/Output Structure**: Requests and responses include structured input (e.g., `input`, `arguments`) and output (e.g., `output`, `content`), supporting abstraction for tool execution workflows.
+- **Authorization Support**: Most providers include mechanisms for authentication (e.g., `authorization_token`, `headers`), which can be abstracted for secure communication with remote servers.
+
+
+
+#### Computer Use
+
+ OpenAI Responses API
+ Source: https://platform.openai.com/docs/guides/tools-computer-use
+
+ Message Request:
+ ```json
+ {
+ "tools": [
+ {
+ "type": "computer_use_preview",
+ "display_width": "{number}",
+ "display_height": "{number}",
+ "environment": "{browser | mac | windows | ubuntu}"
+ }
+ ]
+ }
+ ```
+
+ Tool Call Response:
+ ```json
+ {
+ "output": [
+ {
+ "type": "reasoning",
+ "id": "{string}",
+ "summary": [
+ {
+ "type": "summary_text",
+ "text": "{string}"
+ }
+ ]
+ },
+ {
+ "type": "computer_call",
+ "id": "{string}",
+ "call_id": "{string}",
+ "action": {
+ "type": "{click | double_click | drag | keypress | move | screenshot | scroll | type | wait}",
+ // Other properties are associated with specific action type.
+ },
+ "pending_safety_checks": [],
+ "status": "{in_progress | completed | incomplete}"
+ }
+ ]
+ }
+ ```
+
+
+ Amazon Bedrock Agents
+ Source: https://docs.aws.amazon.com/bedrock/latest/APIReference/API_agent_CreateAgentActionGroup.html#API_agent_CreateAgentActionGroup_RequestSyntax
+ Source: https://docs.aws.amazon.com/bedrock/latest/userguide/agent-computer-use-handle-tools.html
+
+ CreateAgentActionGroup Request:
+ ```json
+ {
+ "actionGroupName": "{string}",
+ "parentActionGroupSignature": "ANTHROPIC.Computer",
+ "actionGroupState": "ENABLED"
+ }
+ ```
+
+ Tool Call Response:
+ ```json
+ {
+ "returnControl": {
+ "invocationId": "{string}",
+ "invocationInputs": [
+ {
+ "functionInvocationInput": {
+ "actionGroup": "{string}",
+ "actionInvocationType": "RESULT",
+ "agentId": "{string}",
+ "function": "{string}",
+ "parameters": [
+ {
+ "name": "{string}",
+ "type": "string",
+ "value": "{string}"
+ }
+ ]
+ }
+ }
+ ]
+ }
+ }
+ ```
+
+
+ Anthropic
+ Source: https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/computer-use-tool
+
+ Message Request:
+ ```json
+ {
+ "tools": [
+ {
+ "type": "computer_20250124",
+ "name": "computer",
+ "display_width_px": "{number}",
+ "display_height_px": "{number}",
+ "display_number": "{number}"
+ },
+ ]
+ }
+ ```
+
+ Tool Call Response:
+ ```json
+ {
+ "role": "assistant",
+ "content": [
+ {
+ "type": "tool_use",
+ "id": "{string}",
+ "name": "{string}",
+ "input": "{object}"
+ }
+ ]
+ }
+ ```
+
+
+#### Commonalities
+
+- **Tool Type Definition**: Providers define a computer use tool (e.g., `computer_use_preview`, `computer_20250124`, `ANTHROPIC.Computer`) within the `tools` array, indicating support for computer interaction capabilities.
+- **Action Specification**: Responses include actions (e.g., `click`, `keypress`, `type`) with associated parameters, enabling standardized interaction with computer environments.
+- **Environment Configuration**: Requests allow specifying the environment (e.g., `browser`, `windows`, `display_width`), which can be abstracted for cross-platform compatibility.
+- **Status Tracking**: Responses include status indicators (e.g., `status`, `pending_safety_checks`), facilitating consistent monitoring of computer use tasks.
+
+
+
+#### OpenAPI Spec Tool
+
+ Azure AI Foundry Agent Service
+ Source: https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/tools/openapi-spec-samples?pivots=rest-api
+ Source: https://learn.microsoft.com/en-us/rest/api/aifoundry/aiagents/run-steps/get-run-step?view=rest-aifoundry-aiagents-v1&tabs=HTTP#runstepopenapitoolcall
+
+ Message Request:
+ ```json
+ {
+ "tools": [
+ {
+ "type": "openapi",
+ "openapi": {
+ "description": "{string}",
+ "name": "{string}",
+ "auth": {
+ "type": "{string}"
+ },
+ "spec": "{OpenAPI specification object}"
+ }
+ }
+ ]
+ }
+ ```
+
+ Tool Call Response:
+ ```json
+ {
+ "tool_calls": [
+ {
+ "id": "{string}",
+ "type": "openapi",
+ "openapi": {} // From documentation: Reserved for future use
+ }
+ ]
+ }
+ ```
+
+
+ Amazon Bedrock Agents
+ Source: https://docs.aws.amazon.com/bedrock/latest/APIReference/API_agent_CreateAgentActionGroup.html#API_agent_CreateAgentActionGroup_RequestSyntax
+
+ CreateAgentActionGroup Request:
+ ```json
+ {
+ "apiSchema": {
+ "payload": "{JSON or YAML OpenAPI specification string}"
+ }
+ }
+ ```
+
+ Tool Call Response:
+ ```json
+ {
+ "invocationInputs": [
+ {
+ "apiInvocationInput": {
+ "actionGroup": "{string}",
+ "apiPath": "{string}",
+ "httpMethod": "{string}",
+ "parameters": [
+ {
+ "name": "{string}",
+ "type": "{string}",
+ "value": "{string}"
+ }
+ ]
+ }
+ }
+ ]
+ }
+ ```
+
+
+#### Commonalities
+
+- **OpenAPI Specification**: Both providers support defining tools using OpenAPI specifications, either as a JSON/YAML payload or a structured `spec` object, enabling standardized API integration.
+- **Tool Type Identification**: The tool is identified as `openapi` or via an `apiSchema`, providing a clear entry point for OpenAPI-based tool usage.
+- **Parameter Handling**: Responses include parameters (e.g., `parameters`, `apiPath`, `httpMethod`) for API invocation, which can be abstracted for unified API call execution.
+
+
+
+#### Stateful Functions
+
+ Azure AI Foundry Agent Service
+ Source: https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/tools/azure-functions-samples?pivots=rest
+
+ Message Request:
+ ```json
+ {
+ "tools": [
+ {
+ "type": "azure_function",
+ "azure_function": {
+ "function": {
+ "name": "{string}",
+ "description": "{string}",
+ "parameters": "{JSON Schema object}"
+ },
+ "input_binding": {
+ "type": "storage_queue",
+ "storage_queue": {
+ "queue_service_endpoint": "{string}",
+ "queue_name": "{string}"
+ }
+ },
+ "output_binding": {
+ "type": "storage_queue",
+ "storage_queue": {
+ "queue_service_endpoint": "{string}",
+ "queue_name": "{string}"
+ }
+ }
+ }
+ }
+ ]
+ }
+ ```
+
+ Tool Call Response: Not specified in the documentation.
+
+
+ Amazon Bedrock Agents
+ Source: https://docs.aws.amazon.com/bedrock/latest/APIReference/API_agent_CreateAgentActionGroup.html#API_agent_CreateAgentActionGroup_RequestSyntax
+
+ CreateAgentActionGroup Request:
+ ```json
+ {
+ "apiSchema": {
+ "payload": "{JSON or YAML OpenAPI specification string}"
+ }
+ }
+ ```
+
+ Tool Call Response:
+ ```json
+ {
+ "invocationInputs": [
+ {
+ "apiInvocationInput": {
+ "actionGroup": "{string}",
+ "apiPath": "{string}",
+ "httpMethod": "{string}",
+ "parameters": [
+ {
+ "name": "{string}",
+ "type": "{string}",
+ "value": "{string}"
+ }
+ ]
+ }
+ }
+ ]
+ }
+ ```
+
+
+#### Commonalities
+
+- **API-Driven Interaction**: Both providers use API-based structures (e.g., `apiSchema`, `azure_function`) to define stateful functions, enabling integration with external services.
+- **Parameter Specification**: Requests include parameter definitions (e.g., `parameters`, `JSON Schema object`), supporting standardized input handling.
+
+
+
+#### Text Editor
+
+ Anthropic
+ Source: https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/text-editor-tool
+
+ Message Request:
+ ```json
+ {
+ "tools": [
+ {
+ "type": "text_editor_20250429",
+ "name": "str_replace_based_edit_tool"
+ }
+ ]
+ }
+ ```
+
+ Tool Call Response:
+ ```json
+ {
+ "role": "assistant",
+ "content": [
+ {
+ "type": "tool_use",
+ "id": "{string}",
+ "name": "str_replace_based_edit_tool",
+ "input": {
+ "command": "{string}",
+ "path": "{string}"
+ }
+ }
+ ]
+ }
+ ```
+
+
+
+
+#### Microsoft Fabric
+
+ Azure AI Foundry Agent Service
+ Source: https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/tools/fabric?pivots=rest
+
+ Message Request:
+ ```json
+ {
+ "tools": [
+ {
+ "type": "fabric_dataagent",
+ "fabric_dataagent": {
+ "connections": [
+ {
+ "connection_id": "{string}"
+ }
+ ]
+ }
+ }
+ ]
+ }
+ ```
+
+ Tool Call Response: Not specified in the documentation.
+
+
+
+
+#### Image Generation
+
+ OpenAI Responses API
+ Source: https://platform.openai.com/docs/guides/tools-image-generation
+
+ Message Request:
+ ```json
+ {
+ "tools": [
+ {
+ "type": "image_generation"
+ }
+ ]
+ }
+ ```
+
+ Tool Call Response:
+ ```json
+ {
+ "output": [
+ {
+ "type": "image_generation_call",
+ "id": "{string}",
+ "result": "{Base64 string}",
+ "status": "{string}"
+ }
+ ]
+ }
+ ```
+
+
+
+
+## Decision Outcome
+
+TBD.
diff --git a/docs/decisions/0003-agent-opentelemetry-instrumentation.md b/docs/decisions/0003-agent-opentelemetry-instrumentation.md
new file mode 100644
index 0000000..863387b
--- /dev/null
+++ b/docs/decisions/0003-agent-opentelemetry-instrumentation.md
@@ -0,0 +1,144 @@
+---
+status: proposed
+contact: rogerbarreto
+date: 2025-07-14
+deciders: stephentoub, markwallace-microsoft, rogerbarreto, westey-m
+informed: {}
+---
+
+# Agent OpenTelemetry Instrumentation
+
+## Context and Problem Statement
+
+Currently, the Agent Framework lacks comprehensive observability and telemetry capabilities, making it difficult for developers to monitor agent performance, track usage patterns, debug issues, and gain insights into agent behavior in production environments. While the underlying ChatClient implementations may have their own telemetry, there is no standardized way to capture agent-specific metrics and traces that provide visibility into agent operations, token usage, response times, and error patterns at the agent abstraction level.
+
+## Decision Drivers
+
+- **Compliance**: The implementation should adhere to established OpenTelemetry semantic conventions for agents, ensuring consistency and interoperability with existing telemetry systems.
+- **Observability Requirements**: Developers need comprehensive telemetry to monitor agent performance, track usage patterns, and debug issues in production environments.
+- **Standardization**: The solution must follow established OpenTelemetry semantic conventions and integrate seamlessly with existing .NET telemetry infrastructure.
+- **Microsoft.Extensions.AI Alignment**: The implementation should follow the exact patterns and conventions established by Microsoft.Extensions.AI's OpenTelemetry instrumentation.
+- **Non-Intrusive Design**: Telemetry should be optional and not impact the core agent functionality or performance when disabled.
+- **Agent-Level Insights**: The telemetry should capture agent-specific operations without duplicating underlying ChatClient telemetry.
+- **Extensibility**: The solution should support future enhancements and additional telemetry scenarios.
+
+## Considered Options
+
+### Option 1: Direct Integration into Core Agent Classes
+
+Embed OpenTelemetry instrumentation directly into the base `Agent` class and `ChatClientAgent` implementations.
+
+#### Pros
+- Automatic telemetry for all agent implementations
+- No additional wrapper classes needed
+- Consistent telemetry across all agents
+
+#### Cons
+- Violates single responsibility principle
+- Increases complexity of core agent classes
+- Makes telemetry mandatory rather than optional
+- Harder to test and maintain
+- Couples telemetry concerns with business logic
+
+### Option 2: Aspect-Oriented Programming (AOP) Approach
+
+Use interceptors or AOP frameworks to inject telemetry behavior into agent methods.
+
+#### Pros
+- Clean separation of concerns
+- Non-intrusive to existing code
+- Can be applied selectively
+
+#### Cons
+- Adds complexity with AOP framework dependencies
+- Runtime overhead for interception
+- Harder to debug and understand
+- Not consistent with Microsoft.Extensions.AI patterns
+
+### Option 3: OpenTelemetryAgent Wrapper Pattern
+
+Create a delegating `OpenTelemetryAgent` wrapper class that implements the `Agent` interface and wraps any existing agent with telemetry instrumentation, following the exact pattern of Microsoft.Extensions.AI's `OpenTelemetryChatClient`.
+
+#### Pros
+- Follows established Microsoft.Extensions.AI patterns exactly
+- Clean separation of concerns
+- Optional and non-intrusive
+- Easy to test and maintain
+- Consistent with .NET telemetry conventions
+- Supports any agent implementation
+- Provides agent-level telemetry without duplicating ChatClient telemetry
+
+#### Cons
+- Requires explicit wrapping of agents
+- Additional object allocation for wrapper
+
+## Decision Outcome
+
+Chosen option: "OpenTelemetryAgent Wrapper Pattern", because it follows the established Microsoft.Extensions.AI patterns exactly, provides clean separation of concerns, maintains optional telemetry, and offers the best balance of functionality, maintainability, and consistency with existing .NET telemetry infrastructure.
+
+### Implementation Details
+
+The implementation includes:
+
+1. **OpenTelemetryAgent Wrapper Class**: A delegating agent that wraps any `Agent` implementation with telemetry instrumentation
+2. **AgentOpenTelemetryConsts**: Comprehensive constants for telemetry attribute names and metric definitions
+3. **Extension Methods**: `.WithOpenTelemetry()` extension method for easy agent wrapping
+4. **Comprehensive Test Suite**: Full test coverage following Microsoft.Extensions.AI testing patterns
+
+### Telemetry Data Captured
+
+**Activities/Spans:**
+- `agent.operation.name` (agent.run, agent.run_streaming)
+- `agent.request.id`, `agent.request.name`, `agent.request.instructions`
+- `agent.request.message_count`, `agent.request.thread_id`
+- `agent.response.id`, `agent.response.message_count`, `agent.response.finish_reason`
+- `agent.usage.input_tokens`, `agent.usage.output_tokens`
+- Error information and activity status codes
+
+**Metrics:**
+- Operation duration histogram with proper buckets
+- Token usage histogram (input/output tokens)
+- Request count counter
+- All metrics tagged with operation type and agent name
+
+### Consequences
+
+- **Good**: Provides comprehensive agent-level observability following established patterns
+- **Good**: Non-intrusive and optional implementation that doesn't affect core functionality
+- **Good**: Consistent with Microsoft.Extensions.AI telemetry conventions
+- **Good**: Easy to integrate with existing OpenTelemetry infrastructure
+- **Good**: Supports debugging, monitoring, and performance analysis
+- **Neutral**: Requires explicit wrapping of agents with `.WithOpenTelemetry()`
+- **Neutral**: Additional object allocation for telemetry wrapper
+
+## Validation
+
+The implementation is validated through:
+
+1. **Comprehensive Unit Tests**: 16 test methods covering all scenarios including success, error, streaming, and edge cases
+2. **Integration Testing**: Step05 telemetry sample demonstrating real-world usage
+3. **Pattern Compliance**: Exact adherence to Microsoft.Extensions.AI OpenTelemetry patterns
+4. **Semantic Convention Compliance**: Follows OpenTelemetry semantic conventions for telemetry data
+
+## More Information
+
+### Usage Example
+
+```csharp
+// Create TracerProvider
+using var tracerProvider = Sdk.CreateTracerProviderBuilder()
+ .AddSource(AgentOpenTelemetryConsts.DefaultSourceName)
+ .AddConsoleExporter()
+ .Build();
+
+// Create and wrap agent with telemetry
+var baseAgent = new ChatClientAgent(chatClient, options);
+using var telemetryAgent = baseAgent.WithOpenTelemetry();
+
+// Use agent normally - telemetry is captured automatically
+var response = await telemetryAgent.RunAsync(messages);
+```
+
+### Relationship to Microsoft.Extensions.AI
+
+This implementation follows the exact patterns established by Microsoft.Extensions.AI's OpenTelemetry instrumentation, ensuring consistency across the AI ecosystem and leveraging proven patterns for telemetry integration.
diff --git a/docs/decisions/0004-foundry-sdk-extensions.md b/docs/decisions/0004-foundry-sdk-extensions.md
new file mode 100644
index 0000000..b4f0308
--- /dev/null
+++ b/docs/decisions/0004-foundry-sdk-extensions.md
@@ -0,0 +1,62 @@
+---
+# These are optional elements. Feel free to remove any of them.
+status: proposed
+contact: markwallace-microsoft
+date: 2025-08-06
+deciders: markwallace-microsoft, westey-m, quibitron, trrwilson
+consulted:
+informed:
+---
+
+# `Azure.AI.Agents.Persistent` package Extensions Methods for Agent Framework
+
+## Context and Problem Statement
+
+To align the `Azure.AI.Agents.Persistent` package and Agent Framework a set of extensions methods have been created which allow a developer to create or retrieve an `AIAgent` using the `PersistentAgentsClient`.
+The purpose of this ADR is to decide where these extension methods should live.
+
+## Decision Drivers
+
+- Provide the optimum experience for developers.
+- Avoid adding additional dependencies to the `Azure.AI.Agents.Persistent` package (and not in the future)
+
+## Considered Options
+
+- Add the extension methods to the `Azure.AI.Agents.Persistent` package and change it's dependencies
+- Add the extension methods to the `Azure.AI.Agents.Persistent` package without changing it's dependencies
+- Add the extension methods to a `Microsoft.Extensions.AI.Azure` package
+
+
+### Add the extension methods to the `Azure.AI.Agents.Persistent` package and change it's dependencies
+
+- `Azure.AI.Agents.Persistent` would depend on `Microsoft.Extensions.AI` instead of `Microsoft.Extensions.AI.Abstractions`
+
+- Good because, extension methods are in the `Azure.AI.Agents.Persistent` package and can be easily kept up-to-date
+- Good because, developers don't need to explicitly depend on a new package to get Agent Framework functionality
+- Bad because, it introduces additional dependencies which would possibly grow overtime
+
+
+### - Add the extension methods to the `Azure.AI.Agents.Persistent` package without changing it's dependencies
+
+- `Azure.AI.Agents.Persistent` would depend on `Microsoft.Extensions.AI.Abstractions` (as it currently does)
+- `ChatClientAgent` and `FunctionInvokingChatClient` would move to `Microsoft.Extensions.AI.Abstractions`
+
+- Good because, extension methods are in the `Azure.AI.Agents.Persistent` package and can be easily kept up-to-date
+- Good because, developers don't need to explicitly depend on a new package to get Agent Framework functionality
+- Good because, it introduces minimal additional dependencies
+- Bad because, it adds additional dependencies to `Microsoft.Extensions.AI.Abstractions` and these additional dependencies add up as transitive to `Azure`.AI.Agents.Persistent`
+
+
+### Add the extension methods to a `Microsoft.Extensions.AI.Azure` package
+
+- Introduce a new package called `Microsoft.Extensions.AI.Azure` where the extension methods would live
+- `Azure.AI.Agents.Persistent` does not change
+
+- Good because, it introduces no additional dependencies to `Azure.AI.Agents.Persistent` package
+- Bad because, extension methods are not in the `Azure.AI.Agents.Persistent` package and cannot be easily kept up-to-date
+- Bad because, developers need to explicitly depend on a new package to get Agent Framework functionality
+
+## Decision Outcome
+
+Chosen option: "Add the extension methods to a `Microsoft.Extensions.AI.Azure` package", because
+it introduces no additional dependencies to `Azure.AI.Agents.Persistent` package.
diff --git a/docs/decisions/0005-python-naming-conventions.md b/docs/decisions/0005-python-naming-conventions.md
new file mode 100644
index 0000000..3a79b98
--- /dev/null
+++ b/docs/decisions/0005-python-naming-conventions.md
@@ -0,0 +1,70 @@
+---
+status: accepted
+contact: eavanvalkenburg
+date: 2025-09-04
+deciders: markwallace-microsoft, dmytrostruk, peterychang, ekzhu, sphenry
+consulted: taochenosu, alliscode, moonbox3, johanste
+---
+
+# Python naming conventions and renames (ADR)
+
+## Context and Problem Statement
+
+The project has a public .NET surface and a Python surface. During a cross-language alignment effort the community proposed renames to make the Python surface more idiomatic while preserving discoverability and mapping to the .NET names. This ADR captures the final naming decisions (or the proposed ones), the rationale, and the alternatives considered and rejected.
+
+## Decision drivers
+
+- Follow Python naming conventions (PEP 8) where appropriate (snake_case for functions and module-level variables, PascalCase for classes).
+- Preserve conceptual parity with .NET names to make it easy for developers reading both surfaces to correlate types and behaviors.
+- Avoid ambiguous or overloaded names in Python that could conflict with stdlib, common third-party packages, or existing package/module names.
+- Prefer clarity and discoverability in the public API surface over strict symmetry with .NET when Python conventions conflict.
+- Minimize churn and migration burden for existing Python users where backwards compatibility is feasible.
+
+## Principles applied
+
+- Map .NET PascalCase class names to PascalCase Python classes when they represent types.
+- Map .NET method/field names that are camelCase to snake_case in Python where they will be used as functions or module-level attributes.
+- When a .NET name is an acronym or initialism, use Python-friendly casing (e.g., `Http` -> `HTTP` in classes, but acronyms in function names should be lowercased per PEP 8 where sensible).
+- Avoid names that shadow common stdlib modules (e.g., `logging`, `asyncio`) or widely used third-party modules.
+- When multiple reasonable Python names exist, prefer the one that communicates intent most clearly to Python users, and record rejected alternatives in the table with justification.
+
+## Renaming table
+
+The table below represents the majority of the naming changes discussed in issue #506. Each row has:
+- Original and/or .NET name — the canonical name used in dotnet or earlier Python variants.
+- New name — the chosen Python name.
+- Status — accepted if the new name differs from the original, rejected if unchanged.
+- Reasoning — short rationale why the new name was chosen.
+- Rejected alternatives — other candidate new names that were considered and rejected; include the rejected 'new name' values and the reason each was rejected.
+
+| Original and/or .NET name | New name (Python) | Status | Reasoning | Rejected alternatives (as "new name" + reason rejected) |
+|---|---|---|---|---|
+| AIAgent | AgentProtocol | accepted | The AI prefix is meaningless in the context of the Agent Framework, and the `protocol` suffix makes it very clear that this is a protocol, and not a concrete agent implementation. | AgentLike, not seen in many other places, but was a frontrunner. Agent, as too generic. BaseAgent/AbstractAgent, it is not a base/ABC class and should not be treated as such. |
+| ChatClientAgent | ChatAgent | accepted | Type name is shorter, while it is still clear that a ChatClient is used, also by virtue of the first parameter for initialization. | Agent, as too generic. |
+| ChatClient/IChatClient (in dotnet) | ChatClientProtocol | accepted | Keeping this protocol in sync with the AgentProtocol naming. | Similar as AgentProtocol. |
+| ChatClientBase | BaseChatClient | accepted | Following convention, serves as base class so, should be named accordingly. | None |
+| AITool | ToolProtocol | accepted | In line with other protocols. | Tool, too generic. |
+| AIToolBase | BaseTool | accepted | More descriptive than just Tool, while still concise. | AbstractTool/BaseTool, it is not an abstract/base class and should not be treated as such. |
+| ChatRole | Role | accepted | More concise while still clear in context. | None |
+| ChatFinishReason | FinishReason | accepted | More concise while still clear in context. | None |
+| AIContent | BaseContent | accepted | More accurate as it serves as the base class for all content types. | Content, too generic. |
+| AIContents | Contents | accepted | This is the annotated typing object that is the union of all concrete content types, so plural makes sense and since this is used as a type hint, the generic nature of the name is acceptable. | None |
+| AIAnnotations | Annotations | accepted | In sync with contents | None |
+| AIAnnotation | BaseAnnotation | accepted | In sync with contents | None |
+| *Mcp* & *Http* | *MCP* & *HTTP* | accepted | Acronyms should be uppercased in class names, according to PEP 8. | None |
+| `agent.run_streaming` | `agent.run_stream` | accepted | Shorter and more closely aligns with AutoGen and Semantic Kernel names for the same methods. | None |
+| `workflow.run_streaming` | `workflow.run_stream` | accepted | In sync with `agent.run_stream` and shorter and more closely aligns with AutoGen and Semantic Kernel names for the same methods. | None |
+| AgentResponse & AgentResponseUpdate | AgentResponse & AgentResponseUpdate | rejected | Rejected, because it is the response to a run invocation and AgentResponse is too generic. | None |
+| *Content | * | rejected | Rejected other content type renames (removing `Content` suffix) because it would reduce clarity and discoverability. | Item was also considered, but rejected as it is very similar to Content, but would be inconsistent with dotnet. |
+| ChatResponse & ChatResponseUpdate | Response & ResponseUpdate | rejected | Rejected, because Response is too generic. | None |
+
+## Naming guidance
+In general Python tends to prefer shorter names, while .NET tends to prefer more descriptive names. The table above captures the specific renames agreed upon, but in general the following guidelines were applied:
+- Use [PEP 8](https://peps.python.org/pep-0008/) for generic naming conventions (snake_case for functions and module-level variables, PascalCase for classes).
+
+When mapping .NET names to Python:
+- Remove `AI` prefix when appropriate, as it is often redundant in the context of an AI SDK.
+- Remove `Chat` prefix when the context is clear (e.g., Role and FinishReason).
+- Use `Protocol` suffix for interfaces/protocols to clarify their purpose.
+- Use `Base` prefix for base classes that are not abstract but serve as a common ancestor for internal implementations.
+- When readability improves while it is still easy to understand what it does and how it maps to the .NET name, prefer the shorter name.
diff --git a/docs/decisions/0006-userapproval.md b/docs/decisions/0006-userapproval.md
new file mode 100644
index 0000000..7823ab4
--- /dev/null
+++ b/docs/decisions/0006-userapproval.md
@@ -0,0 +1,521 @@
+---
+# These are optional elements. Feel free to remove any of them.
+status: accepted
+contact: westey-m
+date: 2025-09-12 {YYYY-MM-DD when the decision was last updated}
+deciders: sergeymenshykh, markwallace-microsoft, rogerbarreto, dmytrostruk, westey-m, eavanvalkenburg, stephentoub, peterychang
+consulted:
+informed:
+---
+
+# Agent User Approvals Content Types and FunctionCall approvals Design
+
+## Context and Problem Statement
+
+When agents are operating on behalf of a user, there may be cases where the agent requires user approval to continue an operation.
+This is complicated by the fact that an agent may be remote and the user may not immediately be available to provide the approval.
+
+Inference services are also increasingly supporting built-in tools or service side MCP invocation, which may require user approval before the tool can be invoked.
+
+This document aims to provide options and capture the decision on how to model this user approval interaction with the agent caller.
+
+See various features that would need to be supported via this type of mechanism, plus how various other frameworks support this:
+
+- Also see [dotnet issue 6492](https://github.com/dotnet/extensions/issues/6492), which discusses the need for a similar pattern in the context of MCP approvals.
+- Also see [the openai human-in-the-loop guide](https://openai.github.io/openai-agents-js/guides/human-in-the-loop/#approval-requests).
+- Also see [the openai MCP guide](https://openai.github.io/openai-agents-js/guides/mcp/#optional-approval-flow).
+- Also see [MCP Approval Requests from OpenAI](https://platform.openai.com/docs/guides/tools-remote-mcp#approvals).
+- Also see [Azure AI Foundry MCP Approvals](https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/tools/model-context-protocol-samples?pivots=rest#submit-your-approval).
+- Also see [MCP Elicitation requests](https://modelcontextprotocol.io/specification/draft/client/elicitation)
+
+## Decision Drivers
+
+- Agents should encapsulate their internal logic and not leak it to the caller.
+- We need to support approvals for local actions as well as remote actions.
+- We need to support approvals for service-side tool use, such as remote MCP tool invocations
+- We should consider how other user input requests will be modeled, so that we can have a consistent approach for user input requests and approvals.
+
+## Considered Options
+
+### 1. Return a FunctionCallContent to the agent caller, that it executes
+
+This introduces a manual function calling element to agents, where the caller of the agent is expected to invoke the function if the user approves it.
+
+This approach is problematic for a number of reasons:
+
+- This may not work for remote agents (e.g. via A2A), where the function that the agent wants to call does not reside on the caller's machine.
+- The main value prop of an agent is to encapsulate the internal logic of the agent, but this leaks that logic to the caller, requiring the caller to know how to invoke the agent's function calls.
+- Inference services are introducing their own approval content types for server side tool or function invocation, and will not be addressed by this approach.
+
+### 2. Introduce an ApprovalCallback in AgentRunOptions and ChatOptions
+
+This approach allows a caller to provide a callback that the agent can invoke when it requires user approval.
+
+This approach is easy to use when the user and agent are in the same application context, such as a desktop application, where the application can show the approval request to the user and get their response from the callback before continuing the agent run.
+
+This approach does not work well for cases where the agent is hosted in a remote service, and where there is no user available to provide the approval in the same application context.
+For cases like this, the agent needs to be suspended, and a network response must be sent to the client app. After the user provides their approval, the client app must call the service that hosts the agent again, with the user's decision, and the agent needs to be resumed. However, with a callback, the agent is deep in the call stack and cannot be suspended or resumed like this.
+
+```csharp
+class AgentRunOptions
+{
+ public Func>? ApprovalCallback { get; set; }
+}
+
+agent.RunAsync("Please book me a flight for Friday to Paris.", thread, new AgentRunOptions
+{
+ ApprovalCallback = async (approvalRequest) =>
+ {
+ // Show the approval request to the user in the appropriate format.
+ // The user can then approve or reject the request.
+ // The optional FunctionCallContent can be used to show the user what function the agent wants to call with the parameter set:
+ // approvalRequest.FunctionCall?.Arguments.
+
+ // If the user approves:
+ return true;
+ }
+});
+```
+
+### 3. Introduce new ApprovalRequestContent and ApprovalResponseContent types
+
+The agent would return an `ApprovalRequestContent` to the caller, which would then be responsible for getting approval from the user in whatever way is appropriate for the application.
+The caller would then invoke the agent again with an `ApprovalResponseContent` to the agent containing the user decision.
+
+When an agent returns an `ApprovalRequestContent`, the run is finished for the time being, and to continue, the agent must be invoked again with an `ApprovalResponseContent` on the same thread as the original request. This doesn't of course have to be the exact same thread object, but it should have the equivalent contents as the original thread, since the agent would have stored the `ApprovalRequestContent` in its thread state.
+
+The `ApprovalRequestContent` could contain an optional `FunctionCallContent` if the approval is for a function call, along with any additional information that the agent wants to provide to the user to help them make a decision.
+
+It is up to the agent to decide when and if a user approval is required, and therefore when to return an `ApprovalRequestContent`.
+
+`ApprovalRequestContent` and `ApprovalResponseContent` will not necessarily always map to a supported content type for the underlying service or agent thread storage.
+Specifically, when we are deciding in the IChatClient stack to ask for approval from the user, for a function call, this does not mean that the underlying ai service or
+service side thread type (where applicable) supports the concept of a function call approval request. While we can store the approval requests and response in local
+threads, service managed threads won't necessarily support this. For service managed threads, there will therefore be no long term record of the approval request in the chat history.
+We should however log approvals so that there is a trace of this for debugging and auditing purposes.
+
+Suggested Types:
+
+```csharp
+class ApprovalRequestContent : AIContent
+{
+ // An ID to uniquely identify the approval request/response pair.
+ public string Id { get; set; }
+
+ // An optional user targeted message to explain what needs to be approved.
+ public string? Text { get; set; }
+
+ // Optional: If the approval is for a function call, this will contain the function call content.
+ public FunctionCallContent? FunctionCall { get; set; }
+
+ public ApprovalResponseContent CreateApproval()
+ {
+ return new ApprovalResponseContent
+ {
+ Id = this.Id,
+ Approved = true,
+ FunctionCall = this.FunctionCall
+ };
+ }
+
+ public ApprovalResponseContent CreateRejection()
+ {
+ return new ApprovalResponseContent
+ {
+ Id = this.Id,
+ Approved = false,
+ FunctionCall = this.FunctionCall
+ };
+ }
+}
+
+class ApprovalResponseContent : AIContent
+{
+ // An ID to uniquely identify the approval request/response pair.
+ public string Id { get; set; }
+
+ // Indicates whether the user approved the request.
+ public bool Approved { get; set; }
+
+ // Optional: If the approval is for a function call, this will contain the function call content.
+ public FunctionCallContent? FunctionCall { get; set; }
+}
+
+var response = await agent.RunAsync("Please book me a flight for Friday to Paris.", thread);
+while (response.ApprovalRequests.Count > 0)
+{
+ List messages = new List();
+ foreach (var approvalRequest in response.ApprovalRequests)
+ {
+ // Show the approval request to the user in the appropriate format.
+ // The user can then approve or reject the request.
+ // The optional FunctionCallContent can be used to show the user what function the agent wants to call with the parameter set:
+ // approvalRequest.FunctionCall?.Arguments.
+ // The Text property of the ApprovalRequestContent can also be used to show the user any additional textual context about the request.
+
+ // If the user approves:
+ messages.Add(new ChatMessage(ChatRole.User, [approvalRequest.CreateApproval()]));
+ }
+
+ // Get the next response from the agent.
+ response = await agent.RunAsync(messages, thread);
+}
+
+class AgentResponse
+{
+ ...
+
+ // A new property on AgentResponse to aggregate the ApprovalRequestContent items from
+ // the response messages (Similar to the Text property).
+ public IEnumerable ApprovalRequests { get; set; }
+
+ ...
+}
+```
+
+### 4. Introduce new Container UserInputRequestContent and UserInputResponseContent types
+
+This approach is similar to the `ApprovalRequestContent` and `ApprovalResponseContent` types, but is more generic and can be used for any type of user input request, not just approvals.
+
+There is some ambiguity with this approach. When using an LLM based agent the LLM may return a text response about missing user input.
+E.g the LLM may need to invoke a function but the user did not supply all necessary information to fill out all arguments.
+Typically an LLM would just respond with a text message asking the user for the missing information.
+In this case, the message is not distinguishable from any other result message, and therefore cannot be returned to the caller as a `UserInputRequestContent`, even though it is conceptually a type of unstructured user input request. Ultimately our types are modeled to make it easy for callers to decide on the right way to represent this to users. E.g. is it just a regular message to show to users, or do we need a special UX for it.
+
+Suggested Types:
+
+```csharp
+class UserInputRequestContent : AIContent
+{
+ // An ID to uniquely identify the approval request/response pair.
+ public string ApprovalId { get; set; }
+
+ // DecisionTarget could contain:
+ // FunctionCallContent: The function call that the agent wants to invoke.
+ // TextContent: Text that describes the question for that the user should answer.
+ object? DecisionTarget { get; set; } // Anything else the user may need to make a decision about.
+
+ // Possible InputFormat subclasses:
+ // SchemaInputFormat: Contains a schema for the user input.
+ // ApprovalInputFormat: Indicates that the user needs to approve something.
+ // FreeformTextInputFormat: Indicates that the user can provide freeform text input.
+ // Other formats can be added as needed, e.g. cards when using activity protocol.
+ public InputFormat InputFormat { get; set; } // How the user should provide input (e.g., form, options, etc.).
+}
+
+class UserInputResponseContent : AIContent
+{
+ // An ID to uniquely identify the approval request/response pair.
+ public string ApprovalId { get; set; }
+
+ // Possible UserInputResult subclasses:
+ // SchemaInputResult: Contains the structured data provided by the user.
+ // ApprovalResult: Contains a bool with approved / rejected.
+ // FreeformTextResult: Contains the freeform text input provided by the user.
+ public UserInputResult Result { get; set; } // The user input.
+
+ public object? DecisionTarget { get; set; } // A copy of the DecisionTarget from the UserInputRequestContent, if applicable.
+}
+
+var response = await agent.RunAsync("Please book me a flight for Friday to Paris.", thread);
+while (response.UserInputRequests.Any())
+{
+ List messages = new List();
+ foreach (var userInputRequest in response.UserInputRequests)
+ {
+ // Show the user input request to the user in the appropriate format.
+ // The DecisionTarget can be used to show the user what function the agent wants to call with the parameter set.
+ // The InputFormat property can be used to determine the type of UX when allowing users to provide input.
+
+ if (userInputRequest.InputFormat is ApprovalInputFormat approvalInputFormat)
+ {
+ // Here we need to show the user an approval request.
+ // We can use the DecisionTarget to show e.g. the function call that the agent wants to invoke.
+ // The user can then approve or reject the request.
+
+ // If the user approves:
+ var approvalMessage = new ChatMessage(ChatRole.User, new UserInputResponseContent {
+ ApprovalId = userInputRequest.ApprovalId,
+ Result = new ApprovalResult { Approved = true },
+ DecisionTarget = userInputRequest.DecisionTarget
+ });
+ messages.Add(approvalMessage);
+ }
+ else
+ {
+ throw new NotSupportedException("Unsupported InputFormat type.");
+ }
+ }
+
+ // Get the next response from the agent.
+ response = await agent.RunAsync(messages, thread);
+}
+
+class AgentResponse
+{
+ ...
+
+ // A new property on AgentResponse to aggregate the UserInputRequestContent items from
+ // the response messages (Similar to the Text property).
+ public IReadOnlyList UserInputRequests { get; set; }
+
+ ...
+}
+```
+
+### 5. Introduce new Base UserInputRequestContent and UserInputResponseContent types
+
+This approach is similar to option 4, but the `UserInputRequestContent` and `UserInputResponseContent` types are base classes rather than generic container types.
+
+Suggested Types:
+
+```csharp
+class UserInputRequestContent : AIContent
+{
+ // An ID to uniquely identify the approval request/response pair.
+ public string Id { get; set; }
+}
+
+class UserInputResponseContent : AIContent
+{
+ // An ID to uniquely identify the approval request/response pair.
+ public string Id { get; set; }
+}
+
+// -----------------------------------
+// Used for approving a function call.
+class FunctionApprovalRequestContent : UserInputRequestContent
+{
+ // Contains the function call that the agent wants to invoke.
+ public FunctionCallContent FunctionCall { get; set; }
+
+ public ApprovalResponseContent CreateApproval()
+ {
+ return new ApprovalResponseContent
+ {
+ Id = this.Id,
+ Approved = true,
+ FunctionCall = this.FunctionCall
+ };
+ }
+
+ public ApprovalResponseContent CreateRejection()
+ {
+ return new ApprovalResponseContent
+ {
+ Id = this.Id,
+ Approved = false,
+ FunctionCall = this.FunctionCall
+ };
+ }
+}
+class FunctionApprovalResponseContent : UserInputResponseContent
+{
+ // Indicates whether the user approved the request.
+ public bool Approved { get; set; }
+
+ // Contains the function call that the agent wants to invoke.
+ public FunctionCallContent FunctionCall { get; set; }
+}
+
+// --------------------------------------------------
+// Used for approving a request described using text.
+class TextApprovalRequestContent : UserInputRequestContent
+{
+ // A user targeted message to explain what needs to be approved.
+ public string Text { get; set; }
+}
+class TextApprovalResponseContent : UserInputResponseContent
+{
+ // Indicates whether the user approved the request.
+ public bool Approved { get; set; }
+}
+
+// ------------------------------------------------
+// Used for providing input in a structured format.
+class StructuredDataInputRequestContent : UserInputRequestContent
+{
+ // A user targeted message to explain what is being requested.
+ public string? Text { get; set; }
+
+ // Contains the schema for the user input.
+ public JsonElement Schema { get; set; }
+}
+class StructuredDataInputResponseContent : UserInputResponseContent
+{
+ // Contains the structured data provided by the user.
+ public JsonElement StructuredData { get; set; }
+}
+
+var response = await agent.RunAsync("Please book me a flight for Friday to Paris.", thread);
+while (response.UserInputRequests.Any())
+{
+ List messages = new List();
+ foreach (var userInputRequest in response.UserInputRequests)
+ {
+ if (userInputRequest is FunctionApprovalRequestContent approvalRequest)
+ {
+ // Here we need to show the user an approval request.
+ // We can use the FunctionCall property to show e.g. the function call that the agent wants to invoke.
+ // If the user approves:
+ messages.Add(new ChatMessage(ChatRole.User, approvalRequest.CreateApproval()));
+ }
+ }
+
+ // Get the next response from the agent.
+ response = await agent.RunAsync(messages, thread);
+}
+
+class AgentResponse
+{
+ ...
+
+ // A new property on AgentResponse to aggregate the UserInputRequestContent items from
+ // the response messages (Similar to the Text property).
+ public IEnumerable UserInputRequests { get; set; }
+
+ ...
+}
+```
+
+## Decision Outcome
+
+Chosen option 5.
+
+## Appendices
+
+### ChatClientAgent Approval Process Flow
+
+1. User passes a User message to the agent with a request.
+1. Agent calls IChatClient with any functions registered on the agent.
+ (IChatClient has FunctionInvokingChatClient)
+1. Model responds with FunctionCallContent indicating function calls required.
+1. FunctionInvokingChatClient decorator identifies any function calls that require user approval and returns an FunctionApprovalRequestContent.
+ (If there are multiple parallel function calls, all function calls will be returned as FunctionApprovalRequestContent even if only some require approval.)
+1. Agent updates the thread with the FunctionApprovalRequestContent (or this may have already been done by a service threaded agent).
+1. Agent returns the FunctionApprovalRequestContent to the caller which shows it to the user in the appropriate format.
+1. User (via caller) invokes the agent again with FunctionApprovalResponseContent.
+1. Agent adds the FunctionApprovalResponseContent to the thread.
+1. Agent calls IChatClient with the provided FunctionApprovalResponseContent.
+1. Agent invokes IChatClient with FunctionApprovalResponseContent and the FunctionInvokingChatClient decorator identifies the response as an approval for the function call.
+ Any rejected approvals are converted to FunctionResultContent with a message indicating that the function invocation was denied.
+ Any approved approvals are executed by the FunctionInvokingChatClient decorator.
+1. FunctionInvokingChatClient decorator passes the FunctionCallContent and FunctionResultContent for the approved and rejected function calls to the model.
+1. Model responds with the result.
+1. FunctionInvokingChatClient returns the FunctionCallContent, FunctionResultContent, and the result message to the agent.
+1. Agent responds to caller with the same messages and updates the thread with these as well.
+
+### CustomAgent Approval Process Flow
+
+1. User passes a User message to the agent with a request.
+1. Agent adds this message to the thread.
+1. Agent executes various steps.
+1. Agent encounters a step for which it requires user input to continue.
+1. Agent responds with an UserInputRequestContent and also adds it to its thread.
+1. User (via caller) invokes the agent again with UserInputResponseContent.
+1. Agent adds the UserInputResponseContent to the thread.
+1. Agent responds to caller with result message and thread is updated with the result message.
+
+### Sequence Diagram: FunctionInvokingChatClient with built in Approval Generation
+
+This is a ChatClient Approval Stack option has been proven to work via a proof of concept implementation.
+
+```mermaid
+---
+title: Multiple Functions with partial approval
+---
+
+sequenceDiagram
+ note right of Developer: Developer asks question with two functions.
+ Developer->>+FunctionInvokingChatClient: What is the special soup today? [GetMenu, GetSpecials]
+ FunctionInvokingChatClient->>+ResponseChatClient: What is the special soup today? [GetMenu, GetSpecials]
+
+ ResponseChatClient-->>-FunctionInvokingChatClient: [FunctionCallContent(GetMenu)], [FunctionCallContent(GetSpecials)]
+ note right of FunctionInvokingChatClient: FICC turns FunctionCallContent into FunctionApprovalRequestContent
+ FunctionInvokingChatClient->>+Developer: [FunctionApprovalRequestContent(GetMenu)] [FunctionApprovalRequestContent(GetSpecials)]
+
+ note right of Developer:Developer asks user for approval
+ Developer->>+FunctionInvokingChatClient: [FunctionApprovalRequestContent(GetMenu, approved=false)] [FunctionApprovalRequestContent(GetSpecials, approved=true)]
+ note right of FunctionInvokingChatClient:FunctionInvokingChatClient executes the approved function and generates a failed FunctionResultContent for the rejected one, before invoking the model again.
+ FunctionInvokingChatClient->>+ResponseChatClient: What is the special soup today? [FunctionCallContent(GetMenu)], [FunctionCallContent(GetSpecials)], [FunctionResultContent(GetMenu, Function invocation denied")] [FunctionResultContent(GetSpecials, "Special Soup: Clam Chowder...")]
+
+ ResponseChatClient-->>-FunctionInvokingChatClient: [TextContent("The specials soup is...")]
+ FunctionInvokingChatClient->>+Developer: [FunctionCallContent(GetMenu)], [FunctionCallContent(GetSpecials)], [FunctionResultContent(GetMenu, Function invocation denied")] [FunctionResultContent(GetSpecials, "Special Soup: Clam Chowder...")] [TextContent("The specials soup is...")]
+```
+
+### Sequence Diagram: Post FunctionInvokingChatClient ApprovalGeneratingChatClient - Multiple function calls with partial approval
+
+This is a discarded ChatClient Approval Stack option, but is included here for reference.
+
+```mermaid
+---
+title: Multiple Functions with partial approval
+---
+
+sequenceDiagram
+ note right of Developer: Developer asks question with two functions.
+ Developer->>+FunctionInvokingChatClient: What is the special soup today? [GetMenu, GetSpecials]
+ FunctionInvokingChatClient->>+ApprovalGeneratingChatClient: What is the special soup today? [GetMenu, GetSpecials]
+ ApprovalGeneratingChatClient->>+ResponseChatClient: What is the special soup today? [GetMenu, GetSpecials]
+
+ ResponseChatClient-->>-ApprovalGeneratingChatClient: [FunctionCallContent(GetMenu)], [FunctionCallContent(GetSpecials)]
+ ApprovalGeneratingChatClient-->>-FunctionInvokingChatClient: [FunctionApprovalRequestContent(GetMenu)], [FunctionApprovalRequestContent(GetSpecials)]
+ FunctionInvokingChatClient-->>-Developer: [FunctionApprovalRequestContent(GetMenu)] [FunctionApprovalRequestContent(GetSpecials)]
+
+ note right of Developer: Developer approves one function call and rejects the other.
+ Developer->>+FunctionInvokingChatClient: [FunctionApprovalResponseContent(GetMenu, approved=true)] [FunctionApprovalResponseContent(GetSpecials, approved=false)]
+ FunctionInvokingChatClient->>+ApprovalGeneratingChatClient: [FunctionApprovalResponseContent(GetMenu, approved=true)] [FunctionApprovalResponseContent(GetSpecials, approved=false)]
+
+ note right of FunctionInvokingChatClient: ApprovalGeneratingChatClient only returns FunctionCallContent for approved FunctionApprovalResponseContent.
+ ApprovalGeneratingChatClient-->>-FunctionInvokingChatClient: [FunctionCallContent(GetMenu)]
+ note right of FunctionInvokingChatClient: FunctionInvokingChatClient has to also include all FunctionApprovalResponseContent in the new downstream request.
+ FunctionInvokingChatClient->>+ApprovalGeneratingChatClient: [FunctionResultContent(GetMenu, "mains.... deserts...")] [FunctionApprovalResponseContent(GetMenu, approved=true)] [FunctionApprovalResponseContent(GetSpecials, approved=false)]
+
+ note right of ApprovalGeneratingChatClient: ApprovalGeneratingChatClient now throws away approvals for executed functions, and creates failed FunctionResultContent for denied function calls.
+ ApprovalGeneratingChatClient->>+ResponseChatClient: [FunctionResultContent(GetMenu, "mains.... deserts...")] [FunctionResultContent(GetSpecials, "Function invocation denied")]
+```
+
+### Sequence Diagram: Pre FunctionInvokingChatClient ApprovalGeneratingChatClient - Multiple function calls with partial approval
+
+This is a discarded ChatClient Approval Stack option, but is included here for reference.
+
+It doesn't work for the scenario where we have multiple function calls for the same function in serial with different arguments.
+
+Flow:
+
+- AGCC turns AIFunctions into AIFunctionDefinitions (not invocable) and FICC ignores these.
+- We get back a FunctionCall for one of these and it gets approved.
+- We invoke the FICC again, this time with an AIFunction.
+- We call the service with the FCC and FRC.
+- We get back a new Function call for the same function again with different arguments.
+- Since we were passed an AIFunction instead of an AIFunctionDefinition, we now incorrectly execute this FC without approval.
+
+```mermaid
+---
+title: Multiple Functions with partial approval
+---
+
+sequenceDiagram
+ note right of Developer: Developer asks question with two functions.
+ Developer->>+ApprovalGeneratingChatClient: What is the special soup today? [GetMenu, GetSpecials]
+ note right of ApprovalGeneratingChatClient: AGCC marks functions as not-invocable
+ ApprovalGeneratingChatClient->>+FunctionInvokingChatClient: What is the special soup today? [GetMenu(invocable=false)] [GetSpecials(invocable=false)]
+ FunctionInvokingChatClient->>+ResponseChatClient: What is the special soup today? [GetMenu(invocable=false)] [GetSpecials(invocable=false)]
+
+ ResponseChatClient-->>-FunctionInvokingChatClient: [FunctionCallContent(GetMenu)], [FunctionCallContent(GetSpecials)]
+ note right of FunctionInvokingChatClient: FICC doesn't invoke functions since they are not invocable.
+ FunctionInvokingChatClient-->>-ApprovalGeneratingChatClient: [FunctionCallContent(GetMenu)], [FunctionCallContent(GetSpecials)]
+ note right of ApprovalGeneratingChatClient: AGCC turns functions into approval requests
+ ApprovalGeneratingChatClient-->>-Developer: [FunctionApprovalRequestContent(GetMenu)] [FunctionApprovalRequestContent(GetSpecials)]
+
+ note right of Developer: Developer approves one function call and rejects the other.
+ Developer->>+ApprovalGeneratingChatClient: [FunctionApprovalResponseContent(GetMenu, approved=true)] [FunctionApprovalResponseContent(GetSpecials, approved=false)]
+ note right of ApprovalGeneratingChatClient: AGCC turns turns approval requests into FCC or failed function calls
+ ApprovalGeneratingChatClient->>+FunctionInvokingChatClient: [FunctionCallContent(GetMenu)] [FunctionCallContent(GetSpecials) [FunctionResultContent(GetSpecials, "Function invocation denied"))]
+ note right of FunctionInvokingChatClient: FICC invokes GetMenu since it's the only remaining one.
+ FunctionInvokingChatClient->>+ResponseChatClient: [FunctionCallContent(GetMenu)] [FunctionResultContent(GetMenu, "mains.... deserts...")] [FunctionCallContent(GetSpecials) [FunctionResultContent(GetSpecials, "Function invocation denied"))]
+
+ ResponseChatClient-->>-FunctionInvokingChatClient: [FunctionCallContent(GetMenu)] [FunctionResultContent(GetMenu, "mains.... deserts...")] [FunctionCallContent(GetSpecials) [FunctionResultContent(GetSpecials, "Function invocation denied"))] [TextContent("The specials soup is...")]
+ FunctionInvokingChatClient-->>-ApprovalGeneratingChatClient: [FunctionCallContent(GetMenu)] [FunctionResultContent(GetMenu, "mains.... deserts...")] [FunctionCallContent(GetSpecials) [FunctionResultContent(GetSpecials, "Function invocation denied"))] [TextContent("The specials soup is...")]
+ ApprovalGeneratingChatClient-->>-Developer: [FunctionCallContent(GetMenu)] [FunctionResultContent(GetMenu, "mains.... deserts...")] [FunctionCallContent(GetSpecials) [FunctionResultContent(GetSpecials, "Function invocation denied"))] [TextContent("The specials soup is...")]
+```
diff --git a/docs/decisions/0007-agent-filtering-middleware.md b/docs/decisions/0007-agent-filtering-middleware.md
new file mode 100644
index 0000000..dbdd6d3
--- /dev/null
+++ b/docs/decisions/0007-agent-filtering-middleware.md
@@ -0,0 +1,1190 @@
+---
+status: proposed
+contact: rogerbarreto
+date: 2025-09-15
+deciders: markwallace-microsoft, rogerbarreto, westey-m, dmytrostruk, sergeymenshykh
+informed: {}
+---
+
+# Agent Filtering Middleware Design
+
+## Context and Problem Statement
+
+The current Agent Framework lacks a standardized, extensible mechanism for intercepting and processing agent execution. Developers need the ability to add custom filters/middleware to intercept and modify agent behavior at various stages of the execution pipeline. While the framework has basic agent abstractions with `RunAsync` and `RunStreamingAsync` methods, and standards like approval workflows, there is no middleware that allows developers to intercept and modify agent behavior at different agent execution contexts.
+
+The challenge is to design an architecture that supports:
+- Multiple execution contexts (invocation, function calls, approval requests, error handling)
+- Support for both streaming and non-streaming scenarios
+- Dependency injection friendly setup
+
+## Decision Drivers
+
+- Agents should be able to intercept and modify agent behavior at various stages of the execution pipeline.
+- The design should be simple and intuitive for developers to understand and use.
+- The design should be extensible to support new execution contexts and scenarios.
+- The design should support both manual and dependency injection configuration.
+- The design should allow flexible custom behaviors provided by enough context information.
+- The design should be exception friendly and allow clear error handling and recovery mechanisms.
+
+## Other AI Agent Framework Analysis
+
+This section provides an analysis of how other major AI agent frameworks handle filtering, middleware, hooks, or similar interception capabilities. The goal is to identify ubiquitous language, design patterns, and approaches that could inform our Agent Middleware design also providing valuable insights into achieving a more idiomatic designs.
+
+### Overview Comparison Table
+
+| Provider | Language | Supports (Y/N) | Naming | TL;DR Observation |
+|---------------------------|----------|----------------|---------------------------------|------------------------|
+| LangChain (Python) | Python | Y (read) | Callbacks (BaseCallbackHandler) | Uses observer pattern with event methods for interception (e.g., on_chain_start); supports agent actions and errors; handlers can read inputs/outputs and modification is limited to the parameters or by raising exceptions to influence flow. [Details](#langchain) |
+| LangChain (JS) | JS | Y (read/write) | Callbacks (BaseCallbackHandler) | Similar observer pattern to Python, with event methods adapted for JS async handling; supports chain/agent interception; handlers can read inputs/outputs and modify metadata or raise exceptions to influence flow. [Details](#langchain) |
+| LangChain | JS/Python/TS | Y (read/write) | Middleware | Middleware concept was recently introduced in LangChain 1.0 alpha; [Details](https://blog.langchain.com/agent-middleware/) |
+| LangGraph | Python | Y (read/write) | Hooks/Callbacks (inherited from LangChain) | Event-driven with runtime handlers; integrates callbacks for observability in graphs; inherits LangChain's ability to read/modify metadata or interrupt execution. [Details](#langgraph) |
+| AutoGen (Python) | Python | Y (read/write) | Reply Functions (register_reply) | Reply functions intercept and process messages; middleware-like for agent replies; can directly modify messages or replies before continuing. [Details](#autogen) |
+| AutoGen (C#) | C# | Y (read/write) | Middleware (MiddlewareAgent) | Decorator/wrapper with middleware delegates for message modification; delegates can read and alter message content or options. [Details](#autogen) |
+| Semantic Kernel (C#) | C# | Y (read/write) | Filters (IFunctionInvocationFilter, etc.) | Interface-based middleware pattern for function/prompt interception; filters can read and modify context, arguments, or results. [Details](#semantic-kernel) |
+| Semantic Kernel (Python) | Python | Y (read/write) | Filters (add_filter, @kernel.filter decorator) | Function and decorator-based for interception; no explicit interfaces like C#, focuses on async functions for filters; can read and modify context/arguments/results. [Details](#semantic-kernel) |
+| CrewAI | Python | Y (read) | Events/Callbacks (BaseEventListener) | Event-driven orchestration with listeners for workflows; listeners can observe events (e.g., read source/event data) but are primarily for logging/reactions without direct modification of workflow state. [Details](#crewai) |
+| LlamaIndex | Python | Y (read) | Callbacks (CallbackManager) | Observer pattern with event methods for queries and tools; handlers can observe events/payloads (e.g., read prompts/responses) but are designed for debugging/tracing without modifying execution context. [Details](#llamaindex) |
+| Haystack | Python | N (Pipeline-based interception) | N/A (Pipeline Components/Routers) | Relies on modular pipelines for implicit interception but lacks explicit middleware/filters; custom components can read/write data flow via routing/transformations, but this is compositional rather than hook-based interception. [Details](#haystack) |
+| OpenAI Swarm | Python | N | N/A | No explicit middleware/filters; interception requires custom wrappers or manual handling (e.g., function decorators, client subclassing), lacking native framework support for built-in components to accept such modifications. [Details](#openai-swarm) |
+| Atomic Agents | Python | N | N/A (Composable Components) | No explicit middleware/filters; modularity allows composable units but no dedicated interception hooks or callbacks for custom reading/modification mid-execution. [Details](#atomic-agents) |
+| Smolagents (Hugging Face)| Python | N | N/A | No explicit support; focuses on simple agent building without interception mechanisms or hooks for reading/modifying execution. [Details](#smolagents-hugging-face) |
+| Phidata (Agno) | Python | N | N/A | No explicit middleware/filters; agents use tools/memory but no interception hooks for custom reading/modification of calls. [Details](#phidata-agno) |
+| PromptFlow (Microsoft) | Python | N (Tracing only) | Tracing | Supports tracing for LLM interactions, acting as callbacks for debugging/iteration; tracing is read-only for observability/telemetry without options to modify context or intercept calls beyond logging. [Details](#promptflow-microsoft) |
+| n8n | JS/TS | Y (read/write) | Callbacks (inherited from LangChain) | AI Agent node uses LangChain under the hood, inheriting callbacks for observability; supports reading/modifying metadata or interrupting flow as in LangChain. [Details](#n8n) |
+
+## Considered Options
+
+### Option 1: Semantic Kernel Approach
+
+Similar to the Semantic Kernel kernel filters this option involves exposing different interface and properties for each specialized filter.
+
+```csharp
+
+var services = new ServiceCollection();
+services.AddSingleton();
+services.AddSingleton();
+
+// Using DI
+var agent = new MyAgent(services.BuildServiceProvider());
+
+// Manual
+var agent = new MyAgent();
+agent.RunFilters.Add(new MyAgentRunFilter());
+agent.FunctionCallFilters.Add(new MyAgentFunctionCallFilter());
+
+public class MyAgentRunFilter : IAgentRunFilter
+{
+ public async Task OnRunAsync(AgentRunContext context, Func next, CancellationToken cancellationToken = default)
+ {
+ // Pre-run logic
+
+ await next(context);
+
+ // Post-run logic
+ }
+}
+
+public interface IAgentRunFilter
+{
+ Task OnRunAsync(AgentRunContext context, Func next, CancellationToken cancellationToken = default);
+}
+
+public interface IAgentFunctionCallFilter
+{
+ Task OnFunctionCallAsync(AgentFunctionCallContext context, Func next, CancellationToken cancellationToken = default);
+}
+
+public class AIAgent
+{
+ private readonly AgentFilterProcessor _filterProcessor;
+
+ public AIAgent(AgentFilterProcessor? filterProcessor = null)
+ {
+ _filterProcessor = filterProcessor ?? new AgentFilterProcessor();
+ }
+
+ public AIAgent(IServiceProvider serviceProvider)
+ {
+ _filterProcessor = serviceProvider.GetService() ?? new AgentFilterProcessor();
+
+ // Auto-register filters from DI
+ var filters = serviceProvider.GetServices();
+ foreach (var filter in filters)
+ {
+ _filterProcessor.AddFilter(filter);
+ }
+ }
+
+ public async Task RunAsync(
+ IReadOnlyCollection messages,
+ AgentThread? thread = null,
+ AgentRunOptions? options = null,
+ CancellationToken cancellationToken = default)
+ {
+ var context = new AgentRunContext(messages, thread, options);
+
+ // Process through filter pipeline using the same pattern as Semantic Kernel
+ await _filterProcessor.ProcessAsync(context, async ctx =>
+ {
+ // Core agent logic - implement actual agent execution here
+ var response = await this.ExecuteCoreLogicAsync(ctx.Messages, ctx.Thread, ctx.Options, cancellationToken);
+ ctx.Response = response;
+ }, cancellationToken);
+
+ // Extract the response from the context
+ return context.Response ?? throw new InvalidOperationException("Agent execution did not produce a response");
+ }
+
+ protected abstract Task ExecuteCoreLogicAsync(
+ IReadOnlyCollection messages,
+ AgentThread? thread,
+ AgentRunOptions? options,
+ CancellationToken cancellationToken);
+}
+
+```
+#### Pros
+- Clean separation of concerns
+- Follows established patterns in Semantic Kernel and easy migration path
+- No resistance or complaints from the community when used in Semantic Kernel
+- Composable and reusable filter components
+
+#### Cons
+- Adding more filters may require adding more properties to the agent class.
+- Filters are not always used, and adding this responsibility to the `AIAgent` abstraction level, may be an overkill.
+
+### Option 2: Agent Filter Decorator Pattern
+
+Similar to the `OpenTelemetryAgent` and the `DelegatingChatClient` in `Microsoft.Extensions.AI`, this option involves creating decorator agents that wrap the inner agent and allow interception of method calls. The current POC implementation demonstrates two approaches:
+
+#### 2a. Direct Decorator Implementation (GuardrailCallbackAgent)
+
+```csharp
+// Current POC implementation from samples
+var agent = persistentAgentsClient.CreateAIAgent(model).AsBuilder()
+ .Use((innerAgent) => new GuardrailCallbackAgent(innerAgent)) // Decoration based agent run handling
+ .Use(async (context, next) => // Context based handling
+ {
+ // Guardrail: Filter input messages for PII
+ context.Messages = context.Messages.Select(m => new ChatMessage(m.Role, FilterPii(m.Text))).ToList();
+ Console.WriteLine($"Pii Middleware - Filtered messages: {new ChatResponse(context.Messages).Text}");
+
+ await next(context);
+
+ if (!context.IsStreaming)
+ {
+ // Guardrail: Filter output messages for PII
+ context.Messages = context.Messages.Select(m => new ChatMessage(m.Role, FilterPii(m.Text))).ToList();
+ }
+ else
+ {
+ context.SetRawResponse(StreamingPiiDetectionAsync(context.RunStreamingResponse!));
+ }
+ })
+ .Build();
+
+// Direct decorator implementation
+internal sealed class GuardrailCallbackAgent : DelegatingAIAgent
+{
+ private readonly string[] _forbiddenKeywords = { "harmful", "illegal", "violence" };
+
+ public GuardrailCallbackAgent(AIAgent innerAgent) : base(innerAgent) { }
+
+ public override async Task RunAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
+ {
+ var filteredMessages = this.FilterMessages(messages);
+ Console.WriteLine($"Guardrail Middleware - Filtered messages: {new ChatResponse(filteredMessages).Text}");
+
+ var response = await this.InnerAgent.RunAsync(filteredMessages, thread, options, cancellationToken);
+
+ response.Messages = response.Messages.Select(m => new ChatMessage(m.Role, this.FilterContent(m.Text))).ToList();
+
+ return response;
+ }
+
+ public override async IAsyncEnumerable RunStreamingAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ var filteredMessages = this.FilterMessages(messages);
+ await foreach (var update in this.InnerAgent.RunStreamingAsync(filteredMessages, thread, options, cancellationToken))
+ {
+ if (update.Text != null)
+ {
+ yield return new AgentResponseUpdate(update.Role, this.FilterContent(update.Text));
+ }
+ else
+ {
+ yield return update;
+ }
+ }
+ }
+
+ private List FilterMessages(IEnumerable messages)
+ {
+ return messages.Select(m => new ChatMessage(m.Role, this.FilterContent(m.Text))).ToList();
+ }
+
+ private string FilterContent(string content)
+ {
+ foreach (var keyword in this._forbiddenKeywords)
+ {
+ if (content.Contains(keyword, StringComparison.OrdinalIgnoreCase))
+ {
+ return "[REDACTED: Forbidden content]";
+ }
+ }
+ return content;
+ }
+}
+```
+
+#### 2b. Context-Based Middleware (RunningCallbackHandlerAgent)
+
+The POC also includes a context-based approach using `RunningCallbackHandlerAgent` that wraps the agent and provides a context object for middleware processing:
+
+```csharp
+// Internal implementation that supports the .Use() pattern
+internal sealed class RunningCallbackHandlerAgent : DelegatingAIAgent
+{
+ private readonly Func, Task> _func;
+
+ internal RunningCallbackHandlerAgent(AIAgent innerAgent, Func, Task> func) : base(innerAgent)
+ {
+ this._func = func;
+ }
+
+ public override async Task RunAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
+ {
+ var context = new AgentInvokeCallbackContext(this, messages, thread, options, isStreaming: false, cancellationToken);
+
+ async Task CoreLogicAsync(AgentInvokeCallbackContext ctx)
+ {
+ var response = await this.InnerAgent.RunAsync(ctx.Messages, ctx.Thread, ctx.Options, ctx.CancellationToken);
+ ctx.SetRawResponse(response);
+ }
+
+ await this._func(context, CoreLogicAsync);
+
+ return context.RunResponse!;
+ }
+}
+```
+
+#### 2c. Function Invocation Filtering
+
+The POC also demonstrates function invocation filtering using a similar decorator pattern:
+
+```csharp
+// Function invocation middleware using .Use() pattern
+var agent = persistentAgentsClient.CreateAIAgent(model)
+ .AsBuilder()
+ .Use((functionInvocationContext, next, ct) =>
+ {
+ Console.WriteLine($"IsStreaming: {functionInvocationContext!.IsStreaming}");
+ return next(functionInvocationContext.Arguments, ct);
+ })
+ .Use((functionInvocationContext, next, ct) =>
+ {
+ Console.WriteLine($"City Name: {(functionInvocationContext!.Arguments.TryGetValue("location", out var location) ? location : "not provided")}");
+ return next(functionInvocationContext.Arguments, ct);
+ })
+ .Build();
+```
+
+This demonstrates that the current POC supports both agent-level and function-level filtering through consistent patterns.
+
+#### Pros
+- Clean separation of concerns
+- Follows established patterns in `Microsoft.Extensions.AI` (DelegatingChatClient, OpenTelemetryAgent)
+- Non-intrusive to existing agent implementations
+- Supports both manual and DI configuration through builder pattern
+- Context-specific processing middleware with `AgentInvokeCallbackContext`
+- Composable and reusable filter components
+- Flexible implementation allowing both direct decorators and context-based middleware
+- Seamless integration with builder pattern using `.Use()` method
+- Support for both streaming and non-streaming scenarios
+- Rich context object providing access to messages, thread, options, and response handling
+
+### Option 3: Dedicated Processor Component for Middleware
+
+This approach involves creating a dedicated `CallbackMiddlewareProcessor` that manages collections of `ICallbackMiddleware` instances. The current POC implementation demonstrates this pattern with the `CallbackEnabledAgent` and processor architecture.
+
+#### Current POC Implementation
+
+```csharp
+// Current POC usage from samples
+var agent = persistentAgentsClient.CreateAIAgent(model)
+ .AsBuilder()
+ .UseCallbacks(config =>
+ {
+ config.AddCallback(new PiiDetectionMiddleware());
+ config.AddCallback(new GuardrailCallbackMiddleware());
+ }).Build();
+
+// Middleware implementation
+internal sealed class PiiDetectionMiddleware : CallbackMiddleware
+{
+ public override async Task OnProcessAsync(AgentInvokeCallbackContext context, Func next, CancellationToken cancellationToken)
+ {
+ // Guardrail: Filter input messages for PII
+ context.Messages = context.Messages.Select(m => new ChatMessage(m.Role, FilterPii(m.Text))).ToList();
+ Console.WriteLine($"Pii Middleware - Filtered messages: {new ChatResponse(context.Messages).Text}");
+ await next(context);
+
+ if (!context.IsStreaming)
+ {
+ // Guardrail: Filter output messages for PII
+ context.Messages = context.Messages.Select(m => new ChatMessage(m.Role, FilterPii(m.Text))).ToList();
+ }
+ else
+ {
+ context.SetRawResponse(StreamingPiiDetectionAsync(context.RunStreamingResponse!));
+ }
+ }
+
+ private static string FilterPii(string content)
+ {
+ // PII detection logic...
+ }
+}
+
+internal sealed class GuardrailCallbackMiddleware : CallbackMiddleware
+{
+ private readonly string[] _forbiddenKeywords = { "harmful", "illegal", "violence" };
+
+ public override async Task OnProcessAsync(AgentInvokeCallbackContext context, Func next, CancellationToken cancellationToken)
+ {
+ // Guardrail: Filter input messages for forbidden content
+ context.Messages = this.FilterMessages(context.Messages);
+ Console.WriteLine($"Guardrail Middleware - Filtered messages: {new ChatResponse(context.Messages).Text}");
+
+ await next(context);
+ if (!context.IsStreaming)
+ {
+ // Guardrail: Filter output messages for forbidden content
+ context.Messages = this.FilterMessages(context.Messages);
+ }
+ else
+ {
+ context.SetRawResponse(StreamingGuardRailAsync(context.RunStreamingResponse!));
+ }
+ }
+}
+```
+
+#### Function Invocation Filtering
+
+The POC also demonstrates function invocation filtering using the processor pattern:
+
+```csharp
+// Processor-based function invocation middleware
+var agent = persistentAgentsClient.CreateAIAgent(model)
+ .AsBuilder()
+ .UseCallbacks(config =>
+ {
+ config.AddCallback(new UsedApiFunctionInvocationCallback());
+ config.AddCallback(new CityInformationFunctionInvocationCallback());
+ }).Build();
+
+internal sealed class UsedApiFunctionInvocationCallback : CallbackMiddleware
+{
+ public override async Task OnProcessAsync(AgentFunctionInvocationCallbackContext context, Func next, CancellationToken cancellationToken)
+ {
+ Console.WriteLine($"IsStreaming: {context!.IsStreaming}");
+
+ await next(context);
+ }
+}
+
+internal sealed class CityInformationFunctionInvocationCallback : CallbackMiddleware
+{
+ public override async Task OnProcessAsync(AgentFunctionInvocationCallbackContext context, Func next, CancellationToken cancellationToken)
+ {
+ Console.WriteLine($"City Name: {(context!.Arguments.TryGetValue("location", out var location) ? location : "not provided")}");
+ await next(context);
+ }
+}
+```
+
+This demonstrates that the current POC supports both agent-level and function-level filtering through consistent patterns.
+
+#### Processor Implementation
+
+The `CallbackMiddlewareProcessor` manages the filter pipeline and chain execution:
+
+```csharp
+public sealed class CallbackMiddlewareProcessor
+{
+ // For thread-safety when used as a Singleton
+ private readonly ConcurrentBag _agentCallbacks = [];
+
+ public CallbackMiddlewareProcessor(IEnumerable? callbacks = null)
+ {
+ if (callbacks is not null)
+ {
+ foreach (var callback in callbacks)
+ {
+ AddCallback(callback);
+ }
+ }
+ }
+
+ internal CallbackMiddlewareProcessor AddCallback(ICallbackMiddleware middleware)
+ {
+ switch (middleware)
+ {
+ case CallbackMiddleware:
+ this._agentCallbacks.Add(middleware);
+ break;
+ default:
+ throw new ArgumentException($"The middleware type '{middleware.GetType().FullName}' is not supported.", nameof(middleware));
+ }
+
+ return this;
+ }
+
+ public async Task ProcessAsync(TContext context, Func coreLogic, CancellationToken cancellationToken = default)
+ where TContext : CallbackContext
+ {
+ var applicableCallbacks = this.GetApplicableCallbacks().ToList();
+ await this.InvokeChainAsync(context, applicableCallbacks, 0, coreLogic, cancellationToken);
+ }
+
+ private IEnumerable GetApplicableCallbacks()
+ where TContext : CallbackContext
+ {
+ return this._agentCallbacks.Where(callback => callback.CanProcess());
+ }
+}
+```
+
+#### CallbackEnabledAgent Implementation
+
+```csharp
+public sealed class CallbackEnabledAgent : DelegatingAIAgent
+{
+ private readonly CallbackMiddlewareProcessor _callbacksProcessor;
+
+ public CallbackEnabledAgent(AIAgent agent, CallbackMiddlewareProcessor? callbackMiddlewareProcessor) : base(agent)
+ {
+ this._callbacksProcessor = callbackMiddlewareProcessor ?? new();
+ }
+
+ public override async Task RunAsync(
+ IEnumerable messages,
+ AgentThread? thread = null,
+ AgentRunOptions? options = null,
+ CancellationToken cancellationToken = default)
+ {
+ AgentInvokeCallbackContext roamingContext = null!;
+
+ async Task CoreLogic(AgentInvokeCallbackContext ctx)
+ {
+ roamingContext ??= ctx;
+ var result = await this.InnerAgent.RunAsync(ctx.Messages, ctx.Thread, ctx.Options, ctx.CancellationToken);
+
+ ctx.SetRawResponse(result);
+ }
+
+ await this._callbacksProcessor.ProcessAsync(
+ new AgentInvokeCallbackContext(
+ agent: this,
+ messages: messages,
+ thread,
+ options,
+ isStreaming: false,
+ cancellationToken),
+ CoreLogic,
+ cancellationToken);
+
+ return roamingContext.RunResponse!;
+ }
+}
+```
+
+#### Pros
+- Flexibility: Use shared processor for multiple agents or create per-agent instances
+- Clean fluent configuration API with `.UseCallbacks()` builder method
+- Type-safe middleware registration with `CallbackMiddleware` base class
+- Thread-safe processor implementation using `ConcurrentBag`
+- Extensible context system with `AgentInvokeCallbackContext` providing rich execution context
+- Seamless integration with existing agent builder pattern
+- Support for both streaming and non-streaming scenarios in middleware
+- Clear separation between middleware logic and agent core functionality
+- Simplicity: Agents stay lean, middleware is externalized to processor
+- Extensibility: Add new contexts/filters without changing agent implementation
+
+#### Cons
+- Additional complexity with processor class and context management
+- Requires understanding of middleware lifecycle and context passing
+- Type switching in processor for different middleware types
+- Roaming context pattern needed to capture specialized contexts through middleware chain
+
+## APPENDIX 1: Proposed Middleware Contexts
+
+The following context classes would be needed to support the filtering architecture:
+
+```csharp
+public abstract class AgentContext
+{
+ // For scenarios where the filter is processed by multiple agents sounds very desirable to provide access to the invoking agent
+ public AIAgent Agent { get; }
+
+ public AgentRunOptions? Options { get; set; } // Options are allowed to be set by filters
+
+ protected AgentContext(AIAgent agent, AgentRunOptions? options)
+ {
+ Agent = agent;
+ Options = options;
+ }
+}
+
+public class AgentRunContext : AgentContext
+{
+ public IList Messages { get; set; }
+ public AgentResponse? Response { get; set; }
+ public AgentThread? Thread { get; }
+
+ public AgentRunContext(AIAgent agent, IList messages, AgentThread? thread, AgentRunOptions? options)
+ : base(agent, options)
+ {
+ Messages = messages;
+ Thread = thread;
+ }
+}
+
+public class AgentFunctionInvocationContext : AgentToolContext
+{
+ // Similar to MEAI.FunctionInvocationContext
+ public AIFunction Function { get; set; }
+ public AIFunctionArguments Arguments { get; set; }
+ public FunctionCallContent CallContent { get; set; }
+ public IList Messages { get; set; }
+ public ChatOptions? Options { get; set; }
+ public int Iteration { get; set; }
+ public int FunctionCallIndex { get; set; }
+ public int FunctionCount { get; set; }
+ public bool Terminate { get; set; }
+ public bool IsStreaming { get; set; }
+}
+
+```
+
+## APPENDIX 2: Setting Up Middleware Options
+
+### 1. Semantic Kernel Setup
+
+Has the benefit of clear separation of concerns, but this approach requires developers
+to manage and maintain separate collections for each filter type, increasing code complexity and maintenance overhead.
+
+```csharp
+// Use Case
+var agent = new MyAgent();
+agent.RunFilters.Add(new MyAgentRunFilter());
+agent.RunFilters.Add(new MyMultipleFilterImplementation());
+agent.FunctionCallFilters.Add(new MyAgentFunctionCallFilter());
+agent.FunctionCallFilters.Add(new MyMultipleFilterImplementation());
+agent.AYZFilters.Add(new MyAgentAYZFilter());
+agent.AYZFilters.Add(new MyMultipleFilterImplementation());
+
+
+
+// Impl
+interface IAgentRunFilter
+{
+ Task OnRunAsync(AgentRunContext context, Func next, CancellationToken cancellationToken = default);
+}
+interface IAgentFunctionCallFilter
+{
+ Task OnFunctionCallAsync(AgentFunctionCallContext context, Func next, CancellationToken cancellationToken = default);
+}
+```
+
+#### Pros
+- Clean separation of concerns
+- Follows established patterns in Semantic Kernel and easy migration path
+- No resistance or complaints from the community when used in Semantic Kernel
+
+#### Cons
+- Adding more filters may require adding more properties to the agent/processor class.
+- Adding more filters requires bigger code changes downstream to callers.
+
+### 2. Setup with Generic Method
+
+Instead of properties, exposing as a method may be more appropriate while still maintaining those filters in separate buckets internally.
+
+```csharp
+// Use Case
+var agent = new MyAgent();
+agent.AddFilters([new MyAgentRunFilter(), new MyMultipleFilterImplementation()]);
+agent.AddFilters([new MyAgentFunctionCallFilter(), new MyMultipleFilterImplementation()]);
+agent.AddFilters([new MyAgentAYZFilter(), new MyMultipleFilterImplementation()]);
+
+```
+
+#### Pros
+- Clean separation of concerns
+- Cleaner API for adding filters compared to option 1
+- No resistance or complaints from the community when used in Semantic Kernel
+
+#### Cons
+- Adding more filters may require adding more properties to the agent/processor class.
+- Adding more filters requires bigger code changes downstream to callers.
+
+### 3. Setup with Filter Hierarchy, Fully Generic Setup
+
+In a more generic approach, filters can be grouped in the same bucket and processed based on the context.
+One generic interface for all filters, with context-specific implementations.
+Allow simple grouping of filters in the same list and adding new filter types with low code-changes.
+
+```csharp
+// Use Case
+var agent = new MyAgent();
+agent.Filters.Add(new MyAgentRunFilter());
+agent.Filters.Add(new MyAgentFunctionCallFilter());
+agent.Filters.Add(new MyAgentAYZFilter());
+agent.Filters.Add(new MyMultipleFilterImplementation());
+
+// OR Via constructor (Also DI Friendly)
+var agent = new MyAgent(new List {
+ new MyAgentRunFilter(),
+ new MyAgentFunctionCallFilter(),
+ new MyAgentAYZFilter(),
+ new MyMultipleFilterImplementation() });
+
+// Impl
+interface IAgentFilter
+{
+ bool CanProcess(AgentContext context);
+ Task OnProcessAsync(AgentContext context, Func next, CancellationToken cancellationToken = default);
+}
+
+interface IAgentFilter : IAgentFilter where T : AgentContext
+{
+ Task OnProcessAsync(T context, Func next, CancellationToken cancellationToken = default);
+}
+
+class MySingleFilterImplementation : IAgentFilter
+{
+ public bool CanProcess(AgentContext context)
+ => context is AgentRunContext;
+
+ public async Task OnProcessAsync(AgentContext context, Func next, CancellationToken cancellationToken = default)
+ {
+ Func wrappedNext = async ctx => await next(ctx);
+ await OnProcessAsync((AgentRunContext)context, wrappedNext, cancellationToken);
+ }
+
+ public async Task OnProcessAsync(AgentRunContext context, Func next, CancellationToken cancellationToken = default)
+ {
+ // Pre-run logic
+ await next(context);
+ // Post-run logic
+ }
+}
+
+class MyMultipleFilterImplementation : IAgentFilter, IAgentFilter
+{
+ public bool CanProcess(AgentContext context)
+ => context is AgentRunContext or FunctionCallAgentContext;
+
+ public async Task OnProcessAsync(AgentContext context, Func next, CancellationToken cancellationToken = default)
+ {
+ if (context is AgentRunContext runContext)
+ {
+ Func wrappedNext = async ctx => await next(ctx);
+ await OnProcessAsync(runContext, wrappedNext, cancellationToken);
+ return;
+ }
+
+ if (context is FunctionCallAgentContext callContext)
+ {
+ Func wrappedNext = async ctx => await next(ctx);
+ await OnProcessAsync(callContext, wrappedNext, cancellationToken);
+ return;
+ }
+
+ await next(context);
+ }
+
+ public async Task OnProcessAsync(AgentRunContext context, Func next, CancellationToken cancellationToken = default)
+ {
+ // Pre-run logic
+ await next(context);
+ // Post-run logic
+ }
+
+ public async Task OnProcessAsync(FunctionCallAgentContext context, Func next, CancellationToken cancellationToken = default)
+ {
+ // Pre-function call logic
+ await next(context);
+ // Post-function call logic
+ }
+}
+```
+
+#### Pros
+- Simple grouping of filters in the same list, help with DI registration and filtering iteration
+- Lower maintenance and learning curve when adding new filter types
+- Can be combined with other patterns like the `AgentFilterProcessor`
+
+#### Cons
+- Less clear separation of concerns compared to dedicated filter types
+- Requires extra runtime type checking and casting for context-specific processing
+
+## Decision Outcome
+
+- **Option 2 (Decorator Pattern)** is the preferred approach for the following reasons:
+ - Adding a processor pattern seems an overkill as we can achieve same results without introducing new abstractions and complexity.
+ - Direct decorator on agents and tools for agent and function invocation middleware.
+ - Support for Context-based middleware also leveraging closer patterns to Semantic Kernel filters.
+ - Agent Builder pattern integration with `.Use()` method for fluent configuration
+
+**Key POC Insights**:
+1. Both patterns actually work
+2. The decorator pattern offers more direct control and simpler and more flexible implementation
+2. The processor seems an overkill compared to decorator as it adds more extra abstractions and complexity
+4. Function invocation filtering is supported in both patterns
+5. Streaming scenarios are well-supported in both approaches
+6. Function approval request filtering is supported in both patterns
+7. Builder pattern added as part of the POC is a must-have and mades both approaches developer-friendly
+
+## Appendix: Other AI Agent Framework Analysis Details
+
+#### LangChain
+
+LangChain uses callbacks for interception, which can be passed at runtime or during construction.
+
+Naming (Python): Callbacks (BaseCallbackHandler)
+Supports: Y (read/write)
+Observation: Uses observer pattern with event methods for interception (e.g., on_chain_start); supports agent actions and errors; handlers can read inputs/outputs and modify metadata or raise exceptions to influence flow.
+
+**Python Example:** For more details, see the official documentation: [Callbacks - Python LangChain](https://python.langchain.com/docs/concepts/callbacks/).
+
+```python
+from langchain_core.callbacks import BaseCallbackHandler
+
+class MyHandler(BaseCallbackHandler):
+ def on_chain_start(self, serialized, inputs, **kwargs):
+ inputs['number'] += 1 # Modify inputs (write capability)
+ print("Chain started!")
+
+handler = MyHandler()
+
+# Pass callback at runtime
+chain.invoke({"number": 25}, {"callbacks": [handler]})
+
+# Or at constructor time
+chain = SomeChain(callbacks=[handler])
+chain.invoke({"number": 25})
+```
+
+Naming (JS): Callbacks (BaseCallbackHandler)
+Supports: Y (read/write)
+Observation: Similar observer pattern to Python, with event methods adapted for JS async handling; supports chain/agent interception; handlers can read inputs/outputs and modify metadata or raise exceptions to influence flow.
+
+**JS Example:** For more details, see the official documentation: [Callbacks - LangChain.js](https://js.langchain.com/docs/concepts/callbacks/). (Adapted for async handling in JS.)
+
+```javascript
+import { BaseCallbackHandler } from "@langchain/core/callbacks/base";
+
+class MyHandler extends BaseCallbackHandler {
+ name = "my_handler";
+
+ async handleChainStart(chain, inputs) {
+ inputs.number += 1; # Modify inputs (write capability)
+ console.log("Chain started!");
+ }
+}
+
+const handler = new MyHandler();
+
+// Pass callback at runtime
+await chain.invoke({ number: 25 }, { callbacks: [handler] });
+
+// Or at constructor time
+const chainWithHandler = new SomeChain({ callbacks: [handler] });
+await chainWithHandler.invoke({ number: 25 });
+```
+
+#### LangGraph
+
+LangGraph inherits callbacks from LangChain and often uses them with handlers for observability (e.g., via Langfuse).
+
+Naming (Python): Hooks/Callbacks (inherited from LangChain)
+Supports: Y (read/write)
+Observation: Event-driven with runtime handlers; integrates callbacks for observability in graphs; inherits LangChain's ability to read/modify metadata or interrupt execution.
+
+For more details, see the official documentation (inherited from LangChain): [Callbacks - Python LangChain](https://python.langchain.com/docs/concepts/callbacks/). Here's an example of streaming with a callback handler (Python):
+
+```python
+from langfuse.langchain import CallbackHandler
+from langchain_core.messages import HumanMessage
+
+class MyLangfuseHandler(CallbackHandler):
+ def on_chain_start(self, serialized, inputs, **kwargs):
+ inputs['messages'][0].content += " modified" # Modify input messages (write capability)
+ super().on_chain_start(serialized, inputs, **kwargs)
+
+langfuse_handler = MyLangfuseHandler()
+
+# Stream with callback in config
+for s in graph.stream(
+ {"messages": [HumanMessage(content="What is Langfuse?")]},
+ config={"callbacks": [langfuse_handler]}
+):
+ print(s)
+```
+
+#### AutoGen
+
+AutoGen supports middleware-like behavior in both languages.
+
+Naming (Python): Reply Functions (register_reply)
+Supports: Y (read/write)
+Observation: Reply functions intercept and process messages; middleware-like for agent replies; can directly modify messages or replies before continuing.
+
+**Python Example:** For more details, see the official documentation: [agentchat.conversable_agent | AutoGen 0.2](https://microsoft.github.io/autogen/0.2/docs/reference/agentchat/conversable_agent). Uses `register_reply` to add reply functions that intercept and process messages.
+
+```python
+def print_messages(recipient, messages, sender, config):
+ if "callback" in config and config["callback"] is not None:
+ callback = config["callback"]
+ callback(sender, recipient, messages[-1])
+ messages[-1]["content"] += " modified" # Modify last message content (write capability)
+ print(f"Messages sent to: {recipient.name} | num messages: {len(messages)}")
+ return False, None # required to ensure the agent communication flow continues
+
+user_proxy.register_reply(
+ [autogen.Agent, None],
+ reply_func=print_messages,
+ config={"callback": None},
+)
+
+assistant.register_reply(
+ [autogen.Agent, None],
+ reply_func=print_messages,
+ config={"callback": None},
+)
+```
+
+Naming (C#): Middleware (MiddlewareAgent)
+Supports: Y (read/write)
+Observation: Decorator/wrapper with middleware delegates for message modification; delegates can read and alter message content or options.
+
+**C# Example:** For more details, see the official documentation: [Use middleware in an agent - AutoGen for .NET](https://microsoft.github.io/autogen-for-net/articles/Middleware-overview.html). Registers middleware to modify messages.
+
+```csharp
+// Register middleware to modify messages
+var middlewareAgent = new MiddlewareAgent(innerAgent: agent);
+middlewareAgent.Use(async (messages, options, agent, ct) =>
+{
+ if (messages.Last() is TextMessage lastMessage && lastMessage.Content.Contains("Hello World"))
+ {
+ lastMessage.Content = $"[middleware] {lastMessage.Content}"; # Modify message content (write capability)
+ return lastMessage;
+ }
+ return await agent.GenerateReplyAsync(messages, options, ct);
+});
+```
+
+#### Semantic Kernel
+
+Semantic Kernel uses filters added to the kernel for interception during function invocation, prompt rendering, etc. Implementations differ by language: C# use interfaces, while Python uses functions and decorators.
+
+Naming (C#): Filters (IFunctionInvocationFilter, etc.)
+Supports: Y (read/write)
+Observation: Interface-based middleware for function/prompt interception; filters can read and modify context, arguments, or results.
+
+**C# Example:** For more details, see the official documentation: [Semantic Kernel Filters | Microsoft Learn](https://learn.microsoft.com/en-us/semantic-kernel/concepts/enterprise-readiness/filters). Adding a function invocation filter using interfaces.
+
+```csharp
+using Microsoft.SemanticKernel;
+
+IKernelBuilder builder = Kernel.CreateBuilder();
+builder.Services.AddSingleton();
+
+Kernel kernel = builder.Build();
+
+// Alternatively, add directly
+kernel.FunctionInvocationFilters.Add(new LoggingFilter(logger));
+
+// Define the filter
+public sealed class LoggingFilter(ILogger logger) : IFunctionInvocationFilter
+{
+ public async Task OnFunctionInvocationAsync(FunctionInvocationContext context, Func next)
+ {
+ context.Arguments["new_arg"] = "modified_value"; # Modify arguments by adding a new key (write capability)
+ logger.LogInformation("Invoking {FunctionName}", context.Function.Name);
+ await next(context);
+ logger.LogInformation("Invoked {FunctionName}", context.Function.Name);
+ }
+}
+```
+
+Naming (Python): Filters (add_filter, @kernel.filter decorator)
+Supports: Y (read/write)
+Observation: Function and decorator-based for interception; no explicit interfaces like C#, focuses on async functions for filters; can read and modify context/arguments/results.
+
+**Python Example:** For more details, see the official documentation: [Semantic Kernel Filters | Microsoft Learn](https://learn.microsoft.com/en-us/semantic-kernel/concepts/enterprise-readiness/filters). Adding function invocation filters (one as a standalone function and one via decorator).
+
+```python
+import logging
+from typing import Callable, Coroutine, Any
+from semantic_kernel import Kernel
+from semantic_kernel.filters import FilterTypes, FunctionInvocationContext
+from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion
+from semantic_kernel.contents import ChatHistory
+from semantic_kernel.exceptions import OperationCancelledException
+
+logger = logging.getLogger(__name__)
+
+async def input_output_filter(
+ context: FunctionInvocationContext,
+ next: Callable[[FunctionInvocationContext], Coroutine[Any, Any, None]],
+) -> None:
+ if context.function.plugin_name != "chat":
+ await next(context)
+ return
+ try:
+ user_input = input("User:> ")
+ except (KeyboardInterrupt, EOFError) as exc:
+ raise OperationCancelledException("User stopped the operation") from exc
+ if user_input == "exit":
+ raise OperationCancelledException("User stopped the operation")
+ context.arguments["chat_history"].add_user_message(user_input) # Modify arguments by adding message (write capability)
+
+ await next(context)
+
+ if context.result:
+ logger.info(f"Usage: {context.result.metadata.get('usage')}")
+ context.arguments["chat_history"].add_message(context.result.value[0])
+ print(f"Mosscap:> {context.result!s}")
+
+kernel = Kernel()
+kernel.add_service(AzureChatCompletion(service_id="chat-gpt"))
+
+# Add filter as a standalone function
+kernel.add_filter("function_invocation", input_output_filter)
+
+# Add filter via decorator
+@kernel.filter(filter_type=FilterTypes.FUNCTION_INVOCATION)
+async def exception_catch_filter(
+ context: FunctionInvocationContext, next: Coroutine[FunctionInvocationContext, Any, None]
+):
+ try:
+ await next(context)
+ except Exception as e:
+ logger.info(e)
+
+# Example invocation (assuming a "chat" plugin is added)
+history = ChatHistory()
+result = await kernel.invoke(
+ function_name="chat",
+ plugin_name="chat",
+ chat_history=history,
+)
+```
+
+#### CrewAI
+
+CrewAI uses event listeners for callbacks.
+
+Naming (Python): Events/Callbacks (BaseEventListener)
+Supports: Y (read)
+Observation: Event-driven orchestration with listeners for workflows; listeners can observe events (e.g., read source/event data) but are primarily for logging/reactions without direct modification of workflow state.
+
+For more details, see the official documentation: [Event Listeners - CrewAI Documentation](https://docs.crewai.com/concepts/event-listener). Here's an example of setting up a custom listener (Python):
+
+```python
+from crewai.utilities.events import (
+ CrewKickoffStartedEvent,
+ BaseEventListener,
+ crewai_event_bus
+)
+
+class MyCustomListener(BaseEventListener):
+ def setup_listeners(self, crewai_event_bus):
+ @crewai_event_bus.on(CrewKickoffStartedEvent)
+ def on_crew_started(source, event):
+ print(f"Crew '{event.crew_name}' started!")
+
+my_listener = MyCustomListener() # Automatically registers on init
+
+# Use in a crew
+crew = Crew(agents=[...], tasks=[...])
+```
+
+#### LlamaIndex
+
+LlamaIndex uses callback managers with handlers.
+
+Naming (Python): Callbacks (CallbackManager, BaseCallbackHandler)
+Supports: Y (read)
+Observation: Observer pattern with event methods for queries and tools; handlers can observe events/payloads (e.g., read prompts/responses) but are designed for debugging/tracing without modifying execution context.
+
+For more details, see the official documentation: [Callbacks - LlamaIndex](https://docs.llamaindex.ai/en/stable/module_guides/observability/callbacks/). Here's an example setup (Python):
+
+```python
+from llama_index.core.callbacks import CallbackManager, LlamaDebugHandler
+
+debug_handler = LlamaDebugHandler() # Concrete handler subclassing BaseCallbackHandler
+callback_manager = CallbackManager([debug_handler])
+
+# Assign to components, e.g., an index or query engine
+index = VectorStoreIndex.from_documents(documents, callback_manager=callback_manager)
+query_engine = index.as_query_engine()
+response = query_engine.query("What is this about?")
+```
+
+#### Haystack
+
+Haystack does not support explicit middleware or filters like the others. Instead, it uses a modular pipeline architecture for interception via components (e.g., ConditionalRouter for routing based on conditions like tool calls) and observability through logging/tracing integrations (e.g., Langfuse).
+
+Naming (Python): N/A (Pipeline Components/Routers)
+Supports: N (Pipeline-based interception)
+Observation: Relies on modular pipelines for implicit interception but lacks explicit middleware/filters; custom components can read/write data flow via routing/transformations, but this is compositional rather than hook-based interception.
+
+For more details, see the official documentation: [Pipelines - Haystack Documentation](https://docs.haystack.deepset.ai/docs/pipelines). Here's an example of pipeline-based interception with a custom collector component (Python):
+
+```python
+from haystack import Pipeline
+from haystack.components.generators.chat import OpenAIChatGenerator
+from haystack.components.routers import ConditionalRouter
+from haystack.components.tools import ToolInvoker
+from haystack.tools import ComponentTool
+from haystack.components.websearch import SerperDevWebSearch
+from haystack.dataclasses import ChatMessage
+from typing import Any, Dict, List
+from haystack import component
+from haystack.core.component.types import Variadic
+
+# Custom component to collect/observe messages (for interception/observation)
+@component()
+class MessageCollector:
+ def __init__(self):
+ self._messages = []
+ @component.output_types(messages=List[ChatMessage])
+ def run(self, messages: Variadic[List[ChatMessage]]) -> Dict[str, Any]:
+ self._messages.extend([msg for inner in messages for msg in inner])
+ return {"messages": self._messages}
+ def clear(self):
+ self._messages = []
+
+# Define a tool
+web_tool = ComponentTool(component=SerperDevWebSearch(top_k=3))
+
+# Define routes for filtering (e.g., check for tool calls)
+routes = [
+ {
+ "condition": "{{replies[0].tool_calls | length > 0}}",
+ "output": "{{replies}}",
+ "output_name": "there_are_tool_calls",
+ "output_type": List[ChatMessage],
+ },
+ {
+ "condition": "{{replies[0].tool_calls | length == 0}}",
+ "output": "{{replies}}",
+ "output_name": "final_replies",
+ "output_type": List[ChatMessage],
+ },
+]
+
+# Build the pipeline
+pipeline = Pipeline()
+pipeline.add_component("generator", OpenAIChatGenerator(model="gpt-4o-mini"))
+pipeline.add_component("router", ConditionalRouter(routes=routes))
+pipeline.add_component("tool_invoker", ToolInvoker(tools=[web_tool]))
+pipeline.add_component("message_collector", MessageCollector())
+
+# Connect components (interception via routing and collection)
+pipeline.connect("generator.replies", "router.replies")
+pipeline.connect("router.there_are_tool_calls", "tool_invoker.messages")
+pipeline.connect("tool_invoker.messages", "message_collector.messages")
+pipeline.connect("router.final_replies", "message_collector.messages")
+
+# Run the pipeline (observes via collector, filters via router)
+result = pipeline.run({"generator": {"messages": [ChatMessage.from_user("What's the weather in Berlin?")]}})
+print(result["message_collector"]["messages"])
+```
+
+#### OpenAI Swarm
+
+OpenAI Swarm does not provide native support for middleware, filters, callbacks, or hooks. While interception can be achieved through custom implementations (e.g., function wrappers, client subclassing, or manual tool execution with `execute_tools=False`), this requires the caller to implement their own logic, which is not considered built-in framework support.
+
+Naming (Python): N/A
+Supports: N
+Observation: No explicit middleware/filters; interception requires custom wrappers or manual handling (e.g., function decorators, client subclassing), lacking native framework support for built-in components to accept such modifications.
+
+For more details, see the official GitHub repository: [OpenAI Swarm GitHub](https://github.com/openai/swarm). No native code examples available for interception; custom approaches are possible but not framework-native.
+
+#### Atomic Agents
+
+Atomic Agents does not support explicit middleware, callbacks, hooks, or filters. Its modularity allows composable components, but no dedicated interception mechanisms are documented.
+
+Naming (Python): N/A (Composable Components)
+Supports: N
+Observation: No explicit middleware/filters; modularity allows composable units but no dedicated interception hooks or callbacks for custom reading/modification mid-execution.
+
+For more details, see the official documentation: [Atomic Agents Docs](https://brainblend-ai.github.io/atomic-agents/). No specific code examples available for interception.
+
+#### Smolagents (Hugging Face)
+
+Smolagents does not support explicit middleware, callbacks, hooks, or filters; it focuses on simple agent building.
+
+Naming (Python): N/A
+Supports: N
+Observation: No explicit support; focuses on simple agent building without interception mechanisms or hooks for reading/modifying execution.
+
+For more details, see the official documentation: [Smolagents Docs](https://huggingface.co/docs/smolagents/en/index). No specific code examples available for interception.
+
+#### Phidata (Agno)
+
+Phidata (Agno) does not support explicit middleware, callbacks, hooks, or filters; agents rely on tools and memory.
+
+Naming (Python): N/A
+Supports: N
+Observation: No explicit middleware/filters; agents use tools/memory but no interception hooks for custom reading/modification of calls.
+
+For more details, see the official documentation: [Phidata Docs](https://docs.phidata.com/). No specific code examples available for interception.
+
+#### PromptFlow (Microsoft)
+
+PromptFlow supports tracing for LLM interactions, which acts like callbacks for debugging and iteration.
+
+Naming (Python): Tracing
+Supports: N (Tracing only)
+Observation: Supports tracing for LLM interactions, acting as callbacks for debugging/iteration; tracing is read-only for observability/telemetry without options to modify context or intercept calls beyond logging.
+
+For more details, see the official documentation: [Tracing in PromptFlow](https://microsoft.github.io/promptflow/how-to-guides/tracing/index.html). No direct code examples in the browsed content, but tracing is integrated into flow debugging (Python).
+
+#### n8n
+
+n8n's AI Agent node inherits callbacks from LangChain for observability in workflows.
+
+Naming (JS/TS): Callbacks (inherited from LangChain)
+Supports: Y (read/write)
+Observation: AI Agent node uses LangChain under the hood, inheriting callbacks for observability; supports reading/modifying metadata or interrupting flow as in LangChain.
+
+For more details, see the official documentation: [AI Agent Node Docs](https://docs.n8n.io/integrations/builtin/cluster-nodes/root-nodes/n8n-nodes-langchain.agent/). (Inherits from LangChain; refer to LangChain docs for callback examples.) No specific n8n-unique code in the content, but uses LangChain's observer pattern. Here's an adapted LangChain JS example for consistency:
+
+```javascript
+import { BaseCallbackHandler } from "@langchain/core/callbacks/base";
+
+class MyHandler extends BaseCallbackHandler {
+ name = "my_handler";
+
+ async handleChainStart(chain, inputs) {
+ inputs.number += 1; # Modify inputs (write capability)
+ console.log("Chain started!");
+ }
+}
+
+const handler = new MyHandler();
+
+// Pass callback at runtime
+await chain.invoke({ number: 25 }, { callbacks: [handler] });
+
+// Or at constructor time
+const chainWithHandler = new SomeChain({ callbacks: [handler] });
+await chainWithHandler.invoke({ number: 25 });
+```
diff --git a/docs/decisions/0008-python-subpackages.md b/docs/decisions/0008-python-subpackages.md
new file mode 100644
index 0000000..fdd5a79
--- /dev/null
+++ b/docs/decisions/0008-python-subpackages.md
@@ -0,0 +1,92 @@
+---
+status: accepted
+contact: eavanvalkenburg
+date: 2025-09-19
+deciders: eavanvalkenburg, markwallace-microsoft, ekzhu, sphenry, alliscode
+consulted: taochenosu, moonbox3, dmytrostruk, giles17
+---
+
+# Python Subpackages Design
+
+## Context and Problem Statement
+
+The goal is to design a subpackage structure for the Python agent framework that balances ease of use, maintainability, and scalability. How can we organize the codebase to facilitate the development and integration of connectors while minimizing complexity for users?
+
+## Decision Drivers
+
+- Ease of use for developers
+- Maintainability of the codebase
+- User experience for installing and using the integrations
+- Clear lifecycle management for integrations
+- Minimize non-GA dependencies in the main package
+
+## Considered Options
+
+1. One subpackage per vendor, so a `google` package that contains all Google related connectors, such as `GoogleChatClient`, `BigQueryCollection`, etc.
+ * Pros:
+ - fewer packages to manage, publish and maintain
+ - easier for users to find and install the right package.
+ - users that work primarily with one platform have a single package to install.
+ * Cons:
+ - larger packages with more dependencies
+ - larger installation sizes
+ - more difficult to version, since some parts may be GA, while other are in preview.
+2. One subpackage per connector, so a i.e. `google_chat` package, a i.e. `google_bigquery` package, etc.
+ * Pros:
+ - smaller packages with fewer dependencies
+ - smaller installation sizes
+ - easy to version and do lifecycle management on
+ * Cons:
+ - more packages to manage, register, publish and maintain
+ - more extras, means more difficult for users to find and install the right package.
+3. Group connectors by vendor and maturity, so that you can graduate something from the i.e. the `google-preview` package to the `google` package when it becomes GA.
+ * Pros:
+ - fewer packages to manage, publish and maintain
+ - easier for users to find and install the right package.
+ - users that work primarily with one platform have a single package to install.
+ - clear what the status is based on extra name
+ * Cons:
+ - moving something from one to the other might be a breaking change
+ - still larger packages with more dependencies
+ It could be mitigated that the `google-preview` package is still imported from `agent_framework.google`, so that the import path does not change, when something graduates, but it is still a clear choice for users to make. And we could then have three extras on that package, `google`, `google-preview` and `google-all` to make it easy to install the right package or just all.
+4. Group connectors by vendor and type, so that you have a `google-chat` package, a `google-data` package, etc.
+ * Pros:
+ - smaller packages with fewer dependencies
+ - smaller installation sizes
+ * Cons:
+ - more packages to manage, register, publish and maintain
+ - more extras, means more difficult for users to find and install the right package.
+ - still keeps the lifecycle more difficult, since some parts may be GA, while other are in preview.
+5. Add `meta`-extras, that combine different subpackages as one extra, so we could have a `google` extra that includes `google-chat`, `google-bigquery`, etc.
+ * Pros:
+ - easier for users on a single platform
+ * Cons:
+ - more packages to manage, register, publish and maintain
+ - more extras, means more difficult for users to find and install the right package.
+ - makes developer package management more complex, because that meta-extra will include both GA and non-GA packages, so during dev they could use that, but then during prod they have to figure out which one they actually need and make a change in their dependencies, leading to mismatches between dev and prod.
+6. Make all imports happen from `agent_framework.connectors` (or from two or three groups `agent_framework.chat_clients`, `agent_framework.context_providers`, or something similar) while the underlying code comes from different packages.
+ * Pros:
+ - best developer experience, since all imports are from the same place and it is easy to find what you need, and we can raise a meaningfull error with which extra to install.
+ - easier for users to find and install the right package.
+ * Cons:
+ - larger overhead in maintaining the `__init__.py` files that do the lazy loading and error handling.
+ - larger overhead in package management, since we have to ensure that the main package.
+7. Subpackage existence will be based off status of dependencies and/or possibilities of a external support mechanism. What this means is that:
+ - Integrations that need non-GA dependencies will be subpackages, so that we can avoid having non-GA dependencies in the main package.
+ - Integrations where the AF-code is still experimental, preview or release candidate will be subpackages, so that we can avoid having non-GA code in the main package and we can version those packages properly.
+ - Integrations that are outside Microsoft and where we might not always be able to fast-follow breaking changes, will stay as subpackages, to provide some isolation and to be able to version them properly.
+ - Integrations that are mature and that have released (GA) dependencies and or features on the service side will be moved into the main package, the dependencies of those packages will stay installable under the same `extra` name, so that users do not have to change anything, and we then remove the subpackage itself.
+ - All subpackage imports in the code should be from a stable place, mostly vendor-based, so that when something moves from a subpackage to the main package, the import path does not change, so `from agent_framework.google import GoogleChatClient` will always work, even if it moves from the `agent-framework-google` package to the main `agent-framework` package.
+ - The imports in those vendor namespaces (these won't be actual python namespaces, just the folders with a __init__.py file and any code) will do lazy loading and raise a meaningful error if the subpackage or dependencies are not installed, so that users know which extra to install with ease.
+ - On a case by case basis we can decide to create additional `extras`, that combine multiple subpackages into one extra, so that users that work primarily with one platform can install everything they need with a single extra, for instance you can install with the `agent-framework[azure-purview]` extra that only implement a Azure Purview Middleware, or you can install with the `agent-framework[azure]` extra that includes all Azure related connectors, like `purview`, `content safety` and others (all examples, not actual packages (yet)), regardless of where the code sits, these should always be importable from `agent_framework.azure`.
+ - Subpackage naming should also follow this, so in principle a package name is `-`, so `google-gemini`, `azure-purview`, `microsoft-copilotstudio`, etc. For smaller vendors, with less likely to have a multitude of connectors, we can skip the feature/brand part, so `mem0`, `redis`, etc.
+
+## Decision Outcome
+
+Option 7: This provides us a good balance between developer experience, user experience, package management and maintenance, while also allowing us to evolve the package structure over time as dependencies and features mature. And it ensures the main package, installed without extras does not include non-GA dependencies or code, extras do not carry that guarantee, for both the code and the dependencies.
+
+# Microsoft vs Azure packages
+Another consideration is for Microsoft, since we have a lot of Azure services, but also other Microsoft services, such as Microsoft Copilot Studio, and potentially other services in the future, and maybe Foundry also will be marketed separate from Azure at some point. We could also have both a `microsoft` and an `azure` package, where the `microsoft` package contains all Microsoft services, excluding Azure, while the `azure` package only contains Azure services. Only applicable for the variants where we group by vendor, including with meta packages.
+
+## Decision Outcome
+Azure and Microsoft will be the two vendor folders for Microsoft services, so Copilot Studio will be imported from `agent_framework.microsoft`, while Foundry, Azure OpenAI and other Azure services will be imported from `agent_framework.azure`.
diff --git a/docs/decisions/0009-support-long-running-operations.md b/docs/decisions/0009-support-long-running-operations.md
new file mode 100644
index 0000000..a62a038
--- /dev/null
+++ b/docs/decisions/0009-support-long-running-operations.md
@@ -0,0 +1,1689 @@
+---
+status: accepted
+contact: sergeymenshykh
+date: 2025-10-15
+deciders: markwallace, rbarreto, westey-m, stephentoub
+informed: {}
+---
+
+## Long-Running Operations Design
+
+## Context and Problem Statement
+
+The Agent Framework currently supports synchronous request-response patterns for AI agent interactions,
+where agents process requests and return results immediately. Similarly, MEAI chat clients follow the same
+synchronous pattern for AI interactions. However, many real-world AI scenarios involve complex tasks that
+require significant processing time, such as:
+- Code generation and analysis tasks
+- Complex reasoning and research operations
+- Image and content generation
+- Large document processing and summarization
+
+The current Agent Framework architecture needs native support for long-running operations, as it is
+essential for handling these scenarios effectively. Additionally, as MEAI chat clients need to start supporting
+long-running operations as well to be used together with AF agents, the design should consider integration
+patterns and consistency with the broader Microsoft.Extensions.AI ecosystem to provide a unified experience
+across both agent and chat client scenarios.
+
+## Decision Drivers
+- Chat clients and agents should support long-running execution as well as quick prompts.
+- The design should be simple and intuitive for developers to use.
+- The design should be extensible to allow new long-running execution features to be added in the future.
+- The design should be additive rather than disruptive to allow existing chat clients to iteratively add
+support for long-running operations without breaking existing functionality.
+
+## Comparison of Long-Running Operation Features
+| Feature | OpenAI Responses | Foundry Agents | A2A |
+|-----------------------------|---------------------------|-------------------------------------|----------------------|
+| Initiated by | User (Background = true) | Long-running execution is always on | Agent |
+| Modeled as | Response | Run | Task |
+| Supported modes1 | Sync, Async | Async | Sync, Async |
+| Getting status support | ✅ | ✅ | ✅ |
+| Getting result support | ✅ | ✅ | ✅ |
+| Update support | ❌ | ❌ | ✅ |
+| Cancellation support | ✅ | ✅ | ✅ |
+| Delete support | ✅ | ❌ | ❌ |
+| Non-streaming support | ✅ | ✅ | ✅ |
+| Streaming support | ✅ | ✅ | ✅ |
+| Execution statuses | InProgress, Completed, Queued Cancelled, Failed, Incomplete | InProgress, Completed, Queued Cancelled, Failed, Cancelling, RequiresAction, Expired | Working, Completed, Canceled, Failed, Rejected, AuthRequired, InputRequired, Submitted, Unknown |
+
+1 Sync is a regular message-based request/response communication pattern; Async is a pattern for long-running operations/tasks where the agent returns an ID for a run/task and allows polling for status and final results by the ID.
+
+**Note:** The names for new classes, interfaces, and their members used in the sections below are tentative and will be discussed in a dedicated section of this document.
+
+## Long-Running Operations Support for Chat Clients
+
+This section describes different options for various aspects required to add long-running operations support to chat clients.
+
+### 1. Methods for Working with Long-Running Operations
+
+Based on the analysis of existing APIs that support long-running operations (such as OpenAI Responses, Azure AI Foundry Agents, and A2A),
+the following operations are used for working with long-running operations:
+- Common operations:
+ - **Start Long-Running Execution**: Initiates a long-running operation and returns its Id.
+ - **Get Status of Long-Running Execution**: This method retrieves the status of a long-running operation.
+ - **Get Result of Long-Running Execution**: Retrieves the result of a long-running operation.
+- Uncommon operations:
+ - **Update Long-Running Execution**: This method updates a long-running operation, such as adding new messages or modifying existing ones.
+ - **Cancel Long-Running Execution**: This method cancels a long-running operation.
+ - **Delete Long-Running Execution**: This method deletes a long-running operation.
+
+To support these operations by `IChatClient` implementations, the following options are available:
+- **1.1 New IAsyncChatClient Interface for All Long-Running Execution Operations**
+- **1.2 Get{Streaming}ResponseAsync for Common Operations & New IAsyncChatClient Interface for Uncommon Operations**
+- **1.3 Get{Streaming}ResponseAsync for Common Operations & New IAsyncChatClient Interface for Uncommon Operations & Capability Check**
+- **1.4 Get{Streaming}ResponseAsync for Common Operations & Individual Interface per Uncommon Operation**
+
+#### 1.1 New IAsyncChatClient Interface for All Long-Running Execution Operations
+
+This option suggests adding a new interface `IAsyncChatClient` that some implementations of `IChatClient` may implement to support long-running operations.
+```csharp
+public interface IAsyncChatClient
+{
+ Task StartAsyncRunAsync(IList chatMessages, RunOptions? options = null, CancellationToken ct = default);
+ Task GetAsyncRunStatusAsync(string runId, CancellationToken ct = default);
+ Task GetAsyncRunResultAsync(string runId, CancellationToken ct = default);
+ Task UpdateAsyncRunAsync(string runId, IList chatMessages, CancellationToken ct = default);
+ Task CancelAsyncRunAsync(string runId, CancellationToken ct = default);
+ Task DeleteAsyncRunAsync(string runId, CancellationToken ct = default);
+}
+
+public class CustomChatClient : IChatClient, IAsyncChatClient
+{
+ ...
+}
+```
+
+Consumer code example:
+```csharp
+IChatClient chatClient = new CustomChatClient();
+
+string prompt = "..."
+
+// Determine if the prompt should be run as a long-running execution
+if(chatClient.GetService() is { } asyncChatClient && ShouldRunPromptAsynchronously(prompt))
+{
+ try
+ {
+ // Start a long-running execution
+ AsyncRunResult result = await asyncChatClient.StartAsyncRunAsync(prompt);
+ }
+ catch (NotSupportedException)
+ {
+ Console.WriteLine("This chat client does not support long-running operations.");
+ throw;
+ }
+
+ AsyncRunContent? asyncRunContent = GetAsyncRunContent(result);
+
+ // Poll for the status of the long-running execution
+ while (asyncRunContent.Status is AsyncRunStatus.InProgress or AsyncRunStatus.Queued)
+ {
+ result = await asyncChatClient.GetAsyncRunStatusAsync(asyncRunContent.RunId);
+ asyncRunContent = GetAsyncRunContent(result);
+ }
+
+ // Get the result of the long-running execution
+ result = await asyncChatClient.GetAsyncRunStatusAsync(asyncRunContent.RunId);
+ Console.WriteLine(result);
+}
+else
+{
+ // Complete a quick prompt
+ ChatResponse response = await chatClient.GetResponseAsync(prompt);
+ Console.WriteLine(response);
+}
+```
+
+**Pros:**
+- Not a breaking change: Existing chat clients are not affected.
+- Callers can determine if a chat client supports long-running operations by calling its `GetService()` method.
+
+**Cons:**
+- Not extensible: Adding new methods to the `IAsyncChatClient` interface after its release will break existing implementations of the interface.
+- Missing capability check: Callers cannot determine if chat clients support specific uncommon operations before attempting to use them.
+- Insufficient information: Callers may not have enough information to decide whether a prompt should run as a long-running operation.
+- The new method calls bypass existing decorators such as logging, telemetry, etc.
+- An alternative solution for decorating the new methods will have to be put in place because the new method calls bypass existing decorators
+such as logging, telemetry, etc.
+
+#### 1.2 Get{Streaming}ResponseAsync for Common Operations & New IAsyncChatClient Interface for Uncommon Operations
+
+This option suggests using the existing `GetResponseAsync` and `GetStreamingResponseAsync` methods of the `IChatClient` interface to support
+common long-running operations, such as starting long-running operations, getting their status, their results, and potentially
+updating them, in addition to their existing functionality of serving quick prompts. Methods for the uncommon operations, such as updating,
+cancelling, and deleting long-running operations, will be added to a new `IAsyncChatClient` interface that will be implemented by chat clients
+that support them.
+
+This option presumes that Option 3.2 (Have one method for getting long-running execution status and result) is selected.
+
+```csharp
+public interface IAsyncChatClient
+{
+ /// The update can be handled by GetResponseAsync method as well.
+ Task UpdateAsyncRunAsync(string runId, IList chatMessages, CancellationToken ct = default);
+
+ Task CancelAsyncRunAsync(string runId, CancellationToken ct = default);
+ Task DeleteAsyncRunAsync(string runId, CancellationToken ct = default);
+}
+
+public class ResponsesChatClient : IChatClient, IAsyncChatClient
+{
+ public async Task GetResponseAsync(string prompt, ChatOptions? options = null, CancellationToken ct = default)
+ {
+ ClientResult? result = null;
+
+ // If long-running execution mode is enabled, we run the prompt as a long-running execution
+ if(enableLongRunningResponses)
+ {
+ // No RunId is provided, so we start a long-running execution
+ if(options?.RunId is null)
+ {
+ result = await this._openAIResponseClient.CreateResponseAsync(prompt, new ResponseCreationOptions
+ {
+ Background = true,
+ });
+ }
+ else // RunId is provided, so we get the status of a long-running execution
+ {
+ result = await this._openAIResponseClient.GetResponseAsync(options.RunId);
+ }
+ }
+ else
+ {
+ // Handle the case when the prompt should be run as a quick prompt
+ result = await this._openAIResponseClient.CreateResponseAsync(prompt, new ResponseCreationOptions
+ {
+ Background = false
+ });
+ }
+
+ ...
+ }
+
+ public Task UpdateAsyncRunAsync(string runId, IList chatMessages, CancellationToken ct = default)
+ {
+ throw new NotSupportedException("This chat client does not support updating long-running operations.");
+ }
+
+ public Task CancelAsyncRunAsync(string runId, CancellationToken cancellationToken = default)
+ {
+ return this._openAIResponseClient.CancelResponseAsync(runId, cancellationToken);
+ }
+
+ public Task DeleteAsyncRunAsync(string runId, CancellationToken cancellationToken = default)
+ {
+ return this._openAIResponseClient.DeleteResponseAsync(runId, cancellationToken);
+ }
+}
+```
+
+Consumer code example:
+```csharp
+IChatClient chatClient = new ResponsesChatClient();
+
+ChatResponse response = await chatClient.GetResponseAsync("");
+
+if (GetAsyncRunContent(response) is AsyncRunContent asyncRunContent)
+{
+ // Get result of the long-running execution
+ response = await chatClient.GetResponseAsync([], new ChatOptions
+ {
+ RunId = asyncRunContent.RunId
+ });
+
+ // After some time
+
+ // If it's still running, cancel and delete the run
+ if (GetAsyncRunContent(response).Status is AsyncRunStatus.InProgress or AsyncRunStatus.Queued)
+ {
+ IAsyncChatClient? asyncChatClient = chatClient.GetService();
+
+ try
+ {
+ await asyncChatClient?.CancelAsyncRunAsync(asyncRunContent.RunId);
+ }
+ catch (NotSupportedException)
+ {
+ Console.WriteLine("This chat client does not support cancelling long-running operations.");
+ }
+
+ try
+ {
+ await asyncChatClient?.DeleteAsyncRunAsync(asyncRunContent.RunId);
+ }
+ catch (NotSupportedException)
+ {
+ Console.WriteLine("This chat client does not support deleting long-running operations.");
+ }
+ }
+}
+else
+{
+ // Handle the case when the response is a quick prompt completion
+ Console.WriteLine(response);
+}
+```
+
+This option addresses the issue that the option above has with callers needing to know whether the prompt should
+be run as a long-running operation or a quick prompt. It allows callers to simply call the existing `GetResponseAsync` method,
+and the chat client will decide whether to run the prompt as a long-running operation or a quick prompt. If control over
+the execution mode is still needed, and the underlying API supports it, it will be possible for callers to set the mode at
+the chat client invocation or configuration. More details about this are provided in one of the sections below about enabling long-running operation mode.
+
+Additionally, it addresses another issue where the `GetResponseAsync` method may return a long-running
+execution response and the `StartAsyncRunAsync` method may return a quick prompt response. Having one method that handles both cases
+allows callers to not worry about this behavior and simply check the type of the response to determine if it is a long-running operation
+or a quick prompt completion.
+
+With the `GetResponseAsync` method becoming responsible for starting, getting status, getting results and updating long-running operations,
+there are only a few operations left in the `IAsyncChatClient` interface - cancel and delete. As a result, the `IAsyncChatClient` interface
+name may not be the best fit, as it suggests that it is responsible for all long-running operations while it is not. Should
+the interface be renamed to reflect the operations it supports? What should the new name be? Option 1.4 considers an alternative
+that might solve the naming issue.
+
+**Pros:**
+- Delegation and control: Callers delegate the decision of whether to run a prompt as a long-running operation or quick prompt to chat clients,
+while still having the option to control the execution mode to determine how to handle prompts if needed.
+- Not a breaking change: Existing chat clients are not affected.
+
+**Cons:**
+- Not extensible: Adding new methods to the `IAsyncChatClient` interface after its release will break existing implementations of the interface.
+- Missing capability check: Callers cannot determine if chat clients support specific uncommon operations before attempting to use them.
+- An alternative solution for decorating the new methods will have to be put in place because the new method calls bypass existing decorators
+such as logging, telemetry, etc.
+
+#### 1.3 Get{Streaming}ResponseAsync for Common Operations & New IAsyncChatClient Interface for Uncommon Operations & Capability Check
+
+This option extends the previous option with a way for callers to determine if a chat client supports uncommon operations before attempting to use them.
+
+```csharp
+public interface IAsyncChatClient
+{
+ bool CanUpdateAsyncRun { get; }
+ bool CanCancelAsyncRun { get; }
+ bool CanDeleteAsyncRun { get; }
+
+ Task UpdateAsyncRunAsync(string runId, IList chatMessages, CancellationToken ct = default);
+ Task CancelAsyncRunAsync(string runId, CancellationToken ct = default);
+ Task DeleteAsyncRunAsync(string runId, CancellationToken ct = default);
+}
+
+public class ResponsesChatClient : IChatClient, IAsyncChatClient
+{
+ public async Task GetResponseAsync(string prompt, ChatOptions? options = null, CancellationToken ct = default)
+ {
+ ...
+ }
+
+ public bool CanUpdateAsyncRun => false; // This chat client does not support updating long-running operations.
+ public bool CanCancelAsyncRun => true; // This chat client supports cancelling long-running operations.
+ public bool CanDeleteAsyncRun => true; // This chat client supports deleting long-running operations.
+
+ public Task UpdateAsyncRunAsync(string runId, IList chatMessages, CancellationToken ct = default)
+ {
+ throw new NotSupportedException("This chat client does not support updating long-running operations.");
+ }
+
+ public Task CancelAsyncRunAsync(string runId, CancellationToken cancellationToken = default)
+ {
+ return this._openAIResponseClient.CancelResponseAsync(runId, cancellationToken);
+ }
+
+ public Task DeleteAsyncRunAsync(string runId, CancellationToken cancellationToken = default)
+ {
+ return this._openAIResponseClient.DeleteResponseAsync(runId, cancellationToken);
+ }
+}
+```
+
+Consumer code example:
+```csharp
+IChatClient chatClient = new ResponsesChatClient();
+
+ChatResponse response = await chatClient.GetResponseAsync("");
+
+if (GetAsyncRunContent(response) is AsyncRunContent asyncRunContent)
+{
+ // Get result of the long-running execution
+ response = await chatClient.GetResponseAsync([], new ChatOptions
+ {
+ RunId = asyncRunContent.RunId
+ });
+
+ // After some time
+
+ IAsyncChatClient? asyncChatClient = chatClient.GetService();
+
+ // If it's still running, cancel and delete the run
+ if (GetAsyncRunContent(response).Status is AsyncRunStatus.InProgress or AsyncRunStatus.Queued)
+ {
+ if(asyncChatClient?.CanCancelAsyncRun ?? false)
+ {
+ await asyncChatClient?.CancelAsyncRunAsync(asyncRunContent.RunId);
+ }
+
+ if(asyncChatClient?.CanDeleteAsyncRun ?? false)
+ {
+ await asyncChatClient?.DeleteAsyncRunAsync(asyncRunContent.RunId);
+ }
+ }
+}
+else
+{
+ // Handle the case when the response is a quick prompt completion
+ Console.WriteLine(response);
+}
+```
+
+**Pros:**
+- Delegation and control: Callers delegate the decision of whether to run a prompt as a long-running execution or quick prompt to chat clients,
+while still having the option to control the execution mode to determine how to handle prompts if needed.
+- Not a breaking change: Existing chat clients are not affected.
+- Capability check: Callers can determine if the chat client supports an uncommon operation before attempting to use it.
+
+**Cons:**
+- Not extensible: Adding new members to the `IAsyncChatClient` interface after its release will break existing implementations of the interface.
+- An alternative solution for decorating the new methods will have to be put in place because the new method calls bypass existing decorators
+such as logging, telemetry, etc.
+
+#### 1.4 Get{Streaming}ResponseAsync for Common Operations & Individual Interface per Uncommon Operation
+
+This option suggests using the existing `Get{Streaming}ResponseAsync` methods of the `IChatClient` interface to support
+common long-running operations, such as starting long-running operations, getting their status, and their results, and potentially
+updating them, in addition to their existing functionality of serving quick prompts.
+
+The uncommon operations that are not supported by all analyzed APIs, such as updating (which can be handled by `Get{Streaming}ResponseAsync`), cancelling,
+and deleting long-running operations, as well as future ones, will be added to their own interfaces that will be implemented by chat clients
+that support them.
+
+This option presumes that Option 3.2 (Have one method for getting long-running execution status and result) is selected.
+
+The interfaces can inherit from `IChatClient` to allow callers to use an instance of `ICancelableChatClient`, `IUpdatableChatClient`, or `IDeletableChatClient`
+for calling the `Get{Streaming}ResponseAsync` methods as well. However, those methods belong to a leaf chat client that, if obtained via the `GetService()`
+method, won't be decorated by existing decorators such as function invocation, logging, etc. As a result, an alternative solution (wrap the instance of the leaf
+chat client in a decorator at the `GetService` method call) will need to be applied not only to the new methods of one of the interfaces but also to the existing
+`Get{Streaming}ResponseAsync` ones.
+
+```csharp
+public interface ICancelableChatClient
+{
+ Task CancelAsyncRunAsync(string runId, CancellationToken cancellationToken = default);
+}
+
+public interface IUpdatableChatClient
+{
+ Task UpdateAsyncRunAsync(string runId, IList chatMessages, CancellationToken cancellationToken = default);
+}
+
+public interface IDeletableChatClient
+{
+ Task DeleteAsyncRunAsync(string runId, CancellationToken cancellationToken = default);
+}
+
+// Responses chat client that supports standard long-running operations + cancellation and deletion
+public class ResponsesChatClient : IChatClient, ICancelableChatClient, IDeletableChatClient
+{
+ public async Task GetResponseAsync(string prompt, ChatOptions? options = null, CancellationToken ct = default)
+ {
+ ...
+ }
+
+ public Task CancelAsyncRunAsync(string runId, CancellationToken cancellationToken = default)
+ {
+ return this._openAIResponseClient.CancelResponseAsync(runId, cancellationToken);
+ }
+
+ public Task DeleteAsyncRunAsync(string runId, CancellationToken cancellationToken = default)
+ {
+ return this._openAIResponseClient.DeleteResponseAsync(runId, cancellationToken);
+ }
+}
+```
+
+Example that starts a long-running operation, gets its status, and cancels and deletes it if it's not completed after some time:
+```csharp
+IChatClient chatClient = new ResponsesChatClient();
+
+ChatResponse response = await chatClient.GetResponseAsync("", new ChatOptions { AllowLongRunningResponses = true });
+
+if (GetAsyncRunContent(response) is AsyncRunContent asyncRunContent)
+{
+ // Get result
+ response = await chatClient.GetResponseAsync([], new ChatOptions
+ {
+ RunId = asyncRunContent.RunId
+ });
+
+ // After some time
+
+ // If it's still running, cancel and delete the run
+ if (GetAsyncRunContent(response).Status is AsyncRunStatus.InProgress or AsyncRunStatus.Queued)
+ {
+ if(chatClient.GetService() is {} cancelableChatClient)
+ {
+ await cancelableChatClient.CancelAsyncRunAsync(asyncRunContent.RunId);
+ }
+
+ if(chatClient.GetService() is {} deletableChatClient)
+ {
+ await deletableChatClient.DeleteAsyncRunAsync(asyncRunContent.RunId);
+ }
+ }
+}
+```
+
+**Pros:**
+- Extensible: New interfaces can be added and implemented to support new long-running operations without breaking
+existing chat client implementations.
+- Not a breaking change: Existing chat clients that implement the `IChatClient` interface are not affected.
+- Delegation and control: Callers delegate the decision of whether to run a prompt as a long-running operation or quick prompt
+to chat clients, while still having the option to control the execution mode to determine how to handle prompts if needed.
+
+**Cons:**
+- Breaking changes: Changing the signatures of the methods of the operation-specific interfaces or adding new members to them will
+break existing implementations of those interfaces. However, the blast radius of this change is much smaller and limited to a subset
+of chat clients that implement the operation-specific interfaces. However, this is still a breaking change.
+
+### 2. Enabling Long-Running Operations
+
+Based on the API analysis, some APIs must be explicitly configured to run in long-running operation mode,
+while others don't need additional configuration because they either decide themselves whether a request
+should run as a long-running operation, or they always operate in long-running operation mode or quick prompt mode:
+| Feature | OpenAI Responses | Foundry Agents | A2A |
+|-----------------------------|---------------------------|-------------------------------------|----------------------|
+| Long-running execution | User (Background = true) | Long-running execution is always on | Agent |
+
+The options below consider how to enable long-running operation mode for chat clients that support both quick prompts and long-running operations.
+
+#### 2.1 Execution Mode per `Get{Streaming}ResponseAsync` Invocation
+
+This option proposes adding a new nullable `AllowLongRunningResponses` property to the `ChatOptions` class.
+The property value will be `true` if the caller requests a long-running operation, `false`, `null` or omitted otherwise.
+
+Chat clients that work with APIs requiring explicit configuration per operation will use this property to determine whether to run the prompt as a long-running
+operation or quick prompt. Chat clients that work with APIs that don't require explicit configuration will ignore this property and operate according
+to their own logic/configuration.
+
+```csharp
+public class ChatOptions
+{
+ // Existing properties...
+ public bool? AllowLongRunningResponses { get; set; }
+}
+
+// Consumer code example
+IChatClient chatClient = ...; // Get an instance of IChatClient
+
+// Start a long-running execution for the prompt if supported by the underlying API
+ChatResponse response = await chatClient.GetResponseAsync("", new ChatOptions { AllowLongRunningResponses = true });
+
+// Start a quick prompt
+ChatResponse quickResponse = await chatClient.GetResponseAsync("", new ChatOptions { AllowLongRunningResponses = false });
+```
+
+**Pros:**
+- Callers can switch between quick prompts and long-running operation per invocation of the `Get{Streaming}ResponseAsync` methods without
+changing the client configuration.
+- Enables explicit control over the execution mode by callers per invocation, meaning that no caller site is broken if the agent is injected via DI,
+and the caller can turn on the long-running operation mode when it can handle it.
+
+**Con:** This may not be valuable for all callers, as they may not have enough information to decide whether the prompt should run as a long-running operation or quick prompt.
+
+#### 2.2 Execution Mode per `Get{Streaming}ResponseAsync` Invocation + Model Class
+
+This option is similar to the previous one, but suggest using a model class `LongRunningResponsesOptions` for properties related to long-running operations.
+
+```csharp
+public class LongRunningResponsesOptions
+{
+ public bool? Allow { get; set; }
+ //public PollingSettings? PollingSettings { get; set; } // Can be added leter if necessary
+}
+
+public class ChatOptions
+{
+ public LongRunningResponsesOptions? LongRunningResponsesOptions { get; set; }
+}
+
+// Consumer code example
+IChatClient chatClient = ...; // Get an instance of IChatClient
+
+// Start a long-running execution for the prompt if supported by the underlying API
+ChatResponse response = await chatClient.GetResponseAsync("", new ChatOptions { LongRunningResponsesOptions = new() { Allow = true } });
+```
+
+**Pros:**
+- Enables explicit control over the execution mode by callers per invocation, meaning that no caller site is broken if the agent is injected via DI,
+and the caller can turn on the long-running operation mode when it can handle it.
+- No proliferation of long-running operation-related properties in the `ChatOptions` class.
+
+**Con:** Slightly more complex initialization.
+
+#### 2.3 Execution Mode per Chat Client Instance
+
+This option proposes adding a new `enableLongRunningResponses` parameter to constructors of chat clients that support both quick prompts and long-running operations.
+The parameter value will be `true` if the chat client should operate in long-running operation mode, `false` if it should operate in quick prompt mode.
+
+Chat clients that work with APIs requiring explicit configuration will use this parameter to determine whether to run prompts as long-running operations or quick prompts.
+Chat clients that work with APIs that don't require explicit configuration won't have this parameter in their constructors and will operate according to their own
+logic/configuration.
+
+```csharp
+public class CustomChatClient : IChatClient
+{
+ private readonly bool _enableLongRunningResponses;
+
+ public CustomChatClient(bool enableLongRunningResponses)
+ {
+ this._enableLongRunningResponses = enableLongRunningResponses;
+ }
+
+ // Existing methods...
+}
+
+// Consumer code example
+IChatClient chatClient = new CustomChatClient(enableLongRunningResponses: true);
+
+// Start a long-running execution for the prompt
+ChatResponse response = await chatClient.GetResponseAsync("");
+```
+
+Chat clients can be configured to always operate in long-running operation mode or quick prompt mode based on their role in a specific scenario.
+For example, a chat client responsible for generating ideas for images can be configured for quick prompt mode, while a chat client responsible for image
+generation can be configured to always use long-running operation mode.
+
+**Pro:** Can be beneficial for scenarios where chat clients need to be configured upfront in accordance with their role in a scenario.
+
+**Con:** Less flexible than the previous option, as it requires configuring the chat client upfront at instantiation time. However, this flexibility might not be needed.
+
+#### 2.4 Combined Approach
+
+This option proposes a combined approach that allows configuration per chat client instance and per `Get{Streaming}ResponseAsync` method invocation.
+
+The chat client will use whichever configuration is provided, whether set in the chat client constructor or in the options for the `Get{Streaming}ResponseAsync`
+method invocation. If both are set, the one provided in the `Get{Streaming}ResponseAsync` method invocation takes precedence.
+
+```csharp
+public class CustomChatClient : IChatClient
+{
+ private readonly bool _enableLongRunningResponses;
+
+ public CustomChatClient(bool enableLongRunningResponses)
+ {
+ this._enableLongRunningResponses = enableLongRunningResponses;
+ }
+
+ public async Task GetResponseAsync(string prompt, ChatOptions? options = null, CancellationToken ct = default)
+ {
+ bool enableLongRunningResponses = options?.AllowLongRunningResponses ?? this._enableLongRunningResponses;
+ // Logic to handle the prompt based on enableLongRunningResponses...
+ }
+}
+
+// Consumer code example
+IChatClient chatClient = new CustomChatClient(enableLongRunningResponses: true);
+
+// Start a long-running execution for the prompt
+ChatResponse response = await chatClient.GetResponseAsync("");
+
+// Start a quick prompt
+ChatResponse quickResponse = await chatClient.GetResponseAsync("", new ChatOptions { AllowLongRunningResponses = false });
+```
+
+**Pros:** Flexible approach that combines the benefits of both previous options.
+
+### 3. Getting Status and Result of Long-Running Execution
+
+The explored APIs use different approaches for retrieving the status and results of long-running operations. Some are using
+one method to retrieve both status and result, while others use two separate methods for each operation:
+| Feature | OpenAI Responses | Foundry Agents | A2A |
+|-------------------|-------------------------------|----------------------------------------------------|-----------------------|
+| API to Get Status | GetResponseAsync(responseId) | Runs.GetRunAsync(thread.Id, threadRun.Id) | GetTaskAsync(task.Id) |
+| API to Get Result | GetResponseAsync(responseId) | Messages.GetMessagesAsync(thread.Id, threadRun.Id) | GetTaskAsync(task.Id) |
+
+Taking into account the differences, the following options propose a few ways to model the API for getting the status and result of
+long-running operations for the `AIAgent` interface implementations.
+
+#### 3.1 Two Separate Methods for Status and Result
+
+This option suggests having two separate methods for getting the status and result of long-running operations:
+```csharp
+public interface IAsyncChatClient
+{
+ Task GetAsyncRunStatusAsync(string runId, CancellationToken ct = default);
+ Task GetAsyncRunResultAsync(string runId, CancellationToken ct = default);
+}
+```
+
+**Pros:** Could be more intuitive for developers, as it clearly separates the concerns of checking the status and retrieving the result of a long-running operation.
+
+**Cons:** Creates inefficiency for chat clients that use APIs that return both status and result in a single call,
+as callers might make redundant calls to get the result after checking the status that already contains the result.
+
+#### 3.2 One Method to Get Status and Result
+
+This option suggests having a single method for getting both the status and result of long-running operations:
+```csharp
+public interface IAsyncChatClient
+{
+ Task GetAsyncRunResultAsync(string runId, AgentThread? thread = null, CancellationToken ct = default);
+}
+```
+
+This option will redirect the call to the appropriate method of the underlying API that uses one method to retrieve both.
+For APIs that use two separate methods, the method will first get the status and if the status indicates that the
+operation is still running, it will return the status to the caller. If the status indicates that the operation is completed,
+it will then call the method to get the result of the long-running operation and return it together with the status.
+
+**Pros:**
+- Simplifies the API by providing a single, intuitive method for retrieving long-running operation information.
+- More optimal for chat clients that use APIs that return both status and result in a single call, as it avoids unnecessary API calls.
+
+### 4. Place For RunId, Status, and UpdateId of Long-Running Operations
+
+This section considers different options for exposing the `RunId`, `Status`, and `UpdateId` properties of long-running operations.
+
+#### 4.1. As AIContent
+
+The `AsyncRunContent` class will represent a long-running operation initiated and managed by an agent/LLM.
+Items of this content type will be returned in a chat message as part of the `AgentResponse` or `ChatResponse`
+response to represent the long-running operation.
+
+The `AsyncRunContent` class has two properties: `RunId` and `Status`. The `RunId` identifies the
+long-running operation, and the `Status` represents the current status of the operation. The class
+inherits from `AIContent`, which is a base class for all AI-related content in MEAI and AF.
+
+The `AsyncRunStatus` class represents the status of a long-running operation. Initially, it will have
+a set of predefined statuses that represent the possible statuses used by existing Agent/LLM APIs that support
+long-running operations. It will be extended to support additional statuses as needed while also
+allowing custom, not-yet-defined statuses to propagate as strings from the underlying API to the callers.
+
+The content class type can be used by both agents and chat clients to represent long-running operations.
+For chat clients to use it, it should be declared in one of the MEAI packages.
+
+```csharp
+public class AsyncRunContent : AIContent
+{
+ public string RunId { get; }
+ public AsyncRunStatus? Status { get; }
+}
+
+public readonly struct AsyncRunStatus : IEquatable
+{
+ public static AsyncRunStatus Queued { get; } = new("Queued");
+ public static AsyncRunStatus InProgress { get; } = new("InProgress");
+ public static AsyncRunStatus Completed { get; } = new("Completed");
+ public static AsyncRunStatus Cancelled { get; } = new("Cancelled");
+ public static AsyncRunStatus Failed { get; } = new("Failed");
+ public static AsyncRunStatus RequiresAction { get; } = new("RequiresAction");
+ public static AsyncRunStatus Expired { get; } = new("Expired");
+ public static AsyncRunStatus Rejected { get; } = new("Rejected");
+ public static AsyncRunStatus AuthRequired { get; } = new("AuthRequired");
+ public static AsyncRunStatus InputRequired { get; } = new("InputRequired");
+ public static AsyncRunStatus Unknown { get; } = new("Unknown");
+
+ public string Label { get; }
+
+ public AsyncRunStatus(string label)
+ {
+ if (string.IsNullOrWhiteSpace(label))
+ {
+ throw new ArgumentException("Label cannot be null or whitespace.", nameof(label));
+ }
+
+ this.Label = label;
+ }
+
+ /// Other members
+}
+````
+
+The streaming API may return an UpdateId identifying a particular update within a streamed response.
+This UpdateId should be available together with RunId to callers, allowing them to resume a long-running operation identified
+by the RunId from the last received update, identified by the UpdateId.
+
+#### 4.2. As Properties Of ChatResponse{Update}
+
+This option suggests adding properties related to long-running operations directly to the `ChatResponse` and `ChatResponseUpdate` classes rather
+than using a separate content class for that. See section "6. Model To Support Long-Running Operations" for more details.
+
+### 5. Streaming Support
+
+All analyzed APIs that support long-running operations also support streaming.
+
+Some of them natively support resuming streaming from a specific point in the stream, while for others, this is either implementation-dependent or needs to be emulated:
+
+| API | Can Resume Streaming | Model |
+|-------------------------|--------------------------------------|------------------------------------------------------------------------------------------------------------|
+| OpenAI Responses | Yes | StreamingResponseUpdate.**SequenceNumber** + GetResponseStreamingAsync(responseId, **startingAfter**, ct) |
+| Azure AI Foundry Agents | Emulated2 | RunStep.**Id** + custom pseudo code: client.Runs.GetRunStepsAsync(...).AllStepsAfter(**stepId**) |
+| A2A | Implementation dependent1 | |
+
+1 The [A2A specification](https://github.com/a2aproject/A2A/blob/main/docs/topics/streaming-and-async.md#1-streaming-with-server-sent-events-sse)
+allows an A2A agent implementation to decide how to handle streaming resumption: _If a client's SSE connection breaks prematurely while
+a task is still active (and the server hasn't sent a final: true event for that phase), the client can attempt to reconnect to the stream using the tasks/resubscribe RPC method.
+The server's behavior regarding missed events during the disconnection period (e.g., whether it backfills or only sends new updates) is implementation-dependent._
+
+2 The Azure AI Foundry Agents API has an API to start a streaming run but does not have an API to resume streaming from a specific point in the stream.
+However, it has non-streaming APIs to access already started runs, which can be used to emulate streaming resumption by accessing a run and its steps and streaming all the steps after a specific step.
+
+#### Required Changes
+
+To support streaming resumption, the following model changes are required:
+
+- The `ChatOptions` class needs to be extended with a new `StartAfter` property that will identify an update to resume streaming from and to start generating responses after.
+- The `ChatResponseUpdate` class needs to be extended with a new `SequenceNumber` property that will identify the update number within the stream.
+
+All the chat clients supporting the streaming resumption will need to return the `SequenceNumber` property as part of the `ChatResponseUpdate` class and
+honor the `StartAfter` property of the `ChatOptions` class.
+
+#### Function Calling
+
+Function calls over streaming are communicated to chat clients through a series of updates. Chat clients accumulate these updates in their internal state to build
+the function call content once the last update has been received. The completed function call content is then returned to the function-calling chat client,
+which eventually invokes it.
+
+Since chat clients keep function call updates in their internal state, resuming streaming from a specific update can be impossible if the resumption request
+is made using a chat client that does not have the previous updates stored. This situation can occur if a host suspends execution during an ongoing function call
+stream and later resumes from that particular update. Because chat clients' internal state is not persisted, they will lack the prior updates needed to continue
+the function call, leading to a failure in resumption.
+
+To address this issue, chat clients can only return sequence numbers for updates that are resumable. For updates that cannot be resumed from, chat clients can
+return the sequence number of the most recent update received before the non-resumable one. This allows callers to resume from that earlier update,
+even if it means re-processing some updates that have already been handled.
+
+Chat clients will continue returning the sequence number of the last resumable update until a new resumable update becomes available. For example, a chat client might
+keep returning sequence number 2, corresponding to the last resumable update received before an update for the first function call. Once **all** function call updates
+are received and processed, and the model returns a non-function call response, the chat client will then return a sequence number, say 10, which corresponds to the
+first non-function call update.
+
+##### Status of Streaming Updates
+
+Different APIs provide different statuses for streamed function call updates
+
+Sequence of updates from OpenAI Responses API to answer the question "What time is it?" using a function call:
+| Id | SN | Update.Kind | Response.Status | ChatResponseUpdate.Status | Description |
+|--------|----|--------------------------|-----------------|---------------------------|---------------------------------------------------|
+| resp_1 | 0 | resp.created | Queued | Queued | |
+| resp_1 | 1 | resp.queued | Queued | Queued | |
+| resp_1 | 2 | resp.in_progress | InProgress | InProgress | |
+| resp_1 | 3 | resp.output_item.added | - | InProgress | |
+| resp_1 | 4 | resp.func_call.args.delta| - | InProgress | |
+| resp_1 | 5 | resp.func_call.args.done | - | InProgress | |
+| resp_1 | 6 | resp.output_item.done | - | InProgress | |
+| resp_1 | 7 | resp.completed | Completed | Complete | |
+| resp_1 | - | - | - | null | FunctionInvokingChatClient yields function result |
+| | | | OpenAI Responses created a new response to handle function call result |
+| resp_2 | 0 | resp.created | Queued | Queued | |
+| resp_2 | 1 | resp.queued | Queued | Queued | |
+| resp_2 | 2 | resp.in_progress | InProgress | InProgress | |
+| resp_2 | 3 | resp.output_item.added | - | InProgress | |
+| resp_2 | 4 | resp.cnt_part.added | - | InProgress | |
+| resp_2 | 5 | resp.output_text.delta | - | InProgress | |
+| resp_2 | 6 | resp.output_text.delta | - | InProgress | |
+| resp_2 | 7 | resp.output_text.delta | - | InProgress | |
+| resp_2 | 8 | resp.output_text.done | - | InProgress | |
+| resp_2 | 9 | resp.cnt_part.done | - | InProgress | |
+| resp_2 | 10 | resp.output_item.done | - | InProgress | |
+| resp_2 | 11 | resp.completed | Completed | Completed | |
+
+Sequence of updates from Azure AI Foundry Agents API to answer the question "What time is it?" using a function call:
+| Id | SN | UpdateKind | Run.Status | Step.Status | Message.Status | ChatResponseUpdate.Status | Description |
+|--------|---------|-------------------|----------------|-------------|-----------------|---------------------------|---------------------------------------------------|
+| run_1 | - | RunCreated | Queued | - | - | Queued | |
+| run_1 | step_1 | - | RequiredAction | InProgress | - | RequiredAction | |
+| TBD | - | - | - | - | - | - | FunctionInvokingChatClient yields function result |
+| run_1 | - | RunStepCompleted | Completed | - | - | InProgress | |
+| run_1 | - | RunQueued | Queued | - | - | Queued | |
+| run_1 | - | RunInProgress | InProgress | - | - | InProgress | |
+| run_1 | step_2 | RunStepCreated | - | InProgress | - | InProgress | |
+| run_1 | step_2 | RunStepInProgress | - | InProgress | - | InProgress | |
+| run_1 | - | MessageCreated | - | - | InProgress | InProgress | |
+| run_1 | - | MessageInProgress | - | - | InProgress | InProgress | |
+| run_1 | - | MessageUpdated | - | - | - | InProgress | |
+| run_1 | - | MessageUpdated | - | - | - | InProgress | |
+| run_1 | - | MessageUpdated | - | - | - | InProgress | |
+| run_1 | - | MessageCompleted | - | - | Completed | InProgress | |
+| run_1 | step_2 | RunStepCompleted | Completed | - | - | InProgress | |
+| run_1 | - | RunCompleted | Completed | - | - | Completed | |
+
+### 6. Model To Support Long-Running Operations
+
+To support long-running operations, the following values need to be returned by the GetResponseAsync and GetStreamingResponseAsync methods:
+- `ResponseId` - identifier of the long-running operation or an entity representing it, such as a task.
+- `ConversationId` - identifier of the conversation or thread the long-running operation is part of. Some APIs, like Azure AI Foundry Agents, use
+ this identifier together with the ResponseId to identify a run.
+- `SequenceNumber` - identifier of an update within a stream of updates. This is required to support streaming resumption by the GetStreamingResponseAsync method only.
+- `Status` - status of the long-running operation: whether it is queued, running, failed, cancelled, completed, etc.
+
+These values need to be supplied to subsequent calls of the GetResponseAsync and GetStreamingResponseAsync methods to get the status and result of long-running operations.
+
+#### 6.1 ChatOptions
+
+The following options consider different ways of extending the `ChatOptions` class to include the following properties to support long-running operations:
+- `AllowLongRunningResponses` - a boolean property that indicates whether the caller allows the chat client to run in long-running operation mode if it's supported by the chat client.
+- `ResponseId` - a string property that represents the identifier of the long-running operation or an entity representing it. A non-null value of this property would indicate to chat clients
+that callers want to get the status and result of an existing long-running operation, identified by the property value, rather than starting a new one.
+- `StartAfter` - a string property that represents the sequence number of an update within a stream of updates so that the chat client can resume streaming after the last received update.
+
+##### 6.1.1 Direct Properties in ChatOptions
+
+```csharp
+public class ChatOptions
+{
+ // Existing properties...
+ /// Gets or sets an optional identifier used to associate a request with an existing conversation.
+ public string? ConversationId { get; set; }
+ ...
+
+ // New properties...
+ public bool? AllowLongRunningResponses { get; set; }
+ public string? ResponseId { get; set; }
+ public string? StartAfter { get; set; }
+}
+
+// Usage example
+var response = await chatClient.GetResponseAsync("", new ChatOptions { AllowLongRunningResponses = true });
+
+// If the response indicates a long-running operation, get its status and result
+if(response.Status is {} status)
+{
+ response = await chatClient.GetResponseAsync([], new ChatOptions
+ {
+ AllowLongRunningResponses = true,
+ ResponseId = response.ResponseId,
+ ConversationId = response.ConversationId,
+ //StartAfter = response.SequenceNumber // for GetStreamingResponseAsync only
+ });
+}
+
+```
+
+**Con:** Proliferation of long-running operation properties in the `ChatOptions` class.
+
+##### 6.1.2 LongRunOptions Model Class
+
+```csharp
+public class ChatOptions
+{
+ // Existing properties...
+ public string? ConversationId { get; set; }
+ ...
+
+ // New properties...
+ public bool? AllowLongRunningResponses { get; set; }
+
+ public LongRunOptions? LongRunOptions { get; set; }
+}
+
+public class LongRunOptions
+{
+ public string? ResponseId { get; set; }
+ public string? ConversationId { get; set; }
+ public string? StartAfter { get; set; }
+
+ // Alternatively, ChatResponse can have an extension method ToLongRunOptions.
+ public LongRunOptions FromChatResponse(ChatResponse response)
+ {
+ return new LongRunOptions
+ {
+ ResponseId = response.ResponseId,
+ ConversationId = response.ConversationId,
+ };
+ }
+
+ // Alternatively, ChatResponseUpdate can have an extension method ToLongRunOptions.
+ public LongRunOptions FromChatResponseUpdate(ChatResponseUpdate update)
+ {
+ return new LongRunOptions
+ {
+ ResponseId = update.ResponseId,
+ ConversationId = update.ConversationId,
+ StartAfter = update.SequenceNumber,
+ };
+ }
+}
+
+// Usage example
+var response = await chatClient.GetResponseAsync("", new ChatOptions { AllowLongRunningResponses = true });
+
+// If the response indicates a long-running operation, get its status and result
+if(response.Status is {} status)
+{
+ while(status != ResponseStatus.Completed)
+ {
+ response = await chatClient.GetResponseAsync([], new ChatOptions
+ {
+ AllowLongRunningResponses = true,
+ LongRunOptions = LongRunOptions.FromChatResponse(response)
+ // or extension method
+ LongRunOptions = response.ToLongRunOptions()
+ // or implicit conversion
+ LongRunOptions = response
+ });
+ }
+}
+```
+
+**Pro:** No proliferation of long-running operation properties in the `ChatOptions` class.
+
+**Con:** Duplicated property `ConversationId`.
+
+##### 6.1.3 Continuation Token of System.ClientModel.ContinuationToken Type
+
+This option suggests using `System.ClientModel.ContinuationToken` to encapsulate all properties required for long-running operations.
+The continuation token will be returned by chat clients as part of the `ChatResponse` and `ChatResponseUpdate` responses to indicate that
+the response is part of a long-running execution. A null value of the property will indicate that the response is not part of a long-running execution.
+Chat clients will accept a non-null value of the property to indicate that callers want to get the status and result of an existing long-running operation.
+
+Each chat client will implement its own continuation token class that inherits from `ContinuationToken` to encapsulate properties required for long-running operations
+that are specific to the underlying API the chat client works with. For example, for the OpenAI Responses API, the continuation token class will encapsulate
+the `ResponseId` and `SequenceNumber` properties.
+
+```csharp
+public class ChatOptions
+{
+ // Existing properties...
+ public string? ConversationId { get; set; }
+ ...
+
+ // New properties...
+ public bool? AllowLongRunningResponses { get; set; }
+
+ public ContinuationToken? ContinuationToken { get; set; }
+}
+
+internal sealed class LongRunContinuationToken : ContinuationToken
+{
+ public LongRunContinuationToken(string responseId)
+ {
+ this.ResponseId = responseId;
+ }
+
+ public string ResponseId { get; set; }
+
+ public int? SequenceNumber { get; set; }
+
+ public static LongRunContinuationToken FromToken(ContinuationToken token)
+ {
+ if (token is LongRunContinuationToken longRunContinuationToken)
+ {
+ return longRunContinuationToken;
+ }
+
+ BinaryData data = token.ToBytes();
+
+ Utf8JsonReader reader = new(data);
+
+ string responseId = null!;
+ int? startAfter = null;
+
+ reader.Read();
+
+ // Reading functionality
+
+ return new(responseId)
+ {
+ SequenceNumber = startAfter
+ };
+ }
+}
+
+// Usage example
+ChatOptions options = new() { AllowLongRunningResponses = true };
+
+var response = await chatClient.GetResponseAsync("", options);
+
+while (response.ContinuationToken is { } token)
+{
+ options.ContinuationToken = token;
+
+ response = await chatClient.GetResponseAsync([], options);
+}
+
+Console.WriteLine(response.Text);
+```
+
+**Pro:** No proliferation of long-running operation properties in the `ChatOptions` class, including the `Status` property.
+
+##### 6.1.4 Continuation Token of String Type
+
+This options is similar to the previous one but suggests using a string type for the continuation token instead of the `System.ClientModel.ContinuationToken` type.
+
+```csharp
+internal sealed class LongRunContinuationToken
+{
+ public LongRunContinuationToken(string responseId)
+ {
+ this.ResponseId = responseId;
+ }
+
+ public string ResponseId { get; set; }
+
+ public int? SequenceNumber { get; set; }
+
+ public static LongRunContinuationToken Deserialize(string json)
+ {
+ Throw.IfNullOrEmpty(json);
+
+ var token = JsonSerializer.Deserialize(json, OpenAIJsonContext2.Default.LongRunContinuationToken)
+ ?? throw new InvalidOperationException("Failed to deserialize LongRunContinuationToken.");
+
+ return token;
+ }
+
+ public string Serialize()
+ {
+ return JsonSerializer.Serialize(this, OpenAIJsonContext2.Default.LongRunContinuationToken);
+ }
+}
+
+public class ChatOptions
+{
+ public string? ContinuationToken { get; set; }
+}
+```
+
+**Pro:** No dependency on the `System.ClientModel` package.
+
+##### 6.1.5 Continuation Token of a Custom Type
+
+The option is similar the the "6.1.3 Continuation Token of System.ClientModel.ContinuationToken Type" option but suggests using a
+custom type for the continuation token instead of the `System.ClientModel.ContinuationToken` type.
+
+**Pros**
+- There is no dependency on the `System.ClientModel` package.
+- There is no ambiguity between extension methods for `IChatClient` that would occur if a new extension method, which accepts a continuation token of string type as the first parameter, is added.
+
+#### 6.2 Overloads of GetResponseAsync and GetStreamingResponseAsync
+
+This option proposes introducing overloads of the `GetResponseAsync` and `GetStreamingResponseAsync` methods that will accept long-running operation parameters directly:
+
+```csharp
+public interface ILongRunningChatClient
+{
+ Task GetResponseAsync(
+ IEnumerable messages,
+ string responseId,
+ ChatOptions? options = null,
+ CancellationToken cancellationToken = default);
+
+ IAsyncEnumerable GetStreamingResponseAsync(
+ IEnumerable messages,
+ string responseId,
+ string? startAfter = null,
+ ChatOptions? options = null,
+ CancellationToken cancellationToken = default);
+}
+
+public class CustomChatClient : IChatClient, ILongRunningChatClient
+{
+ ...
+}
+
+// Usage example
+IChatClient chatClient = ...; // Get an instance of IChatClient
+
+ChatResponse response = await chatClient.GetResponseAsync("", new ChatOptions { AllowLongRunningResponses = true });
+
+if(response.Status is {} status && chatClient.GetService() is {} longRunningChatClient)
+{
+ while(status != AsyncRunStatus.Completed)
+ {
+ response = await longRunningChatClient.GetResponseAsync([], response.ResponseId, new ChatOptions { ConversationId = response.ConversationId });
+ }
+ ...
+}
+
+```
+
+**Pros:**
+- No proliferation of long-running operation properties in the ChatOptions class, except for the new AllowLongRunningResponses property discussed in section 2.
+
+**Cons:**
+- Interface switching: Callers need to switch to the `ILongRunningChatClient` interface to get the status and result of long-running operations.
+- An alternative solution for decorating the new methods will have to be put in place.
+
+## Long-Running Operations Support for AF Agents
+
+### 1. Methods for Working with Long-Running Operations
+
+The design for supporting long-running operations by agents is very similar to that for chat clients because it is based on
+the same analysis of existing APIs and anticipated consumption patterns.
+
+#### 1.1 Run{Streaming}Async Methods for Common Operations and the Update Operation + New Method Per Uncommon Operation
+
+This option suggests using the existing `Run{Streaming}Async` methods of the `AIAgent` interface implementations to start, get results, and update long-running operations.
+
+For cancellation and deletion of long-running operations, new methods will be added to the `AIAgent` interface implementations.
+
+```csharp
+public abstract class AIAgent
+{
+ // Existing methods...
+ public Task RunAsync(string message, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) { ... }
+ public IAsyncEnumerable RunStreamingAsync(string message, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) { ... }
+
+ // New methods for uncommon operations
+ public virtual Task CancelRunAsync(string id, AgentCancelRunOptions? options = null, CancellationToken cancellationToken = default)
+ {
+ return Task.FromResult(null);
+ }
+
+ public virtual Task DeleteRunAsync(string id, AgentDeleteRunOptions? options = null, CancellationToken cancellationToken = default)
+ {
+ return Task.FromResult(null);
+ }
+}
+
+// Agent that supports update and cancellation
+public class CustomAgent : AIAgent
+{
+ public override async Task CancelRunAsync(string id, AgentCancelRunOptions? options = null, CancellationToken cancellationToken = default)
+ {
+ var response = await this._client.CancelRunAsync(id, options?.Thread?.ConversationId);
+
+ return ConvertToAgentResponse(response);
+ }
+
+ // No overload for DeleteRunAsync as it's not supported by the underlying API
+}
+
+// Usage
+AIAgent agent = new CustomAgent();
+
+AgentThread thread = agent.GetNewThread();
+
+AgentResponse response = await agent.RunAsync("What is the capital of France?");
+
+response = await agent.CancelRunAsync(response.ResponseId, new AgentCancelRunOptions { Thread = thread });
+```
+
+In case an agent supports either or both cancellation and deletion of long-running operations, it will override the corresponding methods.
+Otherwise, it won't override them, and the base implementations will return null by default.
+
+Some agents, for example Azure AI Foundry Agents, require the thread identifier to cancel a run. To accommodate this requirement, the `CancelRunAsync` method
+accepts an optional `AgentCancelRunOptions` parameter that allows callers to specify the thread associated with the run they want to cancel.
+
+```csharp
+public class AgentCancelRunOptions
+{
+ public AgentThread? Thread { get; set; }
+}
+```
+
+Similar design considerations can be applied to the `DeleteRunAsync` method and the `AgentDeleteRunOptions` class.
+
+Having options in the method signatures allows for future extensibility; however, they can be added later if needed to the method overloads.
+
+**Pros:**
+- Existing `Run{Streaming}Async` methods are reused for common operations.
+- New methods for uncommon operations can be added in a non-breaking way.
+
+### 2. Enabling Long-Running Operations
+
+The options for enabling long-running operations are exactly the same as those discussed in section "2. Enabling Long-Running Operations" for chat clients:
+- Execution Mode per `Run{Streaming}Async` Invocation
+- Execution Mode per `Run{Streaming}Async` Invocation + Model Class
+- Execution Mode per agent instance
+- Combined Approach
+
+Below are the details of the option selected for chat clients that is also selected for agents.
+
+#### 2.1 Execution Mode per `Run{Streaming}Async` Invocation
+
+This option proposes adding a new nullable `AllowLongRunningResponses` property of bool type to the `AgentRunOptions` class.
+The property value will be `true` if the caller requests a long-running operation, `false`, `null` or omitted otherwise.
+
+AI agents that work with APIs requiring explicit configuration per operation will use this property to determine whether to run the prompt as a long-running
+operation or quick prompt. Agents that work with APIs that don't require explicit configuration will ignore this property and operate according
+to their own logic/configuration.
+
+```csharp
+public class AgentRunOptions
+{
+ // Existing properties...
+ public bool? AllowLongRunningResponses { get; set; }
+}
+
+// Consumer code example
+AIAgent agent = ...; // Get an instance of an AIAgent
+
+// Start a long-running execution for the prompt if supported by the underlying API
+AgentResponse response = await agent.RunAsync("", new AgentRunOptions { AllowLongRunningResponses = true });
+
+// Start a quick prompt
+AgentResponse response = await agent.RunAsync("");
+```
+
+**Pros:**
+- Callers can switch between quick prompts and long-running operations per invocation of the `Run{Streaming}Async` methods without
+changing agent configuration.
+- Enables explicit control over the execution mode by callers per invocation, meaning that no caller site is broken if the agent is injected via DI,
+and the caller can turn on the long-running operation mode when it can handle it.
+
+**Con:** This may not be valuable for all callers, as they may not have enough information to decide whether the prompt should run as a long-running operation or quick prompt.
+
+### 3. Model To Support Long-Running Operations
+
+The options for modeling long-running operations are exactly the same as those for chat clients discussed in section "6. Model To Support Long-Running Operations" above:
+- Direct Properties in ChatOptions
+- LongRunOptions Model Class
+- Continuation Token of System.ClientModel.ContinuationToken Type
+- Continuation Token of String Type
+- Continuation Token of a Custom Type
+
+Below are the details of the option selected for chat clients that is also selected for agents.
+
+#### 3.1 Continuation Token of a Custom Type
+
+This option suggests using `ContinuationToken` to encapsulate all properties representing a long-running operation. The continuation token will be returned by agents in the
+`ContinuationToken` property of the `AgentResponse` and `AgentResponseUpdate` responses to indicate that the response is part of a long-running operation. A null value
+of the property will indicate that the response is not part of a long-running operation or the long-running operation has been completed. Callers will set the token in the
+`ContinuationToken` property of the `AgentRunOptions` class in follow-up calls to the `Run{Streaming}Async` methods to indicate that they want to "continue" the long-running
+operation identified by the token.
+
+Each agent will implement its own continuation token class that inherits from `ContinuationToken` to encapsulate properties required for long-running operations that are
+specific to the underlying API the agent works with. For example, for the A2A agent, the continuation token class will encapsulate the `TaskId` property.
+
+```csharp
+internal sealed class A2AAgentContinuationToken : ResponseContinuationToken
+{
+ public A2AAgentContinuationToken(string taskId)
+ {
+ this.TaskId = taskId;
+ }
+
+ public string TaskId { get; set; }
+
+ public static LongRunContinuationToken FromToken(ContinuationToken token)
+ {
+ if (token is LongRunContinuationToken longRunContinuationToken)
+ {
+ return longRunContinuationToken;
+ }
+
+ ... // Deserialization logic
+ }
+}
+
+public class AgentRunOptions
+{
+ public ResponseContinuationToken? ContinuationToken { get; set; }
+}
+
+public class AgentResponse
+{
+ public ResponseContinuationToken? ContinuationToken { get; }
+}
+
+public class AgentResponseUpdate
+{
+ public ResponseContinuationToken? ContinuationToken { get; }
+}
+
+// Usage example
+AgentResponse response = await agent.RunAsync("What is the capital of France?");
+
+AgentRunOptions options = new() { ContinuationToken = response.ContinuationToken };
+
+while (response.ContinuationToken is { } token)
+{
+ options.ContinuationToken = token;
+ response = await agent.RunAsync([], options);
+}
+
+Console.WriteLine(response.Text);
+```
+
+### 4. Continuation Token and Agent Thread
+
+There are two types of agent threads: server-managed and client-managed. The server-managed threads live server-side and are identified by a conversation identifier, and
+agents use the identifier to associate runs with the threads. The client-managed threads live client-side and are represented by a collection of chat messages that agents maintain
+by adding user messages to them before sending the thread to the service and by adding the agent response back to the thread when received from the service.
+
+When long-running operations are enabled and an agent is configured with tools, the initial run response may contain a tool call that needs to be invoked by the agent. If the agent runs
+with a server-managed thread, the tool call will be captured as part of the conversation history server-side and follow-up runs will have access to it, and as a result the agent will invoke the tool.
+However, if no thread is provided at the agent's initial run and a client-managed thread is provided for follow-up runs and the agent calls a tool, the tool call which the agent made
+at the initial run will not be added to the client-managed thread since the initial run was made with no thread, and as a result the agent will not be able to invoke the tool.
+
+#### 4.1 Require Thread for Long-Running Operations
+
+This option suggests that AI agents require a thread to be provided when long-running operations are enabled. If no thread is provided, the agent will throw an exception.
+
+**Pro:** Ensures agent responses are always captured by client-managed threads when long-running operations are enabled, providing a consistent experience for callers.
+
+**Con:** May be inconvenient for callers to always provide a thread when long-running operations are enabled.
+
+#### 4.2 Don't Require Thread for Long-Running Operations
+
+This option suggests that AI agents don't require a thread to be provided when long-running operations are enabled. According to this option, it's up to the caller to ensure that
+the thread is provided with background operations consistently for all runs.
+
+**Pro:** Provides more flexibility to callers by not enforcing thread requirements.
+
+**Con:** May lead to an inconsistent experience for callers if they forget to provide the thread for initial or follow-up runs.
+
+## Decision Outcome
+
+### Long-Running Execution Support for Chat Clients
+- **Methods**: Option 1.4 - Use existing `Get{Streaming}ResponseAsync` for common operations; individual interfaces for uncommon operations (e.g., `ICancelableChatClient`)
+- **Enabling**: Option 2.1 - Execution mode per invocation via `ChatOptions.AllowLongRunningResponses`
+- **Status/Result**: Option 3.2 - Single method to get both status and result
+- **RunId/UpdateId**: Option 4.2 - As properties of `ChatResponse{Update}`
+- **Model**: Option 6.1.5 - Custom continuation token type
+
+### Long-Running Operations Support for AF Agents
+- **Methods**: Option 1.1 - Use existing `Run{Streaming}Async` for common operations; new methods for uncommon operations
+- **Enabling**: Option 2.1 - Execution mode per invocation via `AgentRunOptions.AllowLongRunningResponses`
+- **Model**: Option 3.1 - Custom continuation token type
+- **Thread Requirement**: Option 4.1 - Require thread for long-running operations
+
+## Addendum 1: APIs of Agents Supporting Long-Running Execution
+
+OpenAI Responses
+
+- Create a background response and wait for it to complete using polling:
+ ```csharp
+ ClientResult result = await this._openAIResponseClient.CreateResponseAsync("What is SLM in AI?", new ResponseCreationOptions
+ {
+ Background = true,
+ });
+
+ // InProgress, Completed, Cancelled, Queued, Incomplete, Failed
+ while (result.Value.Status is (ResponseStatus.Queued or ResponseStatus.InProgress))
+ {
+ Thread.Sleep(500); // Wait for 0.5 seconds before checking the status again
+ result = await this._openAIResponseClient.GetResponseAsync(result.Value.Id);
+ }
+
+ Console.WriteLine($"Response Status: {result.Value.Status}"); // Completed
+ Console.WriteLine(result.Value.GetOutputText()); // SLM in the context of AI refers to ...
+ ```
+
+- Cancel a background response:
+ ```csharp
+ ...
+ ClientResult result = await this._openAIResponseClient.CreateResponseAsync("What is SLM in AI?", new ResponseCreationOptions
+ {
+ Background = true,
+ });
+
+ result = await this._openAIResponseClient.CancelResponseAsync(result.Value.Id);
+
+ Console.WriteLine($"Response Status: {result.Value.Status}"); // Cancelled
+ ```
+
+- Delete a background response:
+ ```csharp
+ ClientResult result = await this._openAIResponseClient.CreateResponseAsync("What is SLM in AI?", new ResponseCreationOptions
+ {
+ Background = true,
+ });
+
+ ClientResult deleteResult = await this._openAIResponseClient.DeleteResponseAsync(result.Value.Id);
+
+ Console.WriteLine($"Response Deleted: {deleteResult.Value.Deleted}"); // True if the response was deleted successfully
+ ```
+
+- Streaming a background response
+ ```csharp
+ await foreach (StreamingResponseUpdate update in this._openAIResponseClient.CreateResponseStreamingAsync("What is SLM in AI?", new ResponseCreationOptions { Background = true }))
+ {
+ Console.WriteLine($"Sequence Number: {update.SequenceNumber}"); // 0, 1, 2, etc.
+
+ switch (update)
+ {
+ case StreamingResponseCreatedUpdate createdUpdate:
+ Console.WriteLine($"Response Status: {createdUpdate.Response.Status}"); // Queued
+ break;
+ case StreamingResponseQueuedUpdate queuedUpdate:
+ Console.WriteLine($"Response Status: {queuedUpdate.Response.Status}"); // Queued
+ break;
+ case StreamingResponseInProgressUpdate inProgressUpdate:
+ Console.WriteLine($"Response Status: {inProgressUpdate.Response.Status}"); // InProgress
+ break;
+ case StreamingResponseOutputItemAddedUpdate outputItemAddedUpdate:
+ Console.WriteLine($"Output index: {outputItemAddedUpdate.OutputIndex}");
+ Console.WriteLine($"Item Id: {outputItemAddedUpdate.Item.Id}");
+ break;
+ case StreamingResponseContentPartAddedUpdate contentPartAddedUpdate:
+ Console.WriteLine($"Output Index: {contentPartAddedUpdate.OutputIndex}");
+ Console.WriteLine($"Item Id: {contentPartAddedUpdate.ItemId}");
+ Console.WriteLine($"Content Index: {contentPartAddedUpdate.ContentIndex}");
+ break;
+ case StreamingResponseOutputTextDeltaUpdate outputTextDeltaUpdate:
+ Console.WriteLine($"Output Index: {outputTextDeltaUpdate.OutputIndex}");
+ Console.WriteLine($"Item Id: {outputTextDeltaUpdate.ItemId}");
+ Console.WriteLine($"Content Index: {outputTextDeltaUpdate.ContentIndex}");
+ Console.WriteLine($"Delta: {outputTextDeltaUpdate.Delta}"); // SL>M> in> AI> typically>....
+ break;
+ case StreamingResponseOutputTextDoneUpdate outputTextDoneUpdate:
+ Console.WriteLine($"Output Index: {outputTextDoneUpdate.OutputIndex}");
+ Console.WriteLine($"Item Id: {outputTextDoneUpdate.ItemId}");
+ Console.WriteLine($"Content Index: {outputTextDoneUpdate.ContentIndex}");
+ Console.WriteLine($"Text: {outputTextDoneUpdate.Text}"); // SLM in the context of AI typically refers to ...
+ break;
+ case StreamingResponseContentPartDoneUpdate contentPartDoneUpdate:
+ Console.WriteLine($"Output Index: {contentPartDoneUpdate.OutputIndex}");
+ Console.WriteLine($"Item Id: {contentPartDoneUpdate.ItemId}");
+ Console.WriteLine($"Content Index: {contentPartDoneUpdate.ContentIndex}");
+ Console.WriteLine($"Text: {contentPartDoneUpdate.Part.Text}"); // SLM in the context of AI typically refers to ...
+ break;
+ case StreamingResponseOutputItemDoneUpdate outputItemDoneUpdate:
+ Console.WriteLine($"Output Index: {outputItemDoneUpdate.OutputIndex}");
+ Console.WriteLine($"Item Id: {outputItemDoneUpdate.Item.Id}");
+ break;
+ case StreamingResponseCompletedUpdate completedUpdate:
+ Console.WriteLine($"Response Status: {completedUpdate.Response.Status}"); // Completed
+ Console.WriteLine($"Output: {completedUpdate.Response.GetOutputText()}"); // SLM in the context of AI typically refers to ...
+ break;
+ default:
+ Console.WriteLine($"Unexpected update type: {update.GetType().Name}");
+ break;
+ }
+ }
+ ```
+
+ Docs: [OpenAI background mode](https://platform.openai.com/docs/guides/background)
+
+- Background Mode Disabled
+
+ - Non-streaming API - returns the final result
+ | Method Call | Status | Result | Notes |
+ |-------------------------------------|-----------|---------------------------------|-------------------------------------|
+ | CreateResponseAsync(msgs, opts, ct) | Completed | The capital of France is Paris. | |
+ | GetResponseAsync(responseId, ct) | Completed | The capital of France is Paris. | response is less than 5 minutes old |
+ | GetResponseAsync(responseId, ct) | Completed | The capital of France is Paris. | response is more than 5 minutes old |
+ | GetResponseAsync(responseId, ct) | Completed | The capital of France is Paris. | response is more than 12 hours old |
+
+ | Cancellation Method | Result |
+ |---------------------|--------------------------------------|
+ | CancelResponseAsync | Cannot cancel a synchronous response |
+
+ - Streaming API - returns streaming updates callers can iterate over to get the result
+ | Method Call | Status | Result |
+ |----------------------------------------------|------------|----------------------------------------------------------------------------------|
+ | CreateResponseStreamingAsync(msgs, opts, ct) | - | updates |
+ | Iterating over updates | InProgress | - |
+ | Iterating over updates | InProgress | - |
+ | Iterating over updates | InProgress | The |
+ | Iterating over updates | InProgress | capital |
+ | Iterating over updates | InProgress | ... |
+ | Iterating over updates | InProgress | Paris. |
+ | Iterating over updates | Completed | The capital of France is Paris. |
+ | GetStreamingResponseAsync(responseId, ct) | - | HTTP 400 - Response cannot be streamed, it was not created with background=true. |
+
+ | Cancellation Method | Result |
+ |---------------------|--------------------------------------|
+ | CancelResponseAsync | Cannot cancel a synchronous response |
+
+- Background Mode Enabled
+
+ - Non-streaming API - returns queued response immediately and allow polling for the status and result
+ | Method Call | Status | Result | Notes |
+ |-------------------------------------|-----------|---------------------------------|--------------------------------------------|
+ | CreateResponseAsync(msgs, opts, ct) | Queued | responseId | |
+ | GetResponseAsync(responseId, ct) | Queued | - | if called before the response is completed |
+ | GetResponseAsync(responseId, ct) | Queued | - | if called before the response is completed |
+ | GetResponseAsync(responseId, ct) | Completed | The capital of France is Paris. | response is less than 5 minutes old |
+ | GetResponseAsync(responseId, ct) | Completed | The capital of France is Paris. | response is more than 5 minutes old |
+ | GetResponseAsync(responseId, ct) | Completed | The capital of France is Paris. | response is more than 12 hours old |
+
+ The response started in background mode runs server-side until it completes, fails, or is cancelled. The client can poll for
+ the status of the response using its Id. If the client polls before the response is completed, it will get the latest status of the response.
+ If the client polls after the response is completed, it will get the completed response with the result.
+
+ | Cancellation Method | Result | Notes |
+ |---------------------|-----------|----------------------------------------|
+ | CancelResponseAsync | Cancelled | if cancelled before response completed |
+ | CancelResponseAsync | Completed | if cancelled after response completed |
+ | CancellationToken | No effect | it just cancels the client side call |
+
+ - Streaming API - returns streaming updates callers can iterate over immediately or after dropping the stream and picking it up later
+ | Method Call | Status | Result | Notes |
+ |----------------------------------------------|------------|--------------------------------------------------------------------------------|-------------------------------------------|
+ | CreateResponseStreamingAsync(msgs, opts, ct) | - | updates | |
+ | Iterating over updates | Queued | - | |
+ | Iterating over updates | Queued | - | |
+ | Iterating over updates | InProgress | - | |
+ | Iterating over updates | InProgress | - | |
+ | Iterating over updates | InProgress | The | |
+ | Iterating over updates | InProgress | capital | |
+ | Iterating over updates | InProgress | ... | |
+ | Iterating over updates | InProgress | Paris. | |
+ | Iterating over updates | Completed | The capital of France is Paris. | |
+ | GetStreamingResponseAsync(responseId, ct) | - | updates | response is less than 5 minutes old |
+ | Iterating over updates | Queued | - | |
+ | ... | ... | ... | |
+ | GetStreamingResponseAsync(responseId, ct) | - | HTTP 400 - Response can no longer be streamed, it is more than 5 minutes old. | response is more than 5 minutes old |
+ | GetResponseAsync(responseId, ct) | Completed | The capital of France is Paris. | accessing response that can't be streamed |
+
+ The streamed response that is not available after 5 minutes can be retrieved using the non-streaming API `GetResponseAsync`.
+
+ | Cancellation Method | Result | Notes |
+ |---------------------|------------------------------------|----------------------------------------|
+ | CancelResponseAsync | Canceled1 | if cancelled before response completed |
+ | CancelResponseAsync | Cannot cancel a completed response | if cancelled after response completed |
+ | CancellationToken | No effect | it just cancels the client side call |
+
+ 1 The CancelResponseAsync method returns `Canceled` status, but a subsequent call to GetResponseStreamingAsync returns
+ an enumerable that can be iterated over to get the rest of the response until it completes.
+
+
+
+
+Azure AI Foundry Agents
+
+- Create a thread and run the agent against it and wait for it to complete using polling:
+ ```csharp
+ // Create a thread with a message.
+ ThreadMessageOptions options = new(MessageRole.User, "What is SLM in AI?");
+ thread = await this._persistentAgentsClient!.Threads.CreateThreadAsync([options]);
+
+ // Run the agent on the thread.
+ ThreadRun threadRun = await this._persistentAgentsClient.Runs.CreateRunAsync(thread.Id, agent.Id);
+
+ // Poll for the run status.
+ // InProgress, Completed, Cancelling, Cancelled, Queued, Failed, RequiresAction, Expired
+ while (threadRun.Status == RunStatus.InProgress || threadRun.Status == RunStatus.Queued)
+ {
+ threadRun = await this._persistentAgentsClient.Runs.GetRunAsync(thread.Id, threadRun.Id);
+ }
+
+ // Access the run result.
+ await foreach (PersistentThreadMessage msg in this._persistentAgentsClient.Messages.GetMessagesAsync(thread.Id, threadRun.Id))
+ {
+ foreach (MessageContent content in msg.ContentItems)
+ {
+ switch (content)
+ {
+ case MessageTextContent textItem:
+ Console.WriteLine($" Text: {textItem.Text}");
+ //M1: In the context of Artificial Intelligence (AI), **SLM** often ...
+ //M2: What is SLM in AI?
+ break;
+ }
+ }
+ }
+ ```
+
+- Cancel an agent run:
+ ```csharp
+ // Create a thread with a message.
+ ThreadMessageOptions options = new(MessageRole.User, "What is SLM in AI?");
+ thread = await this._persistentAgentsClient!.Threads.CreateThreadAsync([options]);
+
+ // Run the agent on the thread.
+ ThreadRun threadRun = await this._persistentAgentsClient.Runs.CreateRunAsync(thread.Id, agent.Id);
+
+ Response cancellationResponse = await this._persistentAgentsClient.Runs.CancelRunAsync(thread.Id, threadRun.Id);
+ ```
+
+- Other agent run operations:
+ GetRunStepAsync
+
+
+
+
+A2A Agents
+
+- Send message to agent and handle the response
+ ```csharp
+ // Send message to the A2A agent.
+ A2AResponse response = await this.Client.SendMessageAsync(messageSendParams, cancellationToken).ConfigureAwait(false);
+
+ // Handle task responses.
+ if (response is AgentTask task)
+ {
+ while (task.Status.State == TaskState.Working)
+ {
+ task = await this.Client.GetTaskAsync(task.Id, cancellationToken).ConfigureAwait(false);
+ }
+
+ if (task.Artifacts != null && task.Artifacts.Count > 0)
+ {
+ foreach (var artifact in task.Artifacts)
+ {
+ foreach (var part in artifact.Parts)
+ {
+ if (part is TextPart textPart)
+ {
+ Console.WriteLine($"Result: {textPart.Text}");
+ }
+ }
+ }
+ Console.WriteLine();
+ }
+ }
+ // Handle message responses.
+ else if (response is Message message)
+ {
+ foreach (var part in message.Parts)
+ {
+ if (part is TextPart textPart)
+ {
+ Console.WriteLine($"Result: {textPart.Text}");
+ }
+ }
+ }
+ else
+ {
+ throw new InvalidOperationException("Unexpected response type from A2A client.");
+ }
+ ```
+
+- Cancel task
+ ```csharp
+ // Send message to the A2A agent.
+ A2AResponse response = await this.Client.SendMessageAsync(messageSendParams, cancellationToken).ConfigureAwait(false);
+
+ // Cancel the task
+ if (response is AgentTask task)
+ {
+ await this.Client.CancelTaskAsync(new TaskIdParams() { Id = task.Id }, cancellationToken).ConfigureAwait(false);
+ }
+ ```
+
+
\ No newline at end of file
diff --git a/docs/decisions/0010-ag-ui-support.md b/docs/decisions/0010-ag-ui-support.md
new file mode 100644
index 0000000..e1d46e9
--- /dev/null
+++ b/docs/decisions/0010-ag-ui-support.md
@@ -0,0 +1,95 @@
+---
+status: accepted
+contact: javiercn
+date: 2025-10-29
+deciders: javiercn, DeagleGross, moonbox3, markwallace-microsoft
+consulted: Agent Framework team
+informed: .NET community
+---
+
+# AG-UI Protocol Support for .NET Agent Framework
+
+## Context and Problem Statement
+
+The .NET Agent Framework needed a standardized way to enable communication between AI agents and user-facing applications with support for streaming, real-time updates, and bidirectional communication. Without AG-UI protocol support, .NET agents could not interoperate with the growing ecosystem of AG-UI-compatible frontends and agent frameworks (LangGraph, CrewAI, Pydantic AI, etc.), limiting the framework's adoption and utility.
+
+The AG-UI (Agent-User Interaction) protocol is an open, lightweight, event-based protocol that addresses key challenges in agentic applications including streaming support for long-running agents, event-driven architecture for nondeterministic behavior, and protocol interoperability that complements MCP (tool/context) and A2A (agent-to-agent) protocols.
+
+## Decision Drivers
+
+- Need for streaming communication between agents and client applications
+- Requirement for protocol interoperability with other AI frameworks
+- Support for long-running, multi-turn conversation sessions
+- Real-time UI updates for nondeterministic agent behavior
+- Standardized approach to agent-to-UI communication
+- Framework abstraction to protect consumers from protocol changes
+
+## Considered Options
+
+1. **Implement AG-UI event types as public API surface** - Expose AG-UI event models directly to consumers
+2. **Use custom AIContent types for lifecycle events** - Create new content types (RunStartedContent, RunFinishedContent, RunErrorContent)
+3. **Current approach** - Internal event types with framework-native abstractions
+
+## Decision Outcome
+
+Chosen option: "Current approach with internal event types and framework-native abstractions", because it:
+
+- Protects consumers from protocol changes by keeping AG-UI events internal
+- Maintains framework abstractions through conversion at boundaries
+- Uses existing framework types (AgentResponseUpdate, ChatMessage) for public API
+- Focuses on core text streaming functionality
+- Leverages existing properties (ConversationId, ResponseId, ErrorContent) instead of custom types
+- Provides bidirectional client and server support
+
+### Implementation Details
+
+**In Scope:**
+1. **Client-side AG-UI consumption** (`Microsoft.Agents.AI.AGUI` package)
+ - `AGUIAgent` class for connecting to remote AG-UI servers
+ - `AGUIAgentThread` for managing conversation threads
+ - HTTP/SSE streaming support
+ - Event-to-framework type conversion
+
+2. **Server-side AG-UI hosting** (`Microsoft.Agents.AI.Hosting.AGUI.AspNetCore` package)
+ - `MapAGUIAgent` extension method for ASP.NET Core
+ - Server-Sent Events (SSE) response formatting
+ - Framework-to-event type conversion
+ - Agent factory pattern for per-request instantiation
+
+3. **Text streaming events**
+ - Lifecycle events: `RunStarted`, `RunFinished`, `RunError`
+ - Text message events: `TextMessageStart`, `TextMessageContent`, `TextMessageEnd`
+ - Thread and run ID management via `ConversationId` and `ResponseId`
+
+### Key Design Decisions
+
+1. **Event Models as Internal Types** - AG-UI event types are internal with conversion via extension methods; public API uses the existing types in Microsoft.Extensions.AI as those are the abstractions people are familiar with
+
+2. **No Custom Content Types** - Run lifecycle communicated through existing `ChatResponseUpdate` properties (`ConversationId`, `ResponseId`) and standard `ErrorContent` type
+
+3. **Agent Factory Pattern** - `MapAGUIAgent` uses factory function `(messages) => AIAgent` to allow request-specific agent configuration supporting multi-tenancy
+
+4. **Bidirectional Conversion Architecture** - Symmetric conversion logic in shared namespace compiled into both packages for server (`AgentResponseUpdate` → AG-UI events) and client (AG-UI events → `AgentResponseUpdate`)
+
+5. **Thread Management** - `AGUIAgentThread` stores only `ThreadId` with thread ID communicated via `ConversationId`; applications manage persistence for parity with other implementations and to be compliant with the protocol. Future extensions will support having the server manage the conversation.
+
+6. **Custom JSON Converter** - Uses custom polymorphic deserialization via `BaseEventJsonConverter` instead of built-in System.Text.Json support to handle AG-UI protocol's flexible discriminator positioning
+
+### Consequences
+
+**Positive:**
+- .NET developers can consume AG-UI servers from any framework
+- .NET agents accessible from any AG-UI-compatible client
+- Standardized streaming communication patterns
+- Protected from protocol changes through internal implementation
+- Symmetric conversion logic between client and server
+- Framework-native public API surface
+
+**Negative:**
+- Custom JSON converter required (internal implementation detail)
+- Shared code uses preprocessor directives (`#if ASPNETCORE`)
+- Additional abstraction layer between protocol and public API
+
+**Neutral:**
+- Initial implementation focused on text streaming
+- Applications responsible for thread persistence
diff --git a/docs/decisions/0011-create-get-agent-api.md b/docs/decisions/0011-create-get-agent-api.md
new file mode 100644
index 0000000..4703c12
--- /dev/null
+++ b/docs/decisions/0011-create-get-agent-api.md
@@ -0,0 +1,368 @@
+---
+status: proposed
+contact: dmytrostruk
+date: 2025-12-12
+deciders: dmytrostruk, markwallace-microsoft, eavanvalkenburg, giles17
+---
+
+# Create/Get Agent API
+
+## Context and Problem Statement
+
+There is a misalignment between the create/get agent API in the .NET and Python implementations.
+
+In .NET, the `CreateAIAgent` method can create either a local instance of an agent or a remote instance if the backend provider supports it. For remote agents, once the agent is created, you can retrieve an existing remote agent by using the `GetAIAgent` method. If a backend provider doesn't support remote agents, `CreateAIAgent` just initializes a new local agent instance and `GetAIAgent` is not available. There is also a `BuildAIAgent` method, which is an extension for the `ChatClientBuilder` class from `Microsoft.Extensions.AI`. It builds pipelines of `IChatClient` instances with an `IServiceProvider`. This functionality does not exist in Python, so `BuildAIAgent` is out of scope.
+
+In Python, there is only one `create_agent` method, which always creates a local instance of the agent. If the backend provider supports remote agents, the remote agent is created only on the first `agent.run()` invocation.
+
+Below is a short summary of different providers and their APIs in .NET:
+
+| Package | Method | Behavior | Python support |
+|---|---|---|---|
+| Microsoft.Agents.AI | `CreateAIAgent` (based on `IChatClient`) | Creates a local instance of `ChatClientAgent`. | Yes (`create_agent` in `BaseChatClient`). |
+| Microsoft.Agents.AI.Anthropic | `CreateAIAgent` (based on `IBetaService` and `IAnthropicClient`) | Creates a local instance of `ChatClientAgent`. | Yes (`AnthropicClient` inherits `BaseChatClient`, which exposes `create_agent`). |
+| Microsoft.Agents.AI.AzureAI (V2) | `GetAIAgent` (based on `AIProjectClient` with `AgentReference`) | Creates a local instance of `ChatClientAgent`. | Partial (Python uses `create_agent` from `BaseChatClient`). |
+| Microsoft.Agents.AI.AzureAI (V2) | `GetAIAgent`/`GetAIAgentAsync` (with `Name`/`ChatClientAgentOptions`) | Fetches `AgentRecord` via HTTP, then creates a local `ChatClientAgent` instance. | No |
+| Microsoft.Agents.AI.AzureAI (V2) | `CreateAIAgent`/`CreateAIAgentAsync` (based on `AIProjectClient`) | Creates a remote agent first, then wraps it into a local `ChatClientAgent` instance. | No |
+| Microsoft.Agents.AI.AzureAI.Persistent (V1) | `GetAIAgent` (based on `PersistentAgentsClient` with `PersistentAgent`) | Creates a local instance of `ChatClientAgent`. | Partial (Python uses `create_agent` from `BaseChatClient`). |
+| Microsoft.Agents.AI.AzureAI.Persistent (V1) | `GetAIAgent`/`GetAIAgentAsync` (with `AgentId`) | Fetches `PersistentAgent` via HTTP, then creates a local `ChatClientAgent` instance. | No |
+| Microsoft.Agents.AI.AzureAI.Persistent (V1) | `CreateAIAgent`/`CreateAIAgentAsync` | Creates a remote agent first, then wraps it into a local `ChatClientAgent` instance. | No |
+| Microsoft.Agents.AI.OpenAI | `GetAIAgent` (based on `AssistantClient` with `Assistant`) | Creates a local instance of `ChatClientAgent`. | Partial (Python uses `create_agent` from `BaseChatClient`). |
+| Microsoft.Agents.AI.OpenAI | `GetAIAgent`/`GetAIAgentAsync` (with `AgentId`) | Fetches `Assistant` via HTTP, then creates a local `ChatClientAgent` instance. | No |
+| Microsoft.Agents.AI.OpenAI | `CreateAIAgent`/`CreateAIAgentAsync` (based on `AssistantClient`) | Creates a remote agent first, then wraps it into a local `ChatClientAgent` instance. | No |
+| Microsoft.Agents.AI.OpenAI | `CreateAIAgent` (based on `ChatClient`) | Creates a local instance of `ChatClientAgent`. | Yes (`create_agent` in `BaseChatClient`). |
+| Microsoft.Agents.AI.OpenAI | `CreateAIAgent` (based on `OpenAIResponseClient`) | Creates a local instance of `ChatClientAgent`. | Yes (`create_agent` in `BaseChatClient`). |
+
+Another difference between Python and .NET implementation is that in .NET `CreateAIAgent`/`GetAIAgent` methods are implemented as extension methods based on underlying SDK client, like `AIProjectClient` from Azure AI or `AssistantClient` from OpenAI:
+
+```csharp
+// Definition
+public static ChatClientAgent CreateAIAgent(
+ this AIProjectClient aiProjectClient,
+ string name,
+ string model,
+ string instructions,
+ string? description = null,
+ IList? tools = null,
+ Func? clientFactory = null,
+ IServiceProvider? services = null,
+ CancellationToken cancellationToken = default)
+{ }
+
+// Usage
+AIProjectClient aiProjectClient = new(new Uri(endpoint), new AzureCliCredential()); // Initialization of underlying SDK client
+
+var newAgent = await aiProjectClient.CreateAIAgentAsync(name: AgentName, model: deploymentName, instructions: AgentInstructions, tools: [tool]); // ChatClientAgent creation from underlying SDK client
+
+// Alternative usage (same as extension method, just explicit syntax)
+var newAgent = await AzureAIProjectChatClientExtensions.CreateAIAgentAsync(
+ aiProjectClient,
+ name: AgentName,
+ model: deploymentName,
+ instructions: AgentInstructions,
+ tools: [tool]);
+```
+
+Python doesn't support extension methods. Currently `create_agent` method is defined on `BaseChatClient`, but this method only creates a local instance of `ChatAgent` and it can't create remote agents for providers that support it for a couple of reasons:
+
+- It's defined as non-async.
+- `BaseChatClient` implementation is stateful for providers like Azure AI or OpenAI Assistants. The implementation stores agent/assistant metadata like `AgentId` and `AgentName`, so currently it's not possible to create different instances of `ChatAgent` from a single `BaseChatClient` in case if the implementation is stateful.
+
+## Decision Drivers
+
+- API should be aligned between .NET and Python.
+- API should be intuitive and consistent between backend providers in .NET and Python.
+
+## Considered Options
+
+Add missing implementations on the Python side. This should include the following:
+
+### agent-framework-azure-ai (both V1 and V2)
+
+- Add a `get_agent` method that accepts an underlying SDK agent instance and creates a local instance of `ChatAgent`.
+- Add a `get_agent` method that accepts an agent identifier, performs an additional HTTP request to fetch agent data, and then creates a local instance of `ChatAgent`.
+- Override the `create_agent` method from `BaseChatClient` to create a remote agent instance and wrap it into a local `ChatAgent`.
+
+.NET:
+
+```csharp
+var agent1 = new AIProjectClient(...).GetAIAgent(agentInstanceFromSdkType); // Creates a local ChatClientAgent instance from Azure.AI.Projects.OpenAI.AgentReference
+var agent2 = new AIProjectClient(...).GetAIAgent(agentName); // Fetches agent data, creates a local ChatClientAgent instance
+var agent3 = new AIProjectClient(...).CreateAIAgent(...); // Creates a remote agent, returns a local ChatClientAgent instance
+```
+
+### agent-framework-core (OpenAI Assistants)
+
+- Add a `get_agent` method that accepts an underlying SDK agent instance and creates a local instance of `ChatAgent`.
+- Add a `get_agent` method that accepts an agent name, performs an additional HTTP request to fetch agent data, and then creates a local instance of `ChatAgent`.
+- Override the `create_agent` method from `BaseChatClient` to create a remote agent instance and wrap it into a local `ChatAgent`.
+
+.NET:
+
+```csharp
+var agent1 = new AssistantClient(...).GetAIAgent(agentInstanceFromSdkType); // Creates a local ChatClientAgent instance from OpenAI.Assistants.Assistant
+var agent2 = new AssistantClient(...).GetAIAgent(agentId); // Fetches agent data, creates a local ChatClientAgent instance
+var agent3 = new AssistantClient(...).CreateAIAgent(...); // Creates a remote agent, returns a local ChatClientAgent instance
+```
+
+### Possible Python implementations
+
+Methods like `create_agent` and `get_agent` should be implemented separately or defined on some stateless component that will allow to create multiple agents from the same instance/place.
+
+Possible options:
+
+#### Option 1: Module-level functions
+
+Implement free functions in the provider package that accept the underlying SDK client as the first argument (similar to .NET extension methods, but expressed in Python).
+
+Example:
+
+```python
+from agent_framework.azure import create_agent, get_agent
+
+ai_project_client = AIProjectClient(...)
+
+# Creates a remote agent first, then returns a local ChatAgent wrapper
+created_agent = await create_agent(
+ ai_project_client,
+ name="",
+ instructions="",
+ tools=[tool],
+)
+
+# Gets an existing remote agent and returns a local ChatAgent wrapper
+first_agent = await get_agent(ai_project_client, agent_id=agent_id)
+
+# Wraps an SDK agent instance (no extra HTTP call)
+second_agent = get_agent(ai_project_client, agent_reference)
+```
+
+Pros:
+
+- Naturally supports async `create_agent` / `get_agent`.
+- Supports multiple agents per SDK client.
+- Closest conceptual match to .NET extension methods while staying Pythonic.
+
+Cons:
+
+- Discoverability is lower (users need to know where the functions live).
+- Verbose when creating multiple agents (client must be passed every time):
+
+ ```python
+ agent1 = await azure_agents.create_agent(client, name="Agent1", ...)
+ agent2 = await azure_agents.create_agent(client, name="Agent2", ...)
+ ```
+
+#### Option 2: Provider object
+
+Introduce a dedicated provider type that is constructed from the underlying SDK client, and exposes async `create_agent` / `get_agent` methods.
+
+Example:
+
+```python
+from agent_framework.azure import AzureAIAgentProvider
+
+ai_project_client = AIProjectClient(...)
+provider = AzureAIAgentProvider(ai_project_client)
+
+agent = await provider.create_agent(
+ name="",
+ instructions="",
+ tools=[tool],
+)
+
+agent = await provider.get_agent(agent_id=agent_id)
+agent = provider.get_agent(agent_reference=agent_reference)
+```
+
+Pros:
+
+- High discoverability and clear grouping of related behavior.
+- Keeps SDK clients unchanged and supports multiple agents per SDK client.
+- Concise when creating multiple agents (client passed once):
+
+ ```python
+ provider = AzureAIAgentProvider(ai_project_client)
+ agent1 = await provider.create_agent(name="Agent1", ...)
+ agent2 = await provider.create_agent(name="Agent2", ...)
+ ```
+
+Cons:
+
+- Adds a new public concept/type for users to learn.
+
+#### Option 3: Inheritance (SDK client subclass)
+
+Create a subclass of the underlying SDK client and add `create_agent` / `get_agent` methods.
+
+Example:
+
+```python
+class ExtendedAIProjectClient(AIProjectClient):
+ async def create_agent(self, *, name: str, model: str, instructions: str, **kwargs) -> ChatAgent:
+ ...
+
+ async def get_agent(self, *, agent_id: str | None = None, sdk_agent=None, **kwargs) -> ChatAgent:
+ ...
+
+client = ExtendedAIProjectClient(...)
+agent = await client.create_agent(name="", instructions="")
+```
+
+Pros:
+
+- Discoverable and ergonomic call sites.
+- Mirrors the .NET “methods on the client” feeling.
+
+Cons:
+
+- Many SDK clients are not designed for inheritance; SDK upgrades can break subclasses.
+- Users must opt into subclass everywhere.
+- Typing/initialization can be tricky if the SDK client has non-trivial constructors.
+
+#### Option 4: Monkey patching
+
+Attach `create_agent` / `get_agent` methods to an SDK client class (or instance) at runtime.
+
+Example:
+
+```python
+def _create_agent(self, *, name: str, model: str, instructions: str, **kwargs) -> ChatAgent:
+ ...
+
+AIProjectClient.create_agent = _create_agent # monkey patch
+```
+
+Pros:
+
+- Produces “extension method-like” call sites without wrappers or subclasses.
+
+Cons:
+
+- Fragile across SDK updates and difficult to type-check.
+- Surprising behavior (global side effects), potential conflicts across packages.
+- Harder to support/debug, especially in larger apps and test suites.
+
+## Decision Outcome
+
+Implement `create_agent`/`get_agent`/`as_agent` API via **Option 2: Provider object**.
+
+### Rationale
+
+| Aspect | Option 1 (Functions) | Option 2 (Provider) |
+|--------|----------------------|---------------------|
+| Multiple implementations | One package may contain V1, V2, and other agent types. Function names like `create_agent` become ambiguous - which agent type does it create? | Each provider class is explicit: `AzureAIAgentsProvider` vs `AzureAIProjectAgentProvider` |
+| Discoverability | Users must know to import specific functions from the package | IDE autocomplete on provider instance shows all available methods |
+| Client reuse | SDK client must be passed to every function call: `create_agent(client, ...)`, `get_agent(client, ...)` | SDK client passed once at construction: `provider = Provider(client)` |
+
+**Option 1 example:**
+```python
+from agent_framework.azure import create_agent, get_agent
+agent1 = await create_agent(client, name="Agent1", ...) # Which agent type, V1 or V2?
+agent2 = await create_agent(client, name="Agent2", ...) # Repetitive client passing
+```
+
+**Option 2 example:**
+```python
+from agent_framework.azure import AzureAIProjectAgentProvider
+provider = AzureAIProjectAgentProvider(client) # Clear which service, client passed once
+agent1 = await provider.create_agent(name="Agent1", ...)
+agent2 = await provider.create_agent(name="Agent2", ...)
+```
+
+### Method Naming
+
+| Operation | Python | .NET | Async |
+|-----------|--------|------|-------|
+| Create on service | `create_agent()` | `CreateAIAgent()` | Yes |
+| Get from service | `get_agent(id=...)` | `GetAIAgent(agentId)` | Yes |
+| Wrap SDK object | `as_agent(reference)` | `AsAIAgent(agentInstance)` | No |
+
+The method names (`create_agent`, `get_agent`) do not explicitly mention "service" or "remote" because:
+- In Python, the provider class name explicitly identifies the service (`AzureAIAgentsProvider`, `OpenAIAssistantProvider`), making additional qualifiers in method names redundant.
+- In .NET, these are extension methods on `AIProjectClient` or `AssistantClient`, which already imply service operations.
+
+### Provider Class Naming
+
+| Package | Provider Class | SDK Client | Service |
+|---------|---------------|------------|---------|
+| `agent_framework.azure` | `AzureAIProjectAgentProvider` | `AIProjectClient` | Azure AI Agent Service, based on Responses API (V2) |
+| `agent_framework.azure` | `AzureAIAgentsProvider` | `AgentsClient` | Azure AI Agent Service (V1) |
+| `agent_framework.openai` | `OpenAIAssistantProvider` | `AsyncOpenAI` | OpenAI Assistants API |
+
+> **Note:** Azure AI naming is temporary. Final naming will be updated according to Azure AI / Microsoft Foundry renaming decisions.
+
+### Usage Examples
+
+#### Azure AI Agent Service V2 (based on Responses API)
+
+```python
+from agent_framework.azure import AzureAIProjectAgentProvider
+from azure.ai.projects import AIProjectClient
+
+client = AIProjectClient(endpoint, credential)
+provider = AzureAIProjectAgentProvider(client)
+
+# Create new agent on service
+agent = await provider.create_agent(name="MyAgent", model="gpt-4", instructions="...")
+
+# Get existing agent by name
+agent = await provider.get_agent(agent_name="MyAgent")
+
+# Wrap already-fetched SDK object (no HTTP calls)
+agent_ref = await client.agents.get("MyAgent")
+agent = provider.as_agent(agent_ref)
+```
+
+#### Azure AI Persistent Agents V1
+
+```python
+from agent_framework.azure import AzureAIAgentsProvider
+from azure.ai.agents import AgentsClient
+
+client = AgentsClient(endpoint, credential)
+provider = AzureAIAgentsProvider(client)
+
+agent = await provider.create_agent(name="MyAgent", model="gpt-4", instructions="...")
+agent = await provider.get_agent(agent_id="persistent-agent-456")
+agent = provider.as_agent(persistent_agent)
+```
+
+#### OpenAI Assistants
+
+```python
+from agent_framework.openai import OpenAIAssistantProvider
+from openai import OpenAI
+
+client = OpenAI()
+provider = OpenAIAssistantProvider(client)
+
+agent = await provider.create_agent(name="MyAssistant", model="gpt-4", instructions="...")
+agent = await provider.get_agent(assistant_id="asst_123")
+agent = provider.as_agent(assistant)
+```
+
+#### Local-Only Agents (No Provider)
+
+Current method `create_agent` (python) / `CreateAIAgent` (.NET) can be renamed to `as_agent` (python) / `AsAIAgent` (.NET) to emphasize the conversion logic rather than creation/initialization logic and to avoid collision with `create_agent` method for remote calls.
+
+```python
+from agent_framework import ChatAgent
+from agent_framework.openai import OpenAIChatClient
+
+# Convert chat client to ChatAgent (no remote service involved)
+client = OpenAIChatClient(model="gpt-4")
+agent = client.as_agent(name="LocalAgent", instructions="...") # instead of create_agent
+```
+
+### Adding New Agent Types
+
+Python:
+
+1. Create provider class in appropriate package.
+2. Implement `create_agent`, `get_agent`, `as_agent` as applicable.
+
+.NET:
+
+1. Create static class for extension methods.
+2. Implement `CreateAIAgentAsync`, `GetAIAgentAsync`, `AsAIAgent` as applicable.
diff --git a/docs/decisions/0012-python-typeddict-options.md b/docs/decisions/0012-python-typeddict-options.md
new file mode 100644
index 0000000..09657b2
--- /dev/null
+++ b/docs/decisions/0012-python-typeddict-options.md
@@ -0,0 +1,129 @@
+---
+# These are optional elements. Feel free to remove any of them.
+status: proposed
+contact: eavanvalkenburg
+date: 2026-01-08
+deciders: eavanvalkenburg, markwallace-microsoft, sphenry, alliscode, johanst, brettcannon
+consulted: taochenosu, moonbox3, dmytrostruk, giles17
+---
+
+# Leveraging TypedDict and Generic Options in Python Chat Clients
+
+## Context and Problem Statement
+
+The Agent Framework Python SDK provides multiple chat client implementations for different providers (OpenAI, Anthropic, Azure AI, Bedrock, Ollama, etc.). Each provider has unique configuration options beyond the common parameters defined in `ChatOptions`. Currently, developers using these clients lack type safety and IDE autocompletion for provider-specific options, leading to runtime errors and a poor developer experience.
+
+How can we provide type-safe, discoverable options for each chat client while maintaining a consistent API across all implementations?
+
+## Decision Drivers
+
+- **Type Safety**: Developers should get compile-time/static analysis errors when using invalid options
+- **IDE Support**: Full autocompletion and inline documentation for all available options
+- **Extensibility**: Users should be able to define custom options that extend provider-specific options
+- **Consistency**: All chat clients should follow the same pattern for options handling
+- **Provider Flexibility**: Each provider can expose its unique options without affecting the common interface
+
+## Considered Options
+
+- **Option 1: Status Quo - Class `ChatOptions` with `**kwargs`**
+- **Option 2: TypedDict with Generic Type Parameters**
+
+### Option 1: Status Quo - Class `ChatOptions` with `**kwargs`
+
+The current approach uses a base `ChatOptions` Class with common parameters, and provider-specific options are passed via `**kwargs` or loosely typed dictionaries.
+
+```python
+# Current usage - no type safety for provider-specific options
+response = await client.get_response(
+ messages=messages,
+ temperature=0.7,
+ top_k=40,
+ random=42, # No validation
+)
+```
+
+**Pros:**
+- Simple implementation
+- Maximum flexibility
+
+**Cons:**
+- No type checking for provider-specific options
+- No IDE autocompletion for available options
+- Runtime errors for typos or invalid options
+- Documentation must be consulted for each provider
+
+### Option 2: TypedDict with Generic Type Parameters (Chosen)
+
+Each chat client is parameterized with a TypeVar bound to a provider-specific `TypedDict` that extends `ChatOptions`. This enables full type safety and IDE support.
+
+```python
+# Provider-specific TypedDict
+class AnthropicChatOptions(ChatOptions, total=False):
+ """Anthropic-specific chat options."""
+ top_k: int
+ thinking: ThinkingConfig
+ # ... other Anthropic-specific options
+
+# Generic chat client
+class AnthropicChatClient(ChatClientBase[TAnthropicChatOptions]):
+ ...
+
+client = AnthropicChatClient(...)
+
+# Usage with full type safety
+response = await client.get_response(
+ messages=messages,
+ options={
+ "temperature": 0.7,
+ "top_k": 40,
+ "random": 42, # fails type checking and IDE would flag this
+ }
+)
+
+# Users can extend for custom options
+class MyAnthropicOptions(AnthropicChatOptions, total=False):
+ custom_field: str
+
+
+client = AnthropicChatClient[MyAnthropicOptions](...)
+
+# Usage of custom options with full type safety
+response = await client.get_response(
+ messages=messages,
+ options={
+ "temperature": 0.7,
+ "top_k": 40,
+ "custom_field": "value",
+ }
+)
+
+```
+
+**Pros:**
+- Full type safety with static analysis
+- IDE autocompletion for all options
+- Compile-time error detection
+- Self-documenting through type hints
+- Users can extend options for their specific needs or advances in models
+
+**Cons:**
+- More complex implementation
+- Some type: ignore comments needed for TypedDict field overrides
+- Minor: Requires TypeVar with default (Python 3.13+ or typing_extensions)
+
+> [NOTE!]
+> In .NET this is already achieved through overloads on the `GetResponseAsync` method for each provider-specific options class, e.g., `AnthropicChatOptions`, `OpenAIChatOptions`, etc. So this does not apply to .NET.
+
+### Implementation Details
+
+1. **Base Protocol**: `ChatClientProtocol[TOptions]` is generic over options type, with default set to `ChatOptions` (the new TypedDict)
+2. **Provider TypedDicts**: Each provider defines its options extending `ChatOptions`
+ They can even override fields with type=None to indicate they are not supported.
+3. **TypeVar Pattern**: `TProviderOptions = TypeVar("TProviderOptions", bound=TypedDict, default=ProviderChatOptions, contravariant=True)`
+4. **Option Translation**: Common options are kept in place,and explicitly documented in the Options class how they are used. (e.g., `user` → `metadata.user_id`) in `_prepare_options` (for Anthropic) to preserve easy use of common options.
+
+## Decision Outcome
+
+Chosen option: **"Option 2: TypedDict with Generic Type Parameters"**, because it provides full type safety, excellent IDE support with autocompletion, and allows users to extend provider-specific options for their use cases. Extended this Generic to ChatAgents in order to also properly type the options used in agent construction and run methods.
+
+See [typed_options.py](../../python/samples/getting_started/chat_client/typed_options.py) for a complete example demonstrating the usage of typed options with custom extensions.
diff --git a/docs/decisions/0013-python-get-response-simplification.md b/docs/decisions/0013-python-get-response-simplification.md
new file mode 100644
index 0000000..2c3965e
--- /dev/null
+++ b/docs/decisions/0013-python-get-response-simplification.md
@@ -0,0 +1,258 @@
+---
+status: Accepted
+contact: eavanvalkenburg
+date: 2026-01-06
+deciders: markwallace-microsoft, dmytrostruk, taochenosu, alliscode, moonbox3, sphenry
+consulted: sergeymenshykh, rbarreto, dmytrostruk, westey-m
+informed:
+---
+
+# Simplify Python Get Response API into a single method
+
+## Context and Problem Statement
+
+Currently chat clients must implement two separate methods to get responses, one for streaming and one for non-streaming. This adds complexity to the client implementations and increases the maintenance burden. This was likely done because the .NET version cannot do proper typing with a single method, in Python this is possible and this for instance is also how the OpenAI python client works, this would then also make it simpler to work with the Python version because there is only one method to learn about instead of two.
+
+## Implications of this change
+
+### Current Architecture Overview
+
+The current design has **two separate methods** at each layer:
+
+| Layer | Non-streaming | Streaming |
+|-------|---------------|-----------|
+| **Protocol** | `get_response()` → `ChatResponse` | `get_streaming_response()` → `AsyncIterable[ChatResponseUpdate]` |
+| **BaseChatClient** | `get_response()` (public) | `get_streaming_response()` (public) |
+| **Implementation** | `_inner_get_response()` (private) | `_inner_get_streaming_response()` (private) |
+
+### Key Usage Areas Identified
+
+#### 1. **ChatAgent** (_agents.py)
+- `run()` → calls `self.chat_client.get_response()`
+- `run_stream()` → calls `self.chat_client.get_streaming_response()`
+
+These are parallel methods on the agent, so consolidating the client methods would **not break** the agent API. You could keep `agent.run()` and `agent.run_stream()` unchanged while internally calling `get_response(stream=True/False)`.
+
+#### 2. **Function Invocation Decorator** (_tools.py)
+This is **the most impacted area**. Currently:
+- `_handle_function_calls_response()` decorates `get_response`
+- `_handle_function_calls_streaming_response()` decorates `get_streaming_response`
+- The `use_function_invocation` class decorator wraps **both methods separately**
+
+**Impact**: The decorator logic is almost identical (~200 lines each) with small differences:
+- Non-streaming collects response, returns it
+- Streaming yields updates, returns async iterable
+
+With a unified method, you'd need **one decorator** that:
+- Checks the `stream` parameter
+- Uses `@overload` to determine return type
+- Handles both paths with conditional logic
+- The new decorator could be applied just on the method, instead of the whole class.
+
+This would **reduce code duplication** but add complexity to a single function.
+
+#### 3. **Observability/Instrumentation** (observability.py)
+Same pattern as function invocation:
+- `_trace_get_response()` wraps `get_response`
+- `_trace_get_streaming_response()` wraps `get_streaming_response`
+- `use_instrumentation` decorator applies both
+
+**Impact**: Would need consolidation into a single tracing wrapper.
+
+#### 4. **Chat Middleware** (_middleware.py)
+The `use_chat_middleware` decorator also wraps both methods separately with similar logic.
+
+#### 5. **AG-UI Client** (_client.py)
+Wraps both methods to unwrap server function calls:
+```python
+original_get_streaming_response = chat_client.get_streaming_response
+original_get_response = chat_client.get_response
+```
+
+#### 6. **Provider Implementations** (all subpackages)
+All subclasses implement both `_inner_*` methods, except:
+- OpenAI Assistants Client (and similar clients, such as Foundry Agents V1) - it implements `_inner_get_response` by calling `_inner_get_streaming_response`
+
+### Implications of Consolidation
+
+| Aspect | Impact |
+|--------|--------|
+| **Type Safety** | Overloads work well: `@overload` with `Literal[True]` → `AsyncIterable`, `Literal[False]` → `ChatResponse`. Runtime return type based on `stream` param. |
+| **Breaking Change** | **Major breaking change** for anyone implementing custom chat clients. They'd need to update from 2 methods to 1 (or 2 inner methods to 1). |
+| **Decorator Complexity** | All 3 decorator systems (function invocation, middleware, observability) would need refactoring to handle both paths in one wrapper. |
+| **Code Reduction** | Significant reduction in _tools.py (~200 lines of near-duplicate code) and other decorators. |
+| **Samples/Tests** | Many samples call `get_streaming_response()` directly - would need updates. |
+| **Protocol Simplification** | `ChatClientProtocol` goes from 2 methods + 1 property to 1 method + 1 property. |
+
+### Recommendation
+
+The consolidation makes sense architecturally, but consider:
+
+1. **The overload pattern with `stream: bool`** works well in Python typing:
+ ```python
+ @overload
+ async def get_response(self, messages, *, stream: Literal[True] = True, ...) -> AsyncIterable[ChatResponseUpdate]: ...
+ @overload
+ async def get_response(self, messages, *, stream: Literal[False] = False, ...) -> ChatResponse: ...
+ ```
+
+2. **The decorator complexity** is the biggest concern. The current approach of separate decorators for separate methods is cleaner than conditional logic inside one wrapper.
+
+## Decision Drivers
+
+- Reduce code needed to implement a Chat Client, simplify the public API for chat clients
+- Reduce code duplication in decorators and middleware
+- Maintain type safety and clarity in method signatures
+
+## Considered Options
+
+1. Status quo: Keep separate methods for streaming and non-streaming
+2. Consolidate into a single `get_response` method with a `stream` parameter
+3. Option 2 plus merging `agent.run` and `agent.run_stream` into a single method with a `stream` parameter as well
+
+## Option 1: Status Quo
+- Good: Clear separation of streaming vs non-streaming logic
+- Good: Aligned with .NET design, although it is already `run` for Python and `RunAsync` for .NET
+- Bad: Code duplication in decorators and middleware
+- Bad: More complex client implementations
+
+## Option 2: Consolidate into Single Method
+- Good: Simplified public API for chat clients
+- Good: Reduced code duplication in decorators
+- Good: Smaller API footprint for users to get familiar with
+- Good: People using OpenAI directly already expect this pattern
+- Bad: Increased complexity in decorators and middleware
+- Bad: Less alignment with .NET design (`get_response(stream=True)` vs `GetStreamingResponseAsync`)
+
+## Option 3: Consolidate + Merge Agent and Workflow Methods
+- Good: Further simplifies agent and workflow implementation
+- Good: Single method for all chat interactions
+- Good: Smaller API footprint for users to get familiar with
+- Good: People using OpenAI directly already expect this pattern
+- Good: Workflows internally already use a single method (_run_workflow_with_tracing), so would eliminate public API duplication as well, with hardly any code changes
+- Bad: More breaking changes for agent users
+- Bad: Increased complexity in agent implementation
+- Bad: More extensive misalignment with .NET design (`run(stream=True)` vs `RunStreamingAsync` in addition to `get_response` change)
+
+## Misc
+
+Smaller questions to consider:
+- Should default be `stream=False` or `stream=True`? (Current is False)
+ - Default to `False` makes it simpler for new users, as non-streaming is easier to handle.
+ - Default to `False` aligns with existing behavior.
+ - Streaming tends to be faster, so defaulting to `True` could improve performance for common use cases.
+ - Should this differ between ChatClient, Agent and Workflows? (e.g., Agent and Workflow defaults to streaming, ChatClient to non-streaming)
+
+## Decision Outcome
+
+Chosen Option: **Option 3: Consolidate + Merge Agent and Workflow Methods**
+
+Since this is the most pythonic option and it reduces the API surface and code duplication the most, we will go with this option.
+We will keep the default of `stream=False` for all methods to maintain backward compatibility and simplicity for new users.
+
+# Appendix
+## Code Samples for Consolidated Method
+
+### Python - Option 3: Direct ChatClient + Agent with Single Method
+
+```python
+# Copyright (c) Microsoft. All rights reserved.
+
+import asyncio
+from random import randint
+from typing import Annotated
+
+from agent_framework import ChatAgent
+from agent_framework.openai import OpenAIChatClient
+from pydantic import Field
+
+
+def get_weather(
+ location: Annotated[str, Field(description="The location to get the weather for.")],
+) -> str:
+ """Get the weather for a given location."""
+ conditions = ["sunny", "cloudy", "rainy", "stormy"]
+ return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C."
+
+
+async def main() -> None:
+ # Example 1: Direct ChatClient usage with single method
+ client = OpenAIChatClient()
+ message = "What's the weather in Amsterdam and in Paris?"
+
+ # Non-streaming usage
+ print(f"User: {message}")
+ response = await client.get_response(message, tools=get_weather)
+ print(f"Assistant: {response.text}")
+
+ # Streaming usage - same method, different parameter
+ print(f"\nUser: {message}")
+ print("Assistant: ", end="")
+ async for chunk in client.get_response(message, tools=get_weather, stream=True):
+ if chunk.text:
+ print(chunk.text, end="")
+ print("")
+
+ # Example 2: Agent usage with single method
+ agent = ChatAgent(
+ chat_client=client,
+ tools=get_weather,
+ name="WeatherAgent",
+ instructions="You are a weather assistant.",
+ )
+ thread = agent.get_new_thread()
+
+ # Non-streaming agent
+ print(f"\nUser: {message}")
+ result = await agent.run(message, thread=thread) # default would be stream=False
+ print(f"{agent.name}: {result.text}")
+
+ # Streaming agent - same method, different parameter
+ print(f"\nUser: {message}")
+ print(f"{agent.name}: ", end="")
+ async for update in agent.run(message, thread=thread, stream=True):
+ if update.text:
+ print(update.text, end="")
+ print("")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
+```
+
+### .NET - Current pattern for comparison
+
+```csharp
+// Copyright (c) Microsoft. All rights reserved.
+
+using Azure.AI.OpenAI;
+using Azure.Identity;
+using Microsoft.Agents.AI;
+using OpenAI.Chat;
+
+var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
+ ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
+var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
+
+AIAgent agent = new AzureOpenAIClient(
+ new Uri(endpoint),
+ new AzureCliCredential())
+ .GetChatClient(deploymentName)
+ .CreateAIAgent(
+ instructions: "You are good at telling jokes about pirates.",
+ name: "PirateJoker");
+
+// Non-streaming: Returns a string directly
+Console.WriteLine("=== Non-streaming ===");
+string result = await agent.RunAsync("Tell me a joke about a pirate.");
+Console.WriteLine(result);
+
+// Streaming: Returns IAsyncEnumerable
+Console.WriteLine("\n=== Streaming ===");
+await foreach (AgentUpdate update in agent.RunStreamingAsync("Tell me a joke about a pirate."))
+{
+ Console.Write(update);
+}
+Console.WriteLine();
+
+```
diff --git a/docs/decisions/0014-feature-collections.md b/docs/decisions/0014-feature-collections.md
new file mode 100644
index 0000000..d96ab4c
--- /dev/null
+++ b/docs/decisions/0014-feature-collections.md
@@ -0,0 +1,423 @@
+---
+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 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(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(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 RunAsync(
+ IEnumerable 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(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> RunAsync(
+ this AIAgent agent,
+ IEnumerable 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(structuredOutputFeature.ChatResponse, serializerOptions ?? AgentJsonUtilities.DefaultOptions);
+ return new AgentRunResponse(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 response = await agent.RunAsync([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(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()`
+
+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.
diff --git a/docs/decisions/README.md b/docs/decisions/README.md
new file mode 100644
index 0000000..55c48a7
--- /dev/null
+++ b/docs/decisions/README.md
@@ -0,0 +1,24 @@
+# Architectural Decision Records (ADRs)
+
+An Architectural Decision (AD) is a justified software design choice that addresses a functional or non-functional requirement that is architecturally significant. An Architectural Decision Record (ADR) captures a single AD and its rationale.
+
+For more information [see](https://adr.github.io/)
+
+## How are we using ADRs to track technical decisions?
+
+1. Copy docs/decisions/adr-template.md to docs/decisions/NNNN-title-with-dashes.md, where NNNN indicates the next number in sequence.
+ 1. Check for existing PR's to make sure you use the correct sequence number.
+ 2. There is also a short form template docs/decisions/adr-short-template.md
+2. Edit NNNN-title-with-dashes.md.
+ 1. Status must initially be `proposed`
+ 2. List of `deciders` must include the github ids of the people who will sign off on the decision.
+ 3. The relevant EM and architect must be listed as deciders or informed of all decisions.
+ 4. You should list the names or github ids of all partners who were consulted as part of the decision.
+ 5. Keep the list of `deciders` short. You can also list people who were `consulted` or `informed` about the decision.
+3. For each option list the good, neutral and bad aspects of each considered alternative.
+ 1. Detailed investigations can be included in the `More Information` section inline or as links to external documents.
+4. Share your PR with the deciders and other interested parties.
+ 1. Deciders must be listed as required reviewers.
+ 2. The status must be updated to `accepted` once a decision is agreed and the date must also be updated.
+ 3. Approval of the decision is captured using PR approval.
+5. Decisions can be changed later and superseded by a new ADR. In this case it is useful to record any negative outcomes in the original ADR.
diff --git a/docs/decisions/adr-short-template.md b/docs/decisions/adr-short-template.md
new file mode 100644
index 0000000..bd8b104
--- /dev/null
+++ b/docs/decisions/adr-short-template.md
@@ -0,0 +1,36 @@
+---
+# These are optional elements. Feel free to remove any of them.
+status: {proposed | rejected | accepted | deprecated | … | superseded by [ADR-0001](0001-madr-architecture-decisions.md)}
+contact: {person proposing the ADR}
+date: {YYYY-MM-DD when the decision was last updated}
+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}
+---
+
+# {short title of solved problem and solution}
+
+## Context and Problem Statement
+
+{Describe the context and problem statement, e.g., in free form using two to three sentences or in the form of an illustrative story.
+You may want to articulate the problem in form of a question and add links to collaboration boards or issue management systems.}
+
+
+
+## Decision Drivers
+
+- {decision driver 1, e.g., a force, facing concern, …}
+- {decision driver 2, e.g., a force, facing concern, …}
+- …
+
+## Considered Options
+
+- {title of option 1}
+- {title of option 2}
+- {title of option 3}
+- …
+
+## Decision Outcome
+
+Chosen option: "{title of option 1}", because
+{justification. e.g., only option, which meets k.o. criterion decision driver | which resolves force {force} | … | comes out best (see below)}.
diff --git a/docs/decisions/adr-template.md b/docs/decisions/adr-template.md
new file mode 100644
index 0000000..a965513
--- /dev/null
+++ b/docs/decisions/adr-template.md
@@ -0,0 +1,87 @@
+---
+# These are optional elements. Feel free to remove any of them.
+status: {proposed | rejected | accepted | deprecated | … | superseded by [ADR-0001](0001-madr-architecture-decisions.md)}
+contact: {person proposing the ADR}
+date: {YYYY-MM-DD when the decision was last updated}
+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}
+---
+
+# {short title of solved problem and solution}
+
+## Context and Problem Statement
+
+{Describe the context and problem statement, e.g., in free form using two to three sentences or in the form of an illustrative story.
+You may want to articulate the problem in form of a question and add links to collaboration boards or issue management systems.}
+
+
+
+## Decision Drivers
+
+- {decision driver 1, e.g., a force, facing concern, …}
+- {decision driver 2, e.g., a force, facing concern, …}
+- …
+
+## Considered Options
+
+- {title of option 1}
+- {title of option 2}
+- {title of option 3}
+- …
+
+## Decision Outcome
+
+Chosen option: "{title of option 1}", because
+{justification. e.g., only option, which meets k.o. criterion decision driver | which resolves force {force} | … | comes out best (see below)}.
+
+
+
+### Consequences
+
+- Good, because {positive consequence, e.g., improvement of one or more desired qualities, …}
+- Bad, because {negative consequence, e.g., compromising one or more desired qualities, …}
+- …
+
+
+
+## Validation
+
+{describe how the implementation of/compliance with the ADR is validated. E.g., by a review or an ArchUnit test}
+
+
+
+## Pros and Cons of the Options
+
+### {title of option 1}
+
+
+
+{example | description | pointer to more information | …}
+
+- Good, because {argument a}
+- Good, because {argument b}
+
+- Neutral, because {argument c}
+- Bad, because {argument d}
+- …
+
+### {title of other option}
+
+{example | description | pointer to more information | …}
+
+- Good, because {argument a}
+- Good, because {argument b}
+- Neutral, because {argument c}
+- Bad, because {argument d}
+- …
+
+
+
+## More Information
+
+{You might want to provide additional evidence/confidence for the decision outcome here and/or
+document the team agreement on the decision and/or
+define when this decision when and how the decision should be realized and if/when it should be re-visited and/or
+how the decision is validated.
+Links to other decisions and resources might appear here as well.}
diff --git a/docs/design/python-package-setup.md b/docs/design/python-package-setup.md
new file mode 100644
index 0000000..1c7afba
--- /dev/null
+++ b/docs/design/python-package-setup.md
@@ -0,0 +1,273 @@
+# Python Package design for Agent Framework
+
+## Design goals
+* Developer experience is key
+ * the components needed for a basic agent with tools and a runtime should be importable from `agent_framework` without having to import from subpackages. This will be referred to as _tier 0_ components.
+ * for more advanced components, _tier 1_ components, such as context providers, guardrails, vector data, text search, exceptions, evaluation, utils, telemetry and workflows, they should be importable from `agent_framework.`, so for instance `from agent_framework.vector_data import vectorstoremodel`.
+ * for parts of the package that are either additional functionality or integrations with other services (connectors) (_tier 2_), we use the term _tier 2_, however they should also be importable from `agent_framework.`, so for instance `from agent_framework.openai import OpenAIClient`.
+ * this means that the package structure is flat, and the components are grouped by functionality, not by type, so for instance `from agent_framework.openai import OpenAIChatClient` will import the OpenAI chat client, but also the OpenAI tools, and any other OpenAI related functionality.
+ * There should not be a need for deeper imports from those packages, unless a good case is made for that, so the internals of the extensions packages should always be a folder with the name of the package, a `__init__.py` and one or more `_files.py` file, where the `_files.py` file contains the implementation details, and the `__init__.py` file exposes the public interface.
+ * if a single file becomes too cumbersome (files are allowed to be 1k+ lines) it should be split into a folder with an `__init__.py` that exposes the public interface and a `_files.py` that contains the implementation details, with a `__all__` in the init to expose the right things, if there are very large dependencies being loaded it can optionally using lazy loading to avoid loading the entire package when importing a single component.
+ * as much as possible, related things are in a single file which makes understanding the code easier.
+ * simple and straightforward logging and telemetry setup, so developers can easily add logging and telemetry to their code without having to worry about the details.
+* Independence of connectors
+ * To allow connectors to be treated as independent packages, we will use namespace packages for connectors, in principle this only includes the packages that we will develop in our repo, since that is easy to manage and maintain.
+ * further advantages are that each package can have a independent lifecycle, versioning, and dependencies.
+ * and this gives us insights into the usage, through pip install statistics, especially for connectors to services outside of Microsoft.
+ * the goal is to group related connectors based on vendors, not on types, so for instance doing: `import agent_framework.google` will import connectors for all Google services, such as `GoogleChatClient` but also `BigQueryCollection`, etc.
+ * All dependencies for a subpackage should be required dependencies in that package, and that package becomes a optional dependency in the main package as an _extra_ with the same name, so in the main `pyproject.toml` we will have:
+ ```toml
+ [project.optional-dependencies]
+ google = [
+ "agent-framework-google == 1.0.0"
+ ]
+ ```
+ * this means developers can use `pip install agent-framework[google] --pre` to get AF with all Google connectors and dependencies, as well as manually installing the subpackage with `pip install agent-framework-google --pre`.
+
+### Sample getting started code
+```python
+from typing import Annotated
+from agent_framework import Agent, ai_function
+from agent_framework.openai import OpenAIChatClient
+
+@ai_function(description="Get the current weather in a given location")
+async def get_weather(location: Annotated[str, "The location as a city name"]) -> str:
+ """Get the current weather in a given location."""
+ # Implementation of the tool to get weather
+ return f"The current weather in {location} is sunny."
+
+agent = Agent(
+ name="MyAgent",
+ model_client=OpenAIChatClient(),
+ tools=get_weather,
+ description="An agent that can get the current weather.",
+)
+response = await agent.run("What is the weather in Amsterdam?")
+print(response)
+```
+
+## Global Package structure
+Overall the following structure is proposed:
+
+* agent-framework
+ * core components, will be exposed directly from `agent_framework`:
+ * (single) agents (includes threads)
+ * tools (includes MCP and OpenAPI)
+ * types
+ * context_providers
+ * logging
+ * workflows (includes multi-agent orchestration)
+ * middleware
+ * telemetry (user_agent)
+ * advanced components, will be exposed from `agent_framework.`:
+ * vector_data (tbd, vector stores and other MEVD-like pieces)
+ * text_search (tbd)
+ * exceptions
+ * evaluations (tbd)
+ * utils (optional)
+ * observability
+ * vendor folders with connectors and integrations, will be exposed from `agent_framework.`:
+ * Code can be both in folder or in subpackage with lazy import.
+ * See subpackage scope below for more detail
+* tests
+* samples
+* extensions
+ * azure
+ * ...
+
+All the init's in the subpackages will use lazy loading so avoid importing the entire package when importing a single component.
+Internal imports will be done using relative imports, so that the package can be used as a namespace package.
+
+### File structure
+The resulting file structure will be as follows (not all things currently implemented, just an example):
+
+```plaintext
+packages/
+ main/
+ agent_framework/
+ azure/
+ __init__.py
+ _chat_client.py
+ ...
+ microsoft/
+ __init__.py
+ _copilot_studio.py
+ ...
+ openai/
+ __init__.py
+ _chat_client.py
+ _shared.py
+ exceptions.py
+ __init__.py
+ __init__.pyi
+ _agents.py
+ _tools.py
+ _models.py
+ _logging.py
+ _middleware.py
+ _telemetry.py
+ observability.py
+ exceptions.py
+ utils.py
+ py.typed
+ _workflow/
+ __init__.py
+ _workflow.py
+ ...etc...
+ tests/
+ unit/
+ test_types.py
+ integration/
+ test_chat_clients.py
+ pyproject.toml
+ README.md
+ ...
+ azure-ai-agents/
+ agent_framework-azure-ai-agents/
+ __init__.py
+ _chat_client.py
+ ...
+ tests/
+ test_azure_ai_agents.py
+ samples/ (optional)
+ ...
+ pyproject.toml
+ README.md
+ ...
+ redis/
+ ...
+ mem0/
+ agent_framework-mem0/
+ __init__.py
+ _provider.py
+ ...
+ tests/
+ test_mem0_provider.py
+ samples/ (optional)
+ ...
+ pyproject.toml
+ README.md
+ ...
+ ...
+samples/
+ ...
+pyproject.toml
+README.md
+LICENSE
+uv.lock
+.pre-commit-config.yaml
+```
+
+We might add a template subpackage as well, to make it easy to setup, this could be based on the first one that is added.
+
+In the [`DEV_SETUP.md`](../../python/DEV_SETUP.md) we will add instructions for how to deal with the path depth issues, especially on Windows, where the maximum path length can be a problem.
+
+### Subpackage scope
+Sub-packages are comprised of two parts, the code itself and the dependencies, the choice of when to use a subpackage and when to use a extra in the main package is based on the status of dependencies and/or possibilities of a external support mechanism. What this means is that:
+
+- Integrations that need non-GA dependencies will be sub-packages and installed only when using a extra, so that we can avoid having non-GA dependencies in the main package.
+- Integrations where the AF-code is still experimental, preview or release candidate will be sub-packages, so that we can avoid having non-GA code in the main package and we can version those packages properly.
+- Integrations that are outside Microsoft and where we might not always be able to fast-follow breaking changes, will stay as sub-packages, to provide some isolation and to be able to version them properly.
+- Integrations that are mature and that have released (GA) dependencies and features on the service side will be moved into the main package, the dependencies of those packages will stay installable under the same `extra` name, so that users do not have to change anything, and we then remove the subpackage itself.
+- All subpackage imports in the code should be from a stable place, mostly vendor-based, so that when something moves from a subpackage to the main package, the import path does not change, so `from agent_framework.microsoft import CopilotAgent` will always work, even if it moves from the `agent-framework-microsoft-copilot` package to the main `agent-framework` package.
+- The imports in those vendor namespaces (these won't be actual python namespaces, just the folders with a __init__.py file and any code) will do lazy loading and raise a meaningful error if the subpackage or dependencies are not installed, so that users know which extra to install with ease.
+- On a case by case basis we can decide to create additional a `extra`, that combines multiple sub-packages and dependencies into one extra, so that users who work primarily with one platform can install everything they need with a single extra, for example (not implemented) you can install with the `agent-framework[azure-purview]` extra that only implement a `PurviewMiddleware`, or you can install with the `agent-framework[azure]` extra that includes all Azure related connectors, like `purview`, `content-safety` and others (all examples, not actual packages), regardless of where the code sits, these should always be importable from `agent_framework.azure`.
+- Subpackage naming should also follow this, so in principle a package name is `-`, so `google-gemini`, `azure-purview`, `microsoft-copilotstudio`, etc. For smaller vendors, where it's less likely to have a multitude of connectors, we can skip the feature/brand part, so `mem0`, `redis`, etc.
+- For Microsoft services we will have two vendor folders, `azure` and `microsoft`, where `azure` contains all Azure services, while `microsoft` contains other Microsoft services, such as Copilot Studio Agents.
+
+This setup was discussed at length and the decision is captured in [ADR-0008](../decisions/0008-python-subpackages.md).
+
+#### Evolving the package structure
+For each of the advanced components, we have two reason why we may split them into a folder, with an `__init__.py` and optionally a `_files.py`:
+1. If the file becomes too large, we can split it into multiple `_files`, while still keeping the public interface in the `__init__.py` file, this is a non-breaking change
+2. If we want to partially or fully move that code into a separate package.
+In this case we do need to lazy load anything that was moved from the main package to the subpackage, so that existing code still works, and if the subpackage is not installed we can raise a meaningful error.
+
+## Coding standards
+
+Coding standards will be maintained in the [`DEV_SETUP.md`](../../python/DEV_SETUP.md) file.
+
+### Tooling
+uv and ruff are the main tools, for package management and code formatting/linting respectively.
+
+#### Type checking
+We currently can choose between mypy, pyright, ty and pyrefly for static type checking.
+I propose we run `mypy` and `pyright` in GHA, similar to what AG already does. We might explore newer tools as a later date.
+
+#### Task runner
+AG already has experience with poe the poet, so let's start there, removing the MAKE file setup that SK uses.
+
+### Unit test coverage
+The goal is to have at least 80% unit test coverage for all code under both the main package and the subpackages.
+
+### Telemetry and logging
+Telemetry and logging are handled by the `agent_framework.telemetry` and `agent_framework._logging` packages.
+
+#### Logging
+
+Logging is considered as part of the basic setup, while telemetry is a advanced concept.
+The telemetry package will use OpenTelemetry to provide a consistent way to collect and export telemetry data, similar to how we do this now in SK.
+
+The logging will be simplified, there will be one logger in the base package:
+* name: `agent_framework` - used for all logging in the abstractions and base components
+
+Each of the other subpackages for connectors will have a similar single logger.
+* name: `agent_framework.openai`
+* name: `agent_framework.azure`
+
+This means that when a logger is needed, it should be created like this:
+```python
+from agent_framework import get_logger
+
+logger = get_logger()
+#or in a subpackage:
+logger = get_logger('agent_framework.openai')
+```
+The implementation should be something like this:
+```python
+# in file _logging.py
+import logging
+
+def get_logger(name: str = "agent_framework") -> logging.Logger:
+ """
+ Get a logger with the specified name, defaulting to 'agent_framework'.
+
+ Args:
+ name (str): The name of the logger. Defaults to 'agent_framework'.
+
+ Returns:
+ logging.Logger: The configured logger instance.
+ """
+ logger = logging.getLogger(name)
+ # create the specifics for the logger, such as setting the level, handlers, etc.
+ return logger
+```
+This will ensure that the logger is created with the correct name and configuration, and it will be consistent across the package.
+
+Further there should be a easy way to configure the log levels, either through a environment variable or with a similar function as the get_logger.
+
+This will not be allowed:
+```python
+import logging
+
+logger = logging.getLogger(__name__)
+```
+
+This is allowed but discouraged, if the get_logger function has been called at least once then this will return the same logger as the get_logger function, however that might not have happened and then the logging experience (in terms of formats and handlers, etc) is not consistent across the package:
+```python
+import logging
+
+logger = logging.getLogger("agent_framework")
+```
+
+#### Telemetry
+Telemetry will be based on OpenTelemetry (OTel), and will be implemented in the `agent_framework.telemetry` package.
+
+We will also add headers with user-agent strings where applicable, these will include `agent-framework-python` and the version.
+
+We should consider auto-instrumentation and provide an implementation of it to the OTel community.
+
+### Build and release
+The build step will be done in GHA, adding the package to the release and then we call into Azure DevOps to use the ESRP pipeline to publish to pypi. This is how SK already works, we will just have to adapt it to the new package structure.
+
+For now we will stick to semantic versioning, and all preview release will be tagged as such.
diff --git a/docs/features/durable-agents/durable-agents-ttl.md b/docs/features/durable-agents/durable-agents-ttl.md
new file mode 100644
index 0000000..1a4a4e3
--- /dev/null
+++ b/docs/features/durable-agents/durable-agents-ttl.md
@@ -0,0 +1,147 @@
+# Time-To-Live (TTL) for durable agent sessions
+
+## Overview
+
+The durable agents automatically maintain conversation history and state for each session. Without automatic cleanup, this state can accumulate indefinitely, consuming storage resources and increasing costs. The Time-To-Live (TTL) feature provides automatic cleanup of idle agent sessions, ensuring that sessions are automatically deleted after a period of inactivity.
+
+## What is TTL?
+
+Time-To-Live (TTL) is a configurable duration that determines how long an agent session state will be retained after its last interaction. When an agent session is idle (no messages sent to it) for longer than the TTL period, the session state is automatically deleted. Each new interaction with an agent resets the TTL timer, extending the session's lifetime.
+
+## Benefits
+
+- **Automatic cleanup**: No manual intervention required to clean up idle agent sessions
+- **Cost optimization**: Reduces storage costs by automatically removing unused session state
+- **Resource management**: Prevents unbounded growth of agent session state in storage
+- **Configurable**: Set TTL globally or per-agent type to match your application's needs
+
+## Configuration
+
+TTL can be configured at two levels:
+
+1. **Global default TTL**: Applies to all agent sessions unless overridden
+2. **Per-agent type TTL**: Overrides the global default for specific agent types
+
+Additionally, you can configure a **minimum deletion delay** that controls how frequently deletion operations are scheduled. The default value is 5 minutes, and the maximum allowed value is also 5 minutes.
+
+> [!NOTE]
+> Reducing the minimum deletion delay below 5 minutes can be useful for testing or for ensuring rapid cleanup of short-lived agent sessions. However, this can also increase the load on the system and should be used with caution.
+
+### Default values
+
+- **Default TTL**: 14 days
+- **Minimum TTL deletion delay**: 5 minutes (maximum allowed value, subject to change in future releases)
+
+### Configuration examples
+
+#### .NET
+
+```csharp
+// Configure global default TTL and minimum signal delay
+services.ConfigureDurableAgents(
+ options =>
+ {
+ // Set global default TTL to 7 days
+ options.DefaultTimeToLive = TimeSpan.FromDays(7);
+
+ // Add agents (will use global default TTL)
+ options.AddAIAgent(myAgent);
+ });
+
+// Configure per-agent TTL
+services.ConfigureDurableAgents(
+ options =>
+ {
+ options.DefaultTimeToLive = TimeSpan.FromDays(14); // Global default
+
+ // Agent with custom TTL of 1 day
+ options.AddAIAgent(shortLivedAgent, timeToLive: TimeSpan.FromDays(1));
+
+ // Agent with custom TTL of 90 days
+ options.AddAIAgent(longLivedAgent, timeToLive: TimeSpan.FromDays(90));
+
+ // Agent using global default (14 days)
+ options.AddAIAgent(defaultAgent);
+ });
+
+// Disable TTL for specific agents by setting TTL to null
+services.ConfigureDurableAgents(
+ options =>
+ {
+ options.DefaultTimeToLive = TimeSpan.FromDays(14);
+
+ // Agent with no TTL (never expires)
+ options.AddAIAgent(permanentAgent, timeToLive: null);
+ });
+```
+
+## How TTL works
+
+The following sections describe how TTL works in detail.
+
+### Expiration tracking
+
+Each agent session maintains an expiration timestamp in its internally managed state that is updated whenever the session processes a message:
+
+1. When a message is sent to an agent session, the expiration time is set to `current time + TTL`
+2. The runtime schedules a delete operation for the expiration time (subject to minimum delay constraints)
+3. When the delete operation runs, if the current time is past the expiration time, the session state is deleted. Otherwise, the delete operation is rescheduled for the next expiration time.
+
+### State deletion
+
+When an agent session expires, its entire state is deleted, including:
+
+- Conversation history
+- Any custom state data
+- Expiration timestamps
+
+After deletion, if a message is sent to the same agent session, a new session is created with a fresh conversation history.
+
+## Behavior examples
+
+The following examples illustrate how TTL works in different scenarios.
+
+### Example 1: Agent session expires after TTL
+
+1. Agent configured with 30-day TTL
+2. User sends message at Day 0 → agent session created, expiration set to Day 30
+3. No further messages sent
+4. At Day 30 → Agent session is deleted
+5. User sends message at Day 31 → New agent session created with fresh conversation history
+
+### Example 2: TTL reset on interaction
+
+1. Agent configured with 30-day TTL
+2. User sends message at Day 0 → agent session created, expiration set to Day 30
+3. User sends message at Day 15 → Expiration reset to Day 45
+4. User sends message at Day 40 → Expiration reset to Day 70
+5. Agent session remains active as long as there are regular interactions
+
+## Logging
+
+The TTL feature includes comprehensive logging to track state changes:
+
+- **Expiration time updated**: Logged when TTL expiration time is set or updated
+- **Deletion scheduled**: Logged when a deletion check signal is scheduled
+- **Deletion check**: Logged when a deletion check operation runs
+- **Session expired**: Logged when an agent session is deleted due to expiration
+- **TTL rescheduled**: Logged when a deletion signal is rescheduled
+
+These logs help monitor TTL behavior and troubleshoot any issues.
+
+## Best practices
+
+1. **Choose appropriate TTL values**: Balance between storage costs and user experience. Too short TTLs may delete active sessions, while too long TTLs may accumulate unnecessary state.
+
+2. **Use per-agent TTLs**: Different agents may have different usage patterns. Configure TTLs per-agent based on expected session lifetimes.
+
+3. **Monitor expiration logs**: Review logs to understand TTL behavior and adjust configuration as needed.
+
+4. **Test with short TTLs**: During development, use short TTLs (e.g., minutes) to verify TTL behavior without waiting for long periods.
+
+## Limitations
+
+- TTL is based on wall-clock time, not activity time. The expiration timer starts from the last message timestamp.
+- Deletion checks are durably scheduled operations and may have slight delays depending on system load.
+- Once an agent session is deleted, its conversation history cannot be recovered.
+- TTL deletion requires at least one worker to be available to process the deletion operation message.
diff --git a/docs/specs/001-foundry-sdk-alignment.md b/docs/specs/001-foundry-sdk-alignment.md
new file mode 100644
index 0000000..b7b780c
--- /dev/null
+++ b/docs/specs/001-foundry-sdk-alignment.md
@@ -0,0 +1,291 @@
+---
+# These are optional elements. Feel free to remove any of them.
+status: accepted
+contact: markwallace
+date: 2025-08-06
+deciders: markwallace-microsoft, westey-m, quibitron
+consulted: shawnhenry, elijahstraight
+informed:
+---
+
+# Agent Framework / Foundry SDK Alignment
+
+Agent Framework and Foundry SDK have overlapping functionality but serve different audiences & scenarios.
+This specification clarifies the positioning of these SDKs to customers, what goes in each and when to use what.
+
+- **Foundry SDK** is a thin-client SDK for accessing everything available in the agent service and is autogenerated from REST APIs in multiple languages
+- **Agent Framework SDK** is general-purpose framework for agentic application development, where common agent abstractions enable creating and orchestrating heterogenous agent systems (across local & cloud)
+
+## What is the goal of this feature?
+
+Goals:
+- Developers can seamlessly combine Foundry and Agent Framework SDK's and there is no friction when using both SDKs at the same time
+- Developers can take advantage of the full capabilities supported by the Foundry SDK
+- Developers can create multi-agent orchestrations using Foundry and other agent types
+
+Success Metrics:
+- Complexity of basic samples is comparable to other agent frameworks
+- Developers can easily discover how to use Foundry Agents in Agent Framework multi-agent orchestrations
+
+## What is the problem being solved?
+
+- In Semantic Kernel the Foundry Agent support isn't integrated into the Foundry SDK so there is a disjointed developer UX
+- Customers are confused as to when they should use Foundry SDK versus Semantic Kernel
+
+
+## API Changes
+
+The proposed solution is to add helper methods which allow developers to either retrieve or create an `AIAgent` using a `PersistentAgentsClient`
+
+- Retrieve an `AIAgent`
+ ```csharp
+ ///
+ /// Retrieves an existing server side agent, wrapped as a using the provided .
+ ///
+ /// The to create the with.
+ /// A for the persistent agent.
+ /// The ID of the server side agent to create a for.
+ /// Options that should apply to all runs of the agent.
+ /// The to monitor for cancellation requests. The default is .
+ /// A instance that can be used to perform operations on the persistent agent.
+ public static async Task GetAIAgentAsync(
+ this PersistentAgentsClient persistentAgentsClient,
+ string agentId,
+ ChatOptions? chatOptions = null,
+ CancellationToken cancellationToken = default)
+ ```
+- Create an `AIAgent`
+ ```csharp
+ ///
+ /// Creates a new server side agent using the provided .
+ ///
+ /// The to create the agent with.
+ /// The model to be used by the agent.
+ /// The name of the agent.
+ /// The description of the agent.
+ /// The instructions for the agent.
+ /// The tools to be used by the agent.
+ /// The resources for the tools.
+ /// The temperature setting for the agent.
+ /// The top-p setting for the agent.
+ /// The response format for the agent.
+ /// The metadata for the agent.
+ /// The to monitor for cancellation requests. The default is .
+ /// A instance that can be used to perform operations on the newly created agent.
+ public static async Task CreateAIAgentAsync(
+ this PersistentAgentsClient persistentAgentsClient,
+ string model,
+ string? name = null,
+ string? description = null,
+ string? instructions = null,
+ IEnumerable? tools = null,
+ ToolResources? toolResources = null,
+ float? temperature = null,
+ float? topP = null,
+ BinaryData? responseFormat = null,
+ IReadOnlyDictionary? metadata = null,
+ CancellationToken cancellationToken = default)
+ ```
+- Additional overload using the M.E.AI types:
+ ```csharp
+ ///
+ /// Creates a new server side agent using the provided .
+ ///
+ /// The to create the agent with.
+ /// The model to be used by the agent.
+ /// The name of the agent.
+ /// The description of the agent.
+ /// The instructions for the agent.
+ /// The tools to be used by the agent.
+ /// The temperature setting for the agent.
+ /// The top-p setting for the agent.
+ /// The response format for the agent.
+ /// The metadata for the agent.
+ /// The to monitor for cancellation requests. The default is .
+ /// A instance that can be used to perform operations on the newly created agent.
+ public static async Task CreateAIAgentAsync(
+ this PersistentAgentsClient persistentAgentsClient,
+ string model,
+ string? name = null,
+ string? description = null,
+ string? instructions = null,
+ IEnumerable? tools = null,
+ float? temperature = null,
+ float? topP = null,
+ BinaryData? responseFormat = null,
+ IReadOnlyDictionary? metadata = null,
+ CancellationToken cancellationToken = default)
+ ```
+
+
+## E2E Code Samples
+
+### 1. Create and retrieve with Foundry SDK, run with Agent Framework
+
+- [Foundry SDK] Create a `PersistentAgentsClient`
+- [Foundry SDK] Create a `PersistentAgent` using the `PersistentAgentsClient`
+- [Foundry SDK] Retrieve an `AIAgent` using the `PersistentAgentsClient`
+- [Agent Framework SDK] Invoke the `AIAgent` instance and access response from the `AgentResponse`
+- [Foundry SDK] Clean up the agent
+
+
+```csharp
+// Get a client to create server side agents with.
+var persistentAgentsClient = new PersistentAgentsClient(
+ TestConfiguration.AzureAI.Endpoint, new AzureCliCredential());
+
+// Create a persistent agent.
+var persistentAgentMetadata = await persistentAgentsClient.Administration.CreateAgentAsync(
+ model: TestConfiguration.AzureAI.DeploymentName!,
+ name: JokerName,
+ instructions: JokerInstructions);
+
+// Get the persistent agent we created in the previous step and expose it as an Agent Framework agent.
+AIAgent agent = await persistentAgentsClient.GetAIAgentAsync(persistentAgent.Value.Id);
+
+// Respond to user input.
+var input = "Tell me a joke about a pirate.";
+Console.WriteLine(input);
+Console.WriteLine(await agent.RunAsync(input));
+
+// Delete the persistent agent.
+await persistentAgentsClient.Administration.DeleteAgentAsync(agent.Id);
+```
+
+### 2. Create directly with Foundry SDK, run with Agent Framework
+
+- [Foundry SDK] Create a `PersistentAgentsClient`
+- [Foundry SDK] Create a `AIAgent` using the `PersistentAgentsClient`
+- [Agent Framework SDK] Invoke the `AIAgent` instance and access response from the `AgentResponse`
+- [Foundry SDK] Clean up the agent
+
+```csharp
+// Get a client to create server side agents with.
+var persistentAgentsClient = new PersistentAgentsClient(
+ TestConfiguration.AzureAI.Endpoint, new AzureCliCredential());
+
+// Create a persistent agent and expose it as an Agent Framework agent.
+AIAgent agent = await persistentAgentsClient.CreateAIAgentAsync(
+ model: TestConfiguration.AzureAI.DeploymentName!,
+ name: JokerName,
+ instructions: JokerInstructions);
+
+// Respond to user input.
+var input = "Tell me a joke about a pirate.";
+Console.WriteLine(input);
+Console.WriteLine(await agent.RunAsync(input));
+
+// Delete the persistent agent.
+await persistentAgentsClient.Administration.DeleteAgentAsync(agent.Id);
+```
+
+### 3. Create directly with Foundry SDK, run with conversation state using Agent Framework
+
+- [Foundry SDK] Create a `PersistentAgentsClient`
+- [Foundry SDK] Create a `AIAgent` using the `PersistentAgentsClient`
+- [Agent Framework SDK] Optionally create an `AgentThread` for the agent run
+- [Agent Framework SDK] Invoke the `AIAgent` instance and access response from the `AgentResponse`
+- [Foundry SDK] Clean up the agent and the agent thread
+
+```csharp
+// Get a client to create server side agents with.
+var persistentAgentsClient = new PersistentAgentsClient(
+ TestConfiguration.AzureAI.Endpoint, new AzureCliCredential());
+
+// Create an Agent Framework agent.
+AIAgent agent = await persistentAgentsClient.CreateAIAgentAsync(
+ model: TestConfiguration.AzureAI.DeploymentName!,
+ name: JokerName,
+ instructions: JokerInstructions);
+
+// Start a new thread for the agent conversation.
+AgentThread thread = agent.GetNewThread();
+
+// Respond to user input.
+await RunAgentAsync("Tell me a joke about a pirate.");
+await RunAgentAsync("Now add some emojis to the joke.");
+
+// Local function to run agent and display the conversation messages for the thread.
+async Task RunAgentAsync(string input)
+{
+ Console.WriteLine(
+ $"""
+ User: {input}
+ Assistant:
+ {await agent.RunAsync(input, thread)}
+
+ """);
+}
+
+// Cleanup
+await persistentAgentsClient.Threads.DeleteThreadAsync(thread.ConversationId);
+await persistentAgentsClient.Administration.DeleteAgentAsync(agent.Id);
+```
+
+### 4. Create directly with Foundry SDK, orchestrate with Agent Framework
+
+- [Foundry SDK] Create a `PersistentAgentsClient`
+- [Foundry SDK] Create multiple `AIAgent` instances using the `PersistentAgentsClient`
+- [Agent Framework SDK] Create a `SequentialOrchestration` and add all of the agents to it
+- [Agent Framework SDK] Invoke the `SequentialOrchestration` instance and access response from the `AgentResponse`
+- [Foundry SDK] Clean up the agents
+
+```csharp
+// Get a client to create server side agents with.
+var persistentAgentsClient = new PersistentAgentsClient(
+ TestConfiguration.AzureAI.Endpoint, new AzureCliCredential());
+var model = TestConfiguration.OpenAI.ChatModelId;
+
+// Define the agents
+AIAgent analystAgent =
+ await persistentAgentsClient.CreateAIAgentAsync(
+ model,
+ name: "Analyst",
+ instructions:
+ """
+ You are a marketing analyst. Given a product description, identify:
+ - Key features
+ - Target audience
+ - Unique selling points
+ """,
+ description: "An agent that extracts key concepts from a product description.");
+AIAgent writerAgent =
+ await persistentAgentsClient.CreateAIAgentAsync(
+ model,
+ name: "copywriter",
+ instructions:
+ """
+ You are a marketing copywriter. Given a block of text describing features, audience, and USPs,
+ compose a compelling marketing copy (like a newsletter section) that highlights these points.
+ Output should be short (around 150 words), output just the copy as a single text block.
+ """,
+ description: "An agent that writes a marketing copy based on the extracted concepts.");
+AIAgent editorAgent =
+ await persistentAgentsClient.CreateAIAgentAsync(
+ model,
+ name: "editor",
+ instructions:
+ """
+ You are an editor. Given the draft copy, correct grammar, improve clarity, ensure consistent tone,
+ give format and make it polished. Output the final improved copy as a single text block.
+ """,
+ description: "An agent that formats and proofreads the marketing copy.");
+
+// Define the orchestration
+SequentialOrchestration orchestration =
+ new(analystAgent, writerAgent, editorAgent)
+ {
+ LoggerFactory = this.LoggerFactory,
+ };
+
+// Run the orchestration
+string input = "An eco-friendly stainless steel water bottle that keeps drinks cold for 24 hours";
+Console.WriteLine($"\n# INPUT: {input}\n");
+AgentResponse result = await orchestration.RunAsync(input);
+Console.WriteLine($"\n# RESULT: {result}");
+
+// Cleanup
+await persistentAgentsClient.Administration.DeleteAgentAsync(analystAgent.Id);
+await persistentAgentsClient.Administration.DeleteAgentAsync(writerAgent.Id);
+await persistentAgentsClient.Administration.DeleteAgentAsync(editorAgent.Id);
+```
\ No newline at end of file
diff --git a/docs/specs/spec-template.md b/docs/specs/spec-template.md
new file mode 100644
index 0000000..827ba04
--- /dev/null
+++ b/docs/specs/spec-template.md
@@ -0,0 +1,75 @@
+---
+# These are optional elements. Feel free to remove any of them.
+status: {proposed | rejected | accepted | deprecated | … | superseded by [SPEC-0001](0001-spec.md)}
+contact: {person proposing the ADR}
+date: {YYYY-MM-DD when the decision was last updated}
+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}
+---
+
+# {short title of solved problem and solution}
+
+## What is the goal of this feature?
+
+Make sure to cover:
+1. What is the value we are providing to users
+1. Include one success metric
+1. Implementation free description of outcome
+
+Consult PM on this.
+
+For example:
+
+We want users to be able to refer to external Azure resources easily when consuming them in other features like indexes, agents,
+and evaluations. We know we're successful when 40% of project client users are using connections.
+
+## What is the problem being solved?
+
+Make sure to cover:
+1. Why is this hard today?
+1. Customer pain points?
+1. Reducing system complexity (maintenance costs, latency, etc)?
+
+Consult PM on this.
+
+For example:
+
+Today, users have to understand control plane vs data plane endpoints and use multiple packages to stitch their application
+code together. This makes using our product confusing and also increases the number of dependencies a customer will have
+in their code.
+
+## API Changes
+
+List all new API changes
+
+## E2E Code Samples
+
+Include python or C# examples of how you expect this feature to be used with other things in our system.
+
+For example:
+
+This connection name is unique across the resource. Given a resource name, system should be able to unambiguously resolve a
+connection name. A connection name can be used to pass along connection details to individual features. Services will be able to parse this ID and use it to access the underlying resource. The below example shows how a connection can be used to create a dataset.
+
+```python
+client.datasets.create_dataset(
+ name="evaluation_dataset",
+ file="myblob/product1.pdf",
+ connection = "my-azure-blob-connection"
+)
+```
+
+How to use a connection when creating an `AzureAISearchIndex`
+
+```python
+from azure.ai.projects.models import AzureAISearchIndex
+
+azure_ai_search_index = AzureAISearchIndex(
+ name="azure-search-index",
+ connection="my-ai-search-connection",
+ index_name="my-index-in-azure-search",
+)
+
+created_index = client.indexes.create_index(azure_ai_search_index)
+```
diff --git a/dotnet/.editorconfig b/dotnet/.editorconfig
new file mode 100644
index 0000000..fea0183
--- /dev/null
+++ b/dotnet/.editorconfig
@@ -0,0 +1,448 @@
+# To learn more about .editorconfig see https://aka.ms/editorconfigdocs
+###############################
+# Core EditorConfig Options #
+###############################
+root = true
+# All files
+[*]
+indent_style = space
+end_of_line = lf
+
+# XML project files
+[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}]
+indent_size = 2
+
+# XML config files
+[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}]
+indent_size = 2
+
+# YAML config files
+[*.{yml,yaml}]
+tab_width = 2
+indent_size = 2
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+# JSON config files
+[*.json]
+tab_width = 2
+indent_size = 2
+insert_final_newline = false
+trim_trailing_whitespace = true
+
+# Typescript files
+[*.{ts,tsx}]
+insert_final_newline = true
+trim_trailing_whitespace = true
+tab_width = 4
+indent_size = 4
+file_header_template = Copyright (c) Microsoft. All rights reserved.
+
+# Stylesheet files
+[*.{css,scss,sass,less}]
+insert_final_newline = true
+trim_trailing_whitespace = true
+tab_width = 4
+indent_size = 4
+
+# Code files
+[*.{cs,csx,vb,vbx}]
+tab_width = 4
+indent_size = 4
+insert_final_newline = true
+trim_trailing_whitespace = true
+charset = utf-8-bom
+file_header_template = Copyright (c) Microsoft. All rights reserved.
+
+###############################
+# .NET Coding Conventions #
+###############################
+[*.{cs,vb}]
+# Organize usings
+dotnet_sort_system_directives_first = true
+# this. preferences
+dotnet_style_qualification_for_field = true:error
+dotnet_style_qualification_for_property = true:error
+dotnet_style_qualification_for_method = true:error
+dotnet_style_qualification_for_event = true:error
+# Language keywords vs BCL types preferences
+dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
+dotnet_style_predefined_type_for_member_access = true:suggestion
+# Parentheses preferences
+dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:suggestion
+dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:suggestion
+dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
+dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
+# Modifier preferences
+dotnet_style_require_accessibility_modifiers = for_non_interface_members:error
+dotnet_style_readonly_field = true:warning
+# Expression-level preferences
+dotnet_style_object_initializer = true:suggestion
+dotnet_style_collection_initializer = true:suggestion
+dotnet_style_explicit_tuple_names = true:suggestion
+dotnet_style_null_propagation = true:suggestion
+dotnet_style_coalesce_expression = true:suggestion
+dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
+dotnet_style_prefer_inferred_tuple_names = true:suggestion
+dotnet_style_prefer_inferred_anonymous_type_member_names = true:silent
+dotnet_style_prefer_auto_properties = true:suggestion
+dotnet_style_prefer_conditional_expression_over_assignment = true:silent
+dotnet_style_prefer_conditional_expression_over_return = true:silent
+dotnet_style_prefer_simplified_interpolation = true:suggestion
+dotnet_style_operator_placement_when_wrapping = beginning_of_line
+dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
+dotnet_style_prefer_compound_assignment = true:suggestion
+# Code quality rules
+dotnet_code_quality_unused_parameters = all:suggestion
+
+[*.cs]
+# Note: these settings cause "dotnet format" to fix the code. You should review each change if you uses "dotnet format".
+dotnet_diagnostic.RCS1036.severity = warning # Remove unnecessary blank line.
+dotnet_diagnostic.RCS1037.severity = warning # Remove trailing white-space.
+dotnet_diagnostic.RCS1097.severity = warning # Remove redundant 'ToString' call.
+dotnet_diagnostic.RCS1138.severity = warning # Add summary to documentation comment.
+dotnet_diagnostic.RCS1139.severity = warning # Add summary element to documentation comment.
+dotnet_diagnostic.RCS1168.severity = warning # Parameter name 'foo' differs from base name 'bar'.
+dotnet_diagnostic.RCS1175.severity = warning # Unused 'this' parameter 'operation'.
+dotnet_diagnostic.RCS1192.severity = warning # Unnecessary usage of verbatim string literal.
+dotnet_diagnostic.RCS1194.severity = warning # Implement exception constructors.
+dotnet_diagnostic.RCS1211.severity = warning # Remove unnecessary else clause.
+dotnet_diagnostic.RCS1214.severity = warning # Unnecessary interpolated string.
+dotnet_diagnostic.RCS1225.severity = warning # Make class sealed.
+dotnet_diagnostic.RCS1232.severity = warning # Order elements in documentation comment.
+
+# Commented out because `dotnet format` change can be disruptive.
+# dotnet_diagnostic.RCS1085.severity = warning # Use auto-implemented property.
+
+# Commented out because `dotnet format` removes the xmldoc element, while we should add the missing documentation instead.
+# dotnet_diagnostic.RCS1228.severity = warning # Unused element in documentation comment.
+
+# Diagnostics elevated as warnings
+dotnet_diagnostic.CA1000.severity = warning # Do not declare static members on generic types
+dotnet_diagnostic.CA1050.severity = warning # Declare types in namespaces
+dotnet_diagnostic.CA1063.severity = warning # Implement IDisposable correctly
+dotnet_diagnostic.CA1064.severity = warning # Exceptions should be public
+dotnet_diagnostic.CA1416.severity = warning # Validate platform compatibility
+dotnet_diagnostic.CA1508.severity = warning # Avoid dead conditional code
+dotnet_diagnostic.CA1805.severity = warning # Member is explicitly initialized to its default value
+dotnet_diagnostic.CA1822.severity = suggestion # Member does not access instance data and can be marked as static
+dotnet_diagnostic.CA1852.severity = warning # Sealed classes
+dotnet_diagnostic.CA1859.severity = warning # Use concrete types when possible for improved performance
+dotnet_diagnostic.CA1860.severity = warning # Prefer comparing 'Count' to 0 rather than using 'Any()', both for clarity and for performance
+dotnet_diagnostic.CA2007.severity = warning # Do not directly await a Task
+dotnet_diagnostic.CA2201.severity = warning # Exception type System.Exception is not sufficiently specific
+
+dotnet_diagnostic.IDE0001.severity = warning # Simplify name
+dotnet_diagnostic.IDE0005.severity = warning # Remove unnecessary using directives
+dotnet_diagnostic.IDE0009.severity = warning # Add this or Me qualification
+dotnet_diagnostic.IDE0011.severity = warning # Add braces
+dotnet_diagnostic.IDE0018.severity = warning # Inline variable declaration
+dotnet_diagnostic.IDE0032.severity = warning # Use auto-implemented property
+dotnet_diagnostic.IDE0034.severity = warning # Simplify 'default' expression
+dotnet_diagnostic.IDE0035.severity = warning # Remove unreachable code
+dotnet_diagnostic.IDE0040.severity = warning # Add accessibility modifiers
+dotnet_diagnostic.IDE0049.severity = warning # Use language keywords instead of framework type names for type references
+dotnet_diagnostic.IDE0050.severity = warning # Convert anonymous type to tuple
+dotnet_diagnostic.IDE0051.severity = warning # Remove unused private member
+dotnet_diagnostic.IDE0055.severity = warning # Formatting rule
+dotnet_diagnostic.IDE0060.severity = warning # Remove unused parameter
+dotnet_diagnostic.IDE0070.severity = warning # Use 'System.HashCode.Combine'
+dotnet_diagnostic.IDE0071.severity = warning # Simplify interpolation
+dotnet_diagnostic.IDE0073.severity = warning # Require file header
+dotnet_diagnostic.IDE0082.severity = warning # Convert typeof to nameof
+dotnet_diagnostic.IDE0090.severity = warning # Simplify new expression
+dotnet_diagnostic.IDE0161.severity = warning # Use file-scoped namespace
+dotnet_diagnostic.IDE0280.severity = warning # Use nameof
+
+dotnet_diagnostic.VSTHRD111.severity = warning # Use .ConfigureAwait(bool)
+dotnet_diagnostic.VSTHRD200.severity = warning # Use Async suffix for async methods
+
+dotnet_diagnostic.RCS1021.severity = warning # Use expression-bodied lambda.
+dotnet_diagnostic.RCS1061.severity = warning # Merge 'if' with nested 'if'.
+dotnet_diagnostic.RCS1069.severity = warning # Remove unnecessary case label.
+dotnet_diagnostic.RCS1077.severity = warning # Optimize LINQ method call.
+dotnet_diagnostic.RCS1118.severity = warning # Mark local variable as const.
+dotnet_diagnostic.RCS1124.severity = warning # Inline local variable.
+dotnet_diagnostic.RCS1129.severity = warning # Remove redundant field initialization.
+dotnet_diagnostic.RCS1146.severity = warning # Use conditional access.
+dotnet_diagnostic.RCS1170.severity = warning # Use read-only auto-implemented property.
+dotnet_diagnostic.RCS1173.severity = warning # Use coalesce expression instead of 'if'.
+dotnet_diagnostic.RCS1186.severity = warning # Use Regex instance instead of static method.
+dotnet_diagnostic.RCS1188.severity = warning # Remove redundant auto-property initialization.
+dotnet_diagnostic.RCS1197.severity = suggestion # Optimize StringBuilder.AppendLine call.
+dotnet_diagnostic.RCS1201.severity = suggestion # Use method chaining.
+
+dotnet_diagnostic.IDE0001.severity = warning # Simplify name
+dotnet_diagnostic.IDE0002.severity = warning # Simplify member access
+dotnet_diagnostic.IDE0004.severity = warning # Remove unnecessary cast
+dotnet_diagnostic.IDE0032.severity = warning # Use auto property
+dotnet_diagnostic.IDE0035.severity = warning # Remove unreachable code
+dotnet_diagnostic.IDE0047.severity = warning # Parentheses can be removed
+dotnet_diagnostic.IDE0051.severity = warning # Remove unused private member
+dotnet_diagnostic.IDE0052.severity = warning # Remove unread private member
+dotnet_diagnostic.IDE0059.severity = warning # Unnecessary assignment of a value
+dotnet_diagnostic.IDE0110.severity = warning # Remove unnecessary discards
+dotnet_diagnostic.IDE1006.severity = warning # Naming rule violations
+
+# Suppressed diagnostics
+dotnet_diagnostic.CA1002.severity = none # Change 'List' in '...' to use 'Collection' ...
+dotnet_diagnostic.CA1031.severity = none # Do not catch general exception types
+dotnet_diagnostic.CA1032.severity = none # We're using RCS1194 which seems to cover more ctors
+dotnet_diagnostic.CA1034.severity = none # Do not nest type. Alternatively, change its accessibility so that it is not externally visible
+dotnet_diagnostic.CA1054.severity = none # Uri parameters should not be strings
+dotnet_diagnostic.CA1062.severity = none # Disable null check, C# already does it for us
+dotnet_diagnostic.CA1303.severity = none # Do not pass literals as localized parameters
+dotnet_diagnostic.CA1305.severity = none # Operation could vary based on current user's locale settings
+dotnet_diagnostic.CA1307.severity = none # Operation has an overload that takes a StringComparison
+dotnet_diagnostic.CA1508.severity = none # Avoid dead conditional code. Too many false positives.
+dotnet_diagnostic.CA1510.severity = none # ArgumentNullException.Throw
+dotnet_diagnostic.CA1512.severity = none # ArgumentOutOfRangeException.Throw
+dotnet_diagnostic.CA1515.severity = none # Making public types from exes internal
+dotnet_diagnostic.CA1707.severity = none # Identifiers should not contain underscores
+dotnet_diagnostic.CA1846.severity = none # Prefer 'AsSpan' over 'Substring'
+dotnet_diagnostic.CA1848.severity = none # For improved performance, use the LoggerMessage delegates
+dotnet_diagnostic.CA1849.severity = none # Use async equivalent; analyzer is currently noisy
+dotnet_diagnostic.CA1865.severity = none # StartsWith(char)
+dotnet_diagnostic.CA1867.severity = none # EndsWith(char)
+dotnet_diagnostic.CS1998.severity = none # async method lacks 'await' operators and will run synchronously
+dotnet_diagnostic.CA2000.severity = none # Call System.IDisposable.Dispose on object before all references to it are out of scope
+dotnet_diagnostic.CA2225.severity = none # Operator overloads have named alternates
+dotnet_diagnostic.CA2227.severity = none # Change to be read-only by removing the property setter
+dotnet_diagnostic.CA2249.severity = suggestion # Consider using 'Contains' method instead of 'IndexOf' method
+dotnet_diagnostic.CA2252.severity = none # Requires preview
+dotnet_diagnostic.CA2253.severity = none # Named placeholders in the logging message template should not be comprised of only numeric characters
+dotnet_diagnostic.CA2253.severity = none # Named placeholders in the logging message template should not be comprised of only numeric characters
+dotnet_diagnostic.CA2263.severity = suggestion # Use generic overload
+dotnet_diagnostic.CA5394.severity = none # Do not use insecure sources of randomness
+
+dotnet_diagnostic.VSTHRD003.severity = none # Waiting on thread from another context
+dotnet_diagnostic.VSTHRD103.severity = none # Use async equivalent; analyzer is currently noisy
+dotnet_diagnostic.VSTHRD111.severity = none # Use .ConfigureAwait(bool) is hidden by default, set to none to prevent IDE from changing on autosave
+
+dotnet_diagnostic.xUnit1004.severity = none # Test methods should not be skipped. Remove the Skip property to start running the test again.
+dotnet_diagnostic.xUnit1042.severity = none # Untyped data rows
+
+dotnet_diagnostic.RCS1032.severity = none # Remove redundant parentheses.
+dotnet_diagnostic.RCS1074.severity = none # Remove redundant constructor.
+dotnet_diagnostic.RCS1140.severity = none # Add exception to documentation comment.
+dotnet_diagnostic.RCS1141.severity = none # Add 'param' element to documentation comment.
+dotnet_diagnostic.RCS1142.severity = none # Add 'typeparam' element to documentation comment.
+dotnet_diagnostic.RCS1151.severity = none # Remove redundant cast.
+dotnet_diagnostic.RCS1158.severity = none # Static member in generic type should use a type parameter.
+dotnet_diagnostic.RCS1161.severity = none # Enum should declare explicit value
+dotnet_diagnostic.RCS1163.severity = none # Unused parameter 'foo'.
+dotnet_diagnostic.RCS1181.severity = none # Convert comment to documentation comment.
+dotnet_diagnostic.RCS1189.severity = none # Add region name to #endregion.
+dotnet_diagnostic.RCS1205.severity = none # Order named arguments according to the order of parameters.
+dotnet_diagnostic.RCS1212.severity = none # Remove redundant assignment.
+dotnet_diagnostic.RCS1217.severity = none # Convert interpolated string to concatenation.
+dotnet_diagnostic.RCS1222.severity = none # Merge preprocessor directives.
+dotnet_diagnostic.RCS1226.severity = none # Add paragraph to documentation comment.
+dotnet_diagnostic.RCS1229.severity = none # Use async/await when necessary.
+dotnet_diagnostic.RCS1234.severity = none # Enum duplicate value
+dotnet_diagnostic.RCS1238.severity = none # Avoid nested ?: operators.
+dotnet_diagnostic.RCS1241.severity = none # Implement IComparable when implementing IComparable
+dotnet_diagnostic.RCS1246.severity = none # Use element access
+dotnet_diagnostic.RCS1261.severity = none # Resource can be disposed asynchronously
+
+dotnet_diagnostic.IDE0010.severity = none # Populate switch
+dotnet_diagnostic.IDE0021.severity = none # Use block body for constructors
+dotnet_diagnostic.IDE0022.severity = none # Use block body for methods
+dotnet_diagnostic.IDE0024.severity = none # Use block body for operator
+dotnet_diagnostic.IDE0042.severity = none # Variable declaration can be deconstructed
+dotnet_diagnostic.IDE0046.severity = none # if statement can be simplified
+dotnet_diagnostic.IDE0056.severity = none # Indexing can be simplified
+dotnet_diagnostic.IDE0057.severity = none # Substring can be simplified
+dotnet_diagnostic.IDE0060.severity = none # Remove unused parameter
+dotnet_diagnostic.IDE0061.severity = none # Use block body for local function
+dotnet_diagnostic.IDE0079.severity = none # Remove unnecessary suppression.
+dotnet_diagnostic.IDE0080.severity = none # Remove unnecessary suppression operator.
+dotnet_diagnostic.IDE0100.severity = none # Remove unnecessary equality operator
+dotnet_diagnostic.IDE0130.severity = none # Namespace does not match folder structure
+dotnet_diagnostic.IDE0160.severity = none # Use block-scoped namespace
+dotnet_diagnostic.IDE0290.severity = none # Use primary constructor
+dotnet_diagnostic.IDE0305.severity = none # ToList can be simplified
+dotnet_diagnostic.IDE0330.severity = none # Use 'System.Threading.Lock'
+
+# Testing
+dotnet_diagnostic.Moq1400.severity = none # Explicitly choose a mocking behavior instead of relying on the default (Loose) behavior
+
+# Resharper disabled rules: https://www.jetbrains.com/help/resharper/Reference__Code_Inspections_CSHARP.html#CodeSmell
+resharper_not_resolved_in_text_highlighting = none # Disable Resharper's "Not resolved in text" highlighting
+resharper_check_namespace_highlighting = none # Disable Resharper's "Check namespace" highlighting
+resharper_object_creation_as_statement_highlighting = none # Disable Resharper's "Object creation as statement" highlighting
+
+###############################
+# Naming Conventions #
+###############################
+
+# Styles
+
+dotnet_naming_style.pascal_case_style.capitalization = pascal_case
+
+dotnet_naming_style.camel_case_style.capitalization = camel_case
+
+dotnet_naming_style.static_underscored.capitalization = camel_case
+dotnet_naming_style.static_underscored.required_prefix = s_
+
+dotnet_naming_style.underscored.capitalization = camel_case
+dotnet_naming_style.underscored.required_prefix = _
+
+dotnet_naming_style.uppercase_with_underscore_separator.capitalization = all_upper
+dotnet_naming_style.uppercase_with_underscore_separator.word_separator = _
+
+dotnet_naming_style.end_in_async.required_prefix =
+dotnet_naming_style.end_in_async.required_suffix = Async
+dotnet_naming_style.end_in_async.capitalization = pascal_case
+dotnet_naming_style.end_in_async.word_separator =
+
+# Symbols
+
+dotnet_naming_symbols.constant_fields.applicable_kinds = field
+dotnet_naming_symbols.constant_fields.applicable_accessibilities = *
+dotnet_naming_symbols.constant_fields.required_modifiers = const
+
+dotnet_naming_symbols.local_constant.applicable_kinds = local
+dotnet_naming_symbols.local_constant.applicable_accessibilities = *
+dotnet_naming_symbols.local_constant.required_modifiers = const
+
+dotnet_naming_symbols.private_static_fields.applicable_kinds = field
+dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private
+dotnet_naming_symbols.private_static_fields.required_modifiers = static
+
+dotnet_naming_symbols.private_fields.applicable_kinds = field
+dotnet_naming_symbols.private_fields.applicable_accessibilities = private
+
+dotnet_naming_symbols.any_async_methods.applicable_kinds = method
+dotnet_naming_symbols.any_async_methods.applicable_accessibilities = *
+dotnet_naming_symbols.any_async_methods.required_modifiers = async
+
+# Rules
+
+dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields
+dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style
+dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = error
+
+dotnet_naming_rule.local_constant_should_be_pascal_case.symbols = local_constant
+dotnet_naming_rule.local_constant_should_be_pascal_case.style = pascal_case_style
+dotnet_naming_rule.local_constant_should_be_pascal_case.severity = error
+
+dotnet_naming_rule.private_static_fields_underscored.symbols = private_static_fields
+dotnet_naming_rule.private_static_fields_underscored.style = static_underscored
+dotnet_naming_rule.private_static_fields_underscored.severity = error
+
+dotnet_naming_rule.private_fields_underscored.symbols = private_fields
+dotnet_naming_rule.private_fields_underscored.style = underscored
+dotnet_naming_rule.private_fields_underscored.severity = error
+
+dotnet_naming_rule.async_methods_end_in_async.symbols = any_async_methods
+dotnet_naming_rule.async_methods_end_in_async.style = end_in_async
+dotnet_naming_rule.async_methods_end_in_async.severity = error
+
+###############################
+# C# Coding Conventions #
+###############################
+
+# var preferences
+csharp_style_var_for_built_in_types = false:none
+csharp_style_var_when_type_is_apparent = false:none
+csharp_style_var_elsewhere = false:none
+# Expression-bodied members
+csharp_style_expression_bodied_methods = false:silent
+csharp_style_expression_bodied_constructors = false:silent
+csharp_style_expression_bodied_operators = false:silent
+csharp_style_expression_bodied_properties = true:silent
+csharp_style_expression_bodied_indexers = true:silent
+csharp_style_expression_bodied_accessors = true:silent
+# Pattern matching preferences
+csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
+csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
+# Null-checking preferences
+csharp_style_throw_expression = true:suggestion
+csharp_style_conditional_delegate_call = true:suggestion
+# Modifier preferences
+csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion
+# Expression-level preferences
+csharp_prefer_braces = true:error
+csharp_style_deconstructed_variable_declaration = true:suggestion
+csharp_prefer_simple_default_expression = true:suggestion
+csharp_style_prefer_local_over_anonymous_function = true:error
+csharp_style_inlined_variable_declaration = true:suggestion
+
+###############################
+# C# Formatting Rules #
+###############################
+
+# New line preferences
+csharp_new_line_before_open_brace = all
+csharp_new_line_before_else = true
+csharp_new_line_before_catch = true
+csharp_new_line_before_finally = true
+csharp_new_line_before_members_in_object_initializers = false # Does not work with resharper, forcing code to be on long lines instead of wrapping
+csharp_new_line_before_members_in_anonymous_types = true
+csharp_new_line_between_query_expression_clauses = true
+# Indentation preferences
+csharp_indent_braces = false
+csharp_indent_case_contents = true
+csharp_indent_case_contents_when_block = false
+csharp_indent_switch_labels = true
+csharp_indent_labels = flush_left
+# Space preferences
+csharp_space_after_cast = false
+csharp_space_after_keywords_in_control_flow_statements = true
+csharp_space_between_method_call_parameter_list_parentheses = false
+csharp_space_between_method_declaration_parameter_list_parentheses = false
+csharp_space_between_parentheses = false
+csharp_space_before_colon_in_inheritance_clause = true
+csharp_space_after_colon_in_inheritance_clause = true
+csharp_space_around_binary_operators = before_and_after
+csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
+csharp_space_between_method_call_name_and_opening_parenthesis = false
+csharp_space_between_method_call_empty_parameter_list_parentheses = false
+# Wrapping preferences
+csharp_preserve_single_line_statements = true
+csharp_preserve_single_line_blocks = true
+csharp_using_directive_placement = outside_namespace:warning
+csharp_prefer_simple_using_statement = true:suggestion
+csharp_style_namespace_declarations = file_scoped:warning
+csharp_style_prefer_method_group_conversion = true:silent
+csharp_style_prefer_top_level_statements = true:silent
+csharp_style_expression_bodied_lambdas = true:silent
+csharp_style_expression_bodied_local_functions = false:silent
+
+###############################
+# Resharper Rules #
+###############################
+
+# Resharper disabled rules: https://www.jetbrains.com/help/resharper/Reference__Code_Inspections_CSHARP.html#CodeSmell
+resharper_redundant_linebreak_highlighting = none # Disable Resharper's "Redundant line break" highlighting
+resharper_missing_linebreak_highlighting = none # Disable Resharper's "Missing line break" highlighting
+resharper_bad_empty_braces_line_breaks_highlighting = none # Disable Resharper's "Bad empty braces line breaks" highlighting
+resharper_missing_indent_highlighting = none # Disable Resharper's "Missing indent" highlighting
+resharper_missing_blank_lines_highlighting = none # Disable Resharper's "Missing blank lines" highlighting
+resharper_wrong_indent_size_highlighting = none # Disable Resharper's "Wrong indent size" highlighting
+resharper_bad_indent_highlighting = none # Disable Resharper's "Bad indent" highlighting
+resharper_bad_expression_braces_line_breaks_highlighting = none # Disable Resharper's "Bad expression braces line breaks" highlighting
+resharper_multiple_spaces_highlighting = none # Disable Resharper's "Multiple spaces" highlighting
+resharper_bad_expression_braces_indent_highlighting = none # Disable Resharper's "Bad expression braces indent" highlighting
+resharper_bad_control_braces_indent_highlighting = none # Disable Resharper's "Bad control braces indent" highlighting
+resharper_bad_preprocessor_indent_highlighting = none # Disable Resharper's "Bad preprocessor indent" highlighting
+resharper_redundant_blank_lines_highlighting = none # Disable Resharper's "Redundant blank lines" highlighting
+resharper_multiple_statements_on_one_line_highlighting = none # Disable Resharper's "Multiple statements on one line" highlighting
+resharper_bad_braces_spaces_highlighting = none # Disable Resharper's "Bad braces spaces" highlighting
+resharper_outdent_is_off_prev_level_highlighting = none # Disable Resharper's "Outdent is off previous level" highlighting
+resharper_bad_symbol_spaces_highlighting = none # Disable Resharper's "Bad symbol spaces" highlighting
+resharper_bad_colon_spaces_highlighting = none # Disable Resharper's "Bad colon spaces" highlighting
+resharper_bad_semicolon_spaces_highlighting = none # Disable Resharper's "Bad semicolon spaces" highlighting
+resharper_bad_square_brackets_spaces_highlighting = none # Disable Resharper's "Bad square brackets spaces" highlighting
+resharper_bad_parens_spaces_highlighting = none # Disable Resharper's "Bad parens spaces" highlighting
+
+# Resharper enabled rules: https://www.jetbrains.com/help/resharper/Reference__Code_Inspections_CSHARP.html#CodeSmell
+resharper_comment_typo_highlighting = suggestion # Resharper's "Comment typo" highlighting
+resharper_redundant_using_directive_highlighting = warning # Resharper's "Redundant using directive" highlighting
+resharper_inconsistent_naming_highlighting = warning # Resharper's "Inconsistent naming" highlighting
+resharper_redundant_this_qualifier_highlighting = warning # Resharper's "Redundant 'this' qualifier" highlighting
+resharper_arrange_this_qualifier_highlighting = warning # Resharper's "Arrange 'this' qualifier" highlighting
+csharp_style_prefer_primary_constructors = true:suggestion
+csharp_prefer_system_threading_lock = true:suggestion
+csharp_style_prefer_simple_property_accessors = true:suggestion
diff --git a/dotnet/.gitignore b/dotnet/.gitignore
new file mode 100644
index 0000000..ce1409a
--- /dev/null
+++ b/dotnet/.gitignore
@@ -0,0 +1,405 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
+
+# User-specific files
+*.rsuser
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Mono auto generated files
+mono_crash.*
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+[Ww][Ii][Nn]32/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
+[Aa][Rr][Mm]64[Ee][Cc]/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+[Ll]ogs/
+
+# Visual Studio 2015/2017 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUnit
+*.VisualState.xml
+TestResult.xml
+nunit-*.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# Benchmark Results
+BenchmarkDotNet.Artifacts/
+
+# .NET Core
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+# ASP.NET Scaffolding
+ScaffoldingReadMe.txt
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_h.h
+*.ilk
+*.meta
+*.obj
+*.iobj
+*.pch
+*.pdb
+*.ipdb
+*.pgc
+*.pgd
+*.rsp
+# but not Directory.Build.rsp, as it configures directory-level build defaults
+!Directory.Build.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*_wpftmp.csproj
+*.log
+*.tlog
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# Visual Studio Trace Files
+*.e2e
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# Coverlet is a free, cross platform Code Coverage Tool
+coverage*.json
+coverage*.xml
+coverage*.info
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# Note: Comment the next line if you want to checkin your web deploy settings,
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# NuGet Symbol Packages
+*.snupkg
+# The packages folder can be ignored because of Package Restore
+**/[Pp]ackages/*
+# except build/, which is used as an MSBuild target.
+!**/[Pp]ackages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/[Pp]ackages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+*.appx
+*.appxbundle
+*.appxupload
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!?*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+ServiceFabricBackup/
+*.rptproj.bak
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+*.rptproj.rsuser
+*- [Bb]ackup.rdl
+*- [Bb]ackup ([0-9]).rdl
+*- [Bb]ackup ([0-9][0-9]).rdl
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio 6 auto-generated project file (contains which files were open etc.)
+*.vbp
+
+# Visual Studio 6 workspace and project file (working project files containing files to include in project)
+*.dsw
+*.dsp
+
+# Visual Studio 6 technical files
+*.ncb
+*.aps
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# CodeRush personal settings
+.cr/personal
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Tabs Studio
+*.tss
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
+
+# OpenCover UI analysis results
+OpenCover/
+
+# Azure Stream Analytics local run output
+ASALocalRun/
+
+# MSBuild Binary and Structured Log
+*.binlog
+
+# AWS SAM Build and Temporary Artifacts folder
+.aws-sam
+
+# NVidia Nsight GPU debugger configuration file
+*.nvuser
+
+# MFractors (Xamarin productivity tool) working folder
+.mfractor/
+
+# Local History for Visual Studio
+.localhistory/
+
+# Visual Studio History (VSHistory) files
+.vshistory/
+
+# BeatPulse healthcheck temp database
+healthchecksdb
+
+# Backup folder for Package Reference Convert tool in Visual Studio 2017
+MigrationBackup/
+
+# Ionide (cross platform F# VS Code tools) working folder
+.ionide/
+
+# Fody - auto-generated XML schema
+FodyWeavers.xsd
+
+# VS Code files for those working on multiple tools
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+*.code-workspace
+
+# Local History for Visual Studio Code
+.history/
+
+# Windows Installer files from build outputs
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# JetBrains Rider
+*.sln.iml
\ No newline at end of file
diff --git a/dotnet/.vscode/extensions.json b/dotnet/.vscode/extensions.json
new file mode 100644
index 0000000..fe812d7
--- /dev/null
+++ b/dotnet/.vscode/extensions.json
@@ -0,0 +1,5 @@
+{
+ "recommendations": [
+ "ms-dotnettools.csdevkit"
+ ]
+}
\ No newline at end of file
diff --git a/dotnet/.vscode/settings.json b/dotnet/.vscode/settings.json
new file mode 100644
index 0000000..4fa848a
--- /dev/null
+++ b/dotnet/.vscode/settings.json
@@ -0,0 +1,5 @@
+{
+ "dotnet.defaultSolution": "agent-framework-dotnet.slnx",
+ "git.openRepositoryInParentFolders": "always",
+ "chat.agent.enabled": true
+}
diff --git a/dotnet/.vscode/tasks.json b/dotnet/.vscode/tasks.json
new file mode 100644
index 0000000..85beec3
--- /dev/null
+++ b/dotnet/.vscode/tasks.json
@@ -0,0 +1,15 @@
+{
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "type": "dotnet",
+ "task": "build",
+ "group": {
+ "kind": "build",
+ "isDefault": true
+ },
+ "problemMatcher": [],
+ "label": "dotnet: build"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/dotnet/Directory.Build.props b/dotnet/Directory.Build.props
new file mode 100644
index 0000000..2482c43
--- /dev/null
+++ b/dotnet/Directory.Build.props
@@ -0,0 +1,50 @@
+
+
+
+ true
+ true
+ 10.0-all
+ true
+ latest
+ enable
+ $(NoWarn);NU5128;CS8002
+ true
+ net10.0;net9.0;net8.0
+ $(TargetFrameworksCore);netstandard2.0;net472
+ true
+ Debug;Release;Publish
+
+
+
+ false
+
+
+
+
+ false
+
+
+
+ True
+
+
+
+
+ $(NoWarn);nullable
+
+
+
+ $([System.IO.Path]::GetDirectoryName($([MSBuild]::GetPathOfFileAbove('CODE_OF_CONDUCT.md', '$(MSBuildThisFileDirectory)'))))
+
+
+
+
+
+ <_Parameter1>false
+
+
+
+
+
+
+
diff --git a/dotnet/Directory.Build.targets b/dotnet/Directory.Build.targets
new file mode 100644
index 0000000..5e62f1c
--- /dev/null
+++ b/dotnet/Directory.Build.targets
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props
new file mode 100644
index 0000000..d721e20
--- /dev/null
+++ b/dotnet/Directory.Packages.props
@@ -0,0 +1,184 @@
+
+
+
+
+ true
+ true
+
+
+
+ 13.0.2
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
\ No newline at end of file
diff --git a/dotnet/README.md b/dotnet/README.md
new file mode 100644
index 0000000..4e52260
--- /dev/null
+++ b/dotnet/README.md
@@ -0,0 +1,41 @@
+# Get Started with Microsoft Agent Framework for C# Developers
+
+## Samples
+
+- [Getting Started with Agents](./samples/GettingStarted/Agents): basic agent creation and tool usage
+- [Agent Provider Samples](./samples/GettingStarted/AgentProviders): samples showing different agent providers
+- [Workflow Samples](./samples/GettingStarted/Workflows): advanced multi-agent patterns and workflow orchestration
+
+## Quickstart
+
+### Basic Agent - .NET
+
+```c#
+using System;
+using Azure.AI.OpenAI;
+using Azure.Identity;
+using Microsoft.Agents.AI;
+
+var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")!;
+var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME")!;
+
+var agent = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential())
+ .GetOpenAIResponseClient(deploymentName)
+ .AsAIAgent(name: "HaikuBot", instructions: "You are an upbeat assistant that writes beautifully.");
+
+Console.WriteLine(await agent.RunAsync("Write a haiku about Microsoft Agent Framework."));
+```
+
+## Examples & Samples
+
+- [Getting Started with Agents](./samples/GettingStarted/Agents): basic agent creation and tool usage
+- [Agent Provider Samples](./samples/GettingStarted/AgentProviders): samples showing different agent providers
+- [Workflow Samples](./samples/GettingStarted/Workflows): advanced multi-agent patterns and workflow orchestration
+
+## Agent Framework Documentation
+
+- [Documentation](https://learn.microsoft.com/agent-framework/)
+- [Agent Framework Repository](https://github.com/microsoft/agent-framework)
+- [Design Documents](../docs/design)
+- [Architectural Decision Records](../docs/decisions)
+- [MSFT Learn Docs](https://learn.microsoft.com/agent-framework/overview/agent-framework-overview)
diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx
new file mode 100644
index 0000000..8b1b00f
--- /dev/null
+++ b/dotnet/agent-framework-dotnet.slnx
@@ -0,0 +1,460 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dotnet/agent-framework-release.slnf b/dotnet/agent-framework-release.slnf
new file mode 100644
index 0000000..ed8ac19
--- /dev/null
+++ b/dotnet/agent-framework-release.slnf
@@ -0,0 +1,31 @@
+{
+ "solution": {
+ "path": "agent-framework-dotnet.slnx",
+ "projects": [
+ "src\\Microsoft.Agents.AI.A2A\\Microsoft.Agents.AI.A2A.csproj",
+ "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.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",
+ "src\\Microsoft.Agents.AI.CosmosNoSql\\Microsoft.Agents.AI.CosmosNoSql.csproj",
+ "src\\Microsoft.Agents.AI.Declarative\\Microsoft.Agents.AI.Declarative.csproj",
+ "src\\Microsoft.Agents.AI.DevUI\\Microsoft.Agents.AI.DevUI.csproj",
+ "src\\Microsoft.Agents.AI.DurableTask\\Microsoft.Agents.AI.DurableTask.csproj",
+ "src\\Microsoft.Agents.AI.Hosting.A2A.AspNetCore\\Microsoft.Agents.AI.Hosting.A2A.AspNetCore.csproj",
+ "src\\Microsoft.Agents.AI.Hosting.A2A\\Microsoft.Agents.AI.Hosting.A2A.csproj",
+ "src\\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore\\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.csproj",
+ "src\\Microsoft.Agents.AI.Hosting.AzureFunctions\\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj",
+ "src\\Microsoft.Agents.AI.Hosting.OpenAI\\Microsoft.Agents.AI.Hosting.OpenAI.csproj",
+ "src\\Microsoft.Agents.AI.Hosting\\Microsoft.Agents.AI.Hosting.csproj",
+ "src\\Microsoft.Agents.AI.Mem0\\Microsoft.Agents.AI.Mem0.csproj",
+ "src\\Microsoft.Agents.AI.OpenAI\\Microsoft.Agents.AI.OpenAI.csproj",
+ "src\\Microsoft.Agents.AI.Purview\\Microsoft.Agents.AI.Purview.csproj",
+ "src\\Microsoft.Agents.AI.Workflows.Declarative.AzureAI\\Microsoft.Agents.AI.Workflows.Declarative.AzureAI.csproj",
+ "src\\Microsoft.Agents.AI.Workflows.Declarative\\Microsoft.Agents.AI.Workflows.Declarative.csproj",
+ "src\\Microsoft.Agents.AI.Workflows\\Microsoft.Agents.AI.Workflows.csproj",
+ "src\\Microsoft.Agents.AI\\Microsoft.Agents.AI.csproj"
+ ]
+ }
+}
diff --git a/dotnet/eng/MSBuild/LegacySupport.props b/dotnet/eng/MSBuild/LegacySupport.props
new file mode 100644
index 0000000..54d6528
--- /dev/null
+++ b/dotnet/eng/MSBuild/LegacySupport.props
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dotnet/eng/MSBuild/Shared.props b/dotnet/eng/MSBuild/Shared.props
new file mode 100644
index 0000000..da8806a
--- /dev/null
+++ b/dotnet/eng/MSBuild/Shared.props
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/eng/MSBuild/Shared.targets b/dotnet/eng/MSBuild/Shared.targets
new file mode 100644
index 0000000..5eaa1a0
--- /dev/null
+++ b/dotnet/eng/MSBuild/Shared.targets
@@ -0,0 +1,7 @@
+
+
+
+ true
+ true
+
+
diff --git a/dotnet/global.json b/dotnet/global.json
new file mode 100644
index 0000000..54533bf
--- /dev/null
+++ b/dotnet/global.json
@@ -0,0 +1,7 @@
+{
+ "sdk": {
+ "version": "10.0.100",
+ "rollForward": "minor",
+ "allowPrerelease": false
+ }
+}
\ No newline at end of file
diff --git a/dotnet/nuget.config b/dotnet/nuget.config
new file mode 100644
index 0000000..76d943c
--- /dev/null
+++ b/dotnet/nuget.config
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dotnet/nuget/NUGET.md b/dotnet/nuget/NUGET.md
new file mode 100644
index 0000000..8f3a9df
--- /dev/null
+++ b/dotnet/nuget/NUGET.md
@@ -0,0 +1,21 @@
+# About Microsoft Agent Framework
+
+Microsoft Agent Framework is a comprehensive .NET library for building, orchestrating, and deploying AI agents and multi-agent workflows. The framework provides everything from simple chat agents to complex multi-agent systems with graph-based orchestration capabilities.
+
+## Key Features
+
+- **Multi-Agent Orchestration**: Coordinate multiple agents using sequential, concurrent, group chat, and handoff patterns
+- **Graph-based Workflows**: Connect agents and functions with streaming, checkpointing, and human-in-the-loop capabilities, with both imperative or declarative workflow support
+- **Multiple Provider Support**: Seamlessly integrate with various LLM providers with more being added continuously
+- **Extensible Middleware**: Flexible request/response processing with custom pipelines and exception handling
+- **Built-in Observability**: OpenTelemetry integration for distributed tracing, monitoring, and debugging
+- **Cross-Platform**: Compatible with .NET 8.0, .NET Standard 2.0, and .NET Framework for broad deployment options
+
+Whether you're building simple AI assistants or complex multi-agent systems, Microsoft Agent Framework provides the tools and abstractions needed to create robust, scalable AI applications in .NET.
+
+# Getting Started ⚡
+
+- Learn more at the [documentation site](https://learn.microsoft.com/agent-framework/overview/agent-framework-overview).
+- Join the [Discord community](https://discord.gg/b5zjErwbQM).
+- Follow the team on [Semantic Kernel blog](https://devblogs.microsoft.com/semantic-kernel/).
+- Check out the [GitHub repository](https://github.com/microsoft/agent-framework) for the latest updates.
diff --git a/dotnet/nuget/icon.png b/dotnet/nuget/icon.png
new file mode 100644
index 0000000..ae30719
Binary files /dev/null and b/dotnet/nuget/icon.png differ
diff --git a/dotnet/nuget/nuget-package.props b/dotnet/nuget/nuget-package.props
new file mode 100644
index 0000000..9fd1487
--- /dev/null
+++ b/dotnet/nuget/nuget-package.props
@@ -0,0 +1,71 @@
+
+
+
+ 1.0.0
+ $(VersionPrefix)-$(VersionSuffix).260121.1
+ $(VersionPrefix)-preview.260121.1
+ 1.0.0-preview.260121.1
+
+ Debug;Release;Publish
+ true
+
+
+ 0.0.1
+
+ $(NoWarn);CP0003
+
+ $(NoWarn);CP1002
+
+
+ true
+
+
+ all
+
+
+ low
+
+
+ Microsoft
+ Microsoft
+ Microsoft Agent Framework
+ Microsoft Agent Framework is a comprehensive .NET library for building, orchestrating, and deploying AI agents and multi-agent workflows. The framework provides everything from simple chat agents to complex multi-agent systems with graph-based orchestration capabilities.
+ AI, Artificial Intelligence, Agent, SDK, Framework
+ $(AssemblyName)
+
+
+ MIT
+ © Microsoft Corporation. All rights reserved.
+ https://learn.microsoft.com/agent-framework/
+ https://github.com/microsoft/agent-framework
+ true
+
+
+ icon.png
+ icon.png
+ NUGET.md
+
+
+ true
+ snupkg
+
+
+ bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+
+
diff --git a/dotnet/samples/.editorconfig b/dotnet/samples/.editorconfig
new file mode 100644
index 0000000..6da078d
--- /dev/null
+++ b/dotnet/samples/.editorconfig
@@ -0,0 +1,17 @@
+# Suppressing errors for Sample projects under dotnet/samples folder
+[*.cs]
+dotnet_diagnostic.CA1716.severity = none # Add summary to documentation comment.
+dotnet_diagnostic.CA1873.severity = none # Evaluation of logging arguments may be expensive
+dotnet_diagnostic.CA2000.severity = none # Call System.IDisposable.Dispose on object before all references to it are out of scope
+dotnet_diagnostic.CA2007.severity = none # Do not directly await a Task
+
+dotnet_diagnostic.CS1591.severity = none # Missing XML comment for publicly visible type or member
+
+dotnet_diagnostic.IDE1006.severity = warning # Naming rule violations
+
+dotnet_diagnostic.VSTHRD111.severity = none # Use .ConfigureAwait(bool) is hidden by default, set to none to prevent IDE from changing on autosave
+dotnet_diagnostic.VSTHRD200.severity = none # Use Async suffix for async methods
+
+dotnet_diagnostic.MEAI001.severity = none # [Experimental] APIs in Microsoft.Extensions.AI
+dotnet_diagnostic.OPENAI001.severity = none # [Experimental] APIs in OpenAI
+dotnet_diagnostic.SKEXP0110.severity = none # [Experimental] APIs in Microsoft.SemanticKernel
\ No newline at end of file
diff --git a/dotnet/samples/A2AClientServer/A2AClient/A2AClient.csproj b/dotnet/samples/A2AClientServer/A2AClient/A2AClient.csproj
new file mode 100644
index 0000000..6b88c5c
--- /dev/null
+++ b/dotnet/samples/A2AClientServer/A2AClient/A2AClient.csproj
@@ -0,0 +1,23 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+ 5ee045b0-aea3-4f08-8d31-32d1a6f8fed0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/A2AClientServer/A2AClient/HostClientAgent.cs b/dotnet/samples/A2AClientServer/A2AClient/HostClientAgent.cs
new file mode 100644
index 0000000..4daf2c5
--- /dev/null
+++ b/dotnet/samples/A2AClientServer/A2AClient/HostClientAgent.cs
@@ -0,0 +1,62 @@
+// Copyright (c) Microsoft. All rights reserved.
+using System.ClientModel;
+using Microsoft.Agents.AI;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.Logging;
+using OpenAI;
+using OpenAI.Chat;
+
+namespace A2A;
+
+internal sealed class HostClientAgent
+{
+ internal HostClientAgent(ILoggerFactory loggerFactory)
+ {
+ this._logger = loggerFactory.CreateLogger("HostClientAgent");
+ }
+
+ internal async Task InitializeAgentAsync(string modelId, string apiKey, string[] agentUrls)
+ {
+ try
+ {
+ this._logger.LogInformation("Initializing Agent Framework agent with model: {ModelId}", modelId);
+
+ // Connect to the remote agents via A2A
+ var createAgentTasks = agentUrls.Select(CreateAgentAsync);
+ var agents = await Task.WhenAll(createAgentTasks);
+ var tools = agents.Select(agent => (AITool)agent.AsAIFunction()).ToList();
+
+ // Create the agent that uses the remote agents as tools
+ this.Agent = new OpenAIClient(new ApiKeyCredential(apiKey))
+ .GetChatClient(modelId)
+ .AsAIAgent(instructions: "You specialize in handling queries for users and using your tools to provide answers.", name: "HostClient", tools: tools);
+ }
+ catch (Exception ex)
+ {
+ this._logger.LogError(ex, "Failed to initialize HostClientAgent");
+ throw;
+ }
+ }
+
+ ///
+ /// The associated
+ ///
+ public AIAgent? Agent { get; private set; }
+
+ #region private
+ private readonly ILogger _logger;
+
+ private static async Task CreateAgentAsync(string agentUri)
+ {
+ var url = new Uri(agentUri);
+ var httpClient = new HttpClient
+ {
+ Timeout = TimeSpan.FromSeconds(60)
+ };
+
+ var agentCardResolver = new A2ACardResolver(url, httpClient);
+
+ return await agentCardResolver.GetAIAgentAsync();
+ }
+ #endregion
+}
diff --git a/dotnet/samples/A2AClientServer/A2AClient/Program.cs b/dotnet/samples/A2AClientServer/A2AClient/Program.cs
new file mode 100644
index 0000000..b701ea7
--- /dev/null
+++ b/dotnet/samples/A2AClientServer/A2AClient/Program.cs
@@ -0,0 +1,79 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.CommandLine;
+using System.Reflection;
+using Microsoft.Agents.AI;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+
+namespace A2A;
+
+public static class Program
+{
+ public static async Task Main(string[] args)
+ {
+ // Create root command with options
+ var rootCommand = new RootCommand("A2AClient");
+ rootCommand.SetAction((_, ct) => HandleCommandsAsync(ct));
+
+ // Run the command
+ return await rootCommand.Parse(args).InvokeAsync();
+ }
+
+ private static async Task HandleCommandsAsync(CancellationToken cancellationToken)
+ {
+ // Set up the logging
+ using var loggerFactory = LoggerFactory.Create(builder =>
+ {
+ builder.AddConsole();
+ builder.SetMinimumLevel(LogLevel.Information);
+ });
+ var logger = loggerFactory.CreateLogger("A2AClient");
+
+ // Retrieve configuration settings
+ IConfigurationRoot configRoot = new ConfigurationBuilder()
+ .AddEnvironmentVariables()
+ .AddUserSecrets(Assembly.GetExecutingAssembly())
+ .Build();
+ var apiKey = configRoot["A2AClient:ApiKey"] ?? throw new ArgumentException("A2AClient:ApiKey must be provided");
+ var modelId = configRoot["A2AClient:ModelId"] ?? "gpt-4.1";
+ var agentUrls = configRoot["A2AClient:AgentUrls"] ?? "http://localhost:5000/;http://localhost:5001/;http://localhost:5002/";
+
+ // Create the Host agent
+ var hostAgent = new HostClientAgent(loggerFactory);
+ await hostAgent.InitializeAgentAsync(modelId, apiKey, agentUrls!.Split(";"));
+ AgentThread thread = await hostAgent.Agent!.GetNewThreadAsync(cancellationToken);
+ try
+ {
+ while (true)
+ {
+ // Get user message
+ Console.Write("\nUser (:q or quit to exit): ");
+ string? message = Console.ReadLine();
+ if (string.IsNullOrWhiteSpace(message))
+ {
+ Console.WriteLine("Request cannot be empty.");
+ continue;
+ }
+
+ if (message is ":q" or "quit")
+ {
+ break;
+ }
+
+ var agentResponse = await hostAgent.Agent!.RunAsync(message, thread, cancellationToken: cancellationToken);
+ foreach (var chatMessage in agentResponse.Messages)
+ {
+ Console.ForegroundColor = ConsoleColor.Cyan;
+ Console.WriteLine($"\nAgent: {chatMessage.Text}");
+ Console.ResetColor();
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "An error occurred while running the A2AClient");
+ return;
+ }
+ }
+}
diff --git a/dotnet/samples/A2AClientServer/A2AClient/README.md b/dotnet/samples/A2AClientServer/A2AClient/README.md
new file mode 100644
index 0000000..c542430
--- /dev/null
+++ b/dotnet/samples/A2AClientServer/A2AClient/README.md
@@ -0,0 +1,26 @@
+
+# A2A Client Sample
+Show how to create an A2A Client with a command line interface which invokes agents using the A2A protocol.
+
+## Run the Sample
+
+To run the sample, follow these steps:
+
+1. Run the A2A client:
+ ```bash
+ cd A2AClient
+ dotnet run
+ ```
+2. Enter your request e.g. "Show me all invoices for Contoso?"
+
+## Set Environment Variables
+
+The agent urls are provided as a ` ` delimited list of strings
+
+```powershell
+cd dotnet/samples/A2AClientServer/A2AClient
+
+$env:OPENAI_MODEL="gpt-4o-mini"
+$env:OPENAI_API_KEY=""
+$env:AGENT_URLS="http://localhost:5000/policy;http://localhost:5000/invoice;http://localhost:5000/logistics"
+```
diff --git a/dotnet/samples/A2AClientServer/A2AServer/A2AServer.csproj b/dotnet/samples/A2AClientServer/A2AServer/A2AServer.csproj
new file mode 100644
index 0000000..0a3b170
--- /dev/null
+++ b/dotnet/samples/A2AClientServer/A2AServer/A2AServer.csproj
@@ -0,0 +1,30 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+ 5ee045b0-aea3-4f08-8d31-32d1a6f8fed0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/A2AClientServer/A2AServer/A2AServer.http b/dotnet/samples/A2AClientServer/A2AServer/A2AServer.http
new file mode 100644
index 0000000..9e50c67
--- /dev/null
+++ b/dotnet/samples/A2AClientServer/A2AServer/A2AServer.http
@@ -0,0 +1,85 @@
+### Each A2A agent is available at a different host address
+@hostInvoice = http://localhost:5000
+@hostPolicy = http://localhost:5001
+@hostLogistics = http://localhost:5002
+
+### Query agent card for the invoice agent
+GET {{hostInvoice}}/.well-known/agent-card.json
+
+### Send a message to the invoice agent
+POST {{hostInvoice}}
+Content-Type: application/json
+
+{
+ "id": "1",
+ "jsonrpc": "2.0",
+ "method": "message/send",
+ "params": {
+ "id": "12345",
+ "message": {
+ "kind": "message",
+ "role": "user",
+ "messageId": "msg_1",
+ "parts": [
+ {
+ "kind": "text",
+ "text": "Show me all invoices for Contoso?"
+ }
+ ]
+ }
+ }
+}
+
+### Query agent card for the policy agent
+GET {{hostPolicy}}/.well-known/agent-card.json
+
+### Send a message to the policy agent
+POST {{hostPolicy}}
+Content-Type: application/json
+
+{
+ "id": "1",
+ "jsonrpc": "2.0",
+ "method": "message/send",
+ "params": {
+ "id": "12345",
+ "message": {
+ "kind": "message",
+ "role": "user",
+ "messageId": "msg_1",
+ "parts": [
+ {
+ "kind": "text",
+ "text": "What is the policy for short shipments?"
+ }
+ ]
+ }
+ }
+}
+
+### Query agent card for the logistics agent
+GET {{hostLogistics}}/.well-known/agent-card.json
+
+### Send a message to the logistics agent
+POST {{hostLogistics}}
+Content-Type: application/json
+
+{
+ "id": "1",
+ "jsonrpc": "2.0",
+ "method": "message/send",
+ "params": {
+ "id": "12345",
+ "message": {
+ "kind": "message",
+ "role": "user",
+ "messageId": "msg_1",
+ "parts": [
+ {
+ "kind": "text",
+ "text": "What is the status for SHPMT-SAP-001?"
+ }
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/dotnet/samples/A2AClientServer/A2AServer/HostAgentFactory.cs b/dotnet/samples/A2AClientServer/A2AServer/HostAgentFactory.cs
new file mode 100644
index 0000000..8af2b01
--- /dev/null
+++ b/dotnet/samples/A2AClientServer/A2AServer/HostAgentFactory.cs
@@ -0,0 +1,148 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using A2A;
+using Azure.AI.Agents.Persistent;
+using Azure.Identity;
+using Microsoft.Agents.AI;
+using Microsoft.Extensions.AI;
+using OpenAI;
+using OpenAI.Chat;
+
+namespace A2AServer;
+
+internal static class HostAgentFactory
+{
+ internal static async Task<(AIAgent, AgentCard)> CreateFoundryHostAgentAsync(string agentType, string model, string endpoint, string assistantId, IList? tools = null)
+ {
+ var persistentAgentsClient = new PersistentAgentsClient(endpoint, new AzureCliCredential());
+ PersistentAgent persistentAgent = await persistentAgentsClient.Administration.GetAgentAsync(assistantId);
+
+ AIAgent agent = await persistentAgentsClient
+ .GetAIAgentAsync(persistentAgent.Id, chatOptions: new() { Tools = tools });
+
+ AgentCard agentCard = agentType.ToUpperInvariant() switch
+ {
+ "INVOICE" => GetInvoiceAgentCard(),
+ "POLICY" => GetPolicyAgentCard(),
+ "LOGISTICS" => GetLogisticsAgentCard(),
+ _ => throw new ArgumentException($"Unsupported agent type: {agentType}"),
+ };
+
+ return new(agent, agentCard);
+ }
+
+ internal static async Task<(AIAgent, AgentCard)> CreateChatCompletionHostAgentAsync(string agentType, string model, string apiKey, string name, string instructions, IList? tools = null)
+ {
+ AIAgent agent = new OpenAIClient(apiKey)
+ .GetChatClient(model)
+ .AsAIAgent(instructions, name, tools: tools);
+
+ AgentCard agentCard = agentType.ToUpperInvariant() switch
+ {
+ "INVOICE" => GetInvoiceAgentCard(),
+ "POLICY" => GetPolicyAgentCard(),
+ "LOGISTICS" => GetLogisticsAgentCard(),
+ _ => throw new ArgumentException($"Unsupported agent type: {agentType}"),
+ };
+
+ return new(agent, agentCard);
+ }
+
+ #region private
+ private static AgentCard GetInvoiceAgentCard()
+ {
+ var capabilities = new AgentCapabilities()
+ {
+ Streaming = false,
+ PushNotifications = false,
+ };
+
+ var invoiceQuery = new AgentSkill()
+ {
+ Id = "id_invoice_agent",
+ Name = "InvoiceQuery",
+ Description = "Handles requests relating to invoices.",
+ Tags = ["invoice", "semantic-kernel"],
+ Examples =
+ [
+ "List the latest invoices for Contoso.",
+ ],
+ };
+
+ return new()
+ {
+ Name = "InvoiceAgent",
+ Description = "Handles requests relating to invoices.",
+ Version = "1.0.0",
+ DefaultInputModes = ["text"],
+ DefaultOutputModes = ["text"],
+ Capabilities = capabilities,
+ Skills = [invoiceQuery],
+ };
+ }
+
+ private static AgentCard GetPolicyAgentCard()
+ {
+ var capabilities = new AgentCapabilities()
+ {
+ Streaming = false,
+ PushNotifications = false,
+ };
+
+ var policyQuery = new AgentSkill()
+ {
+ Id = "id_policy_agent",
+ Name = "PolicyAgent",
+ Description = "Handles requests relating to policies and customer communications.",
+ Tags = ["policy", "semantic-kernel"],
+ Examples =
+ [
+ "What is the policy for short shipments?",
+ ],
+ };
+
+ return new AgentCard()
+ {
+ Name = "PolicyAgent",
+ Description = "Handles requests relating to policies and customer communications.",
+ Version = "1.0.0",
+ DefaultInputModes = ["text"],
+ DefaultOutputModes = ["text"],
+ Capabilities = capabilities,
+ Skills = [policyQuery],
+ };
+ }
+
+ private static AgentCard GetLogisticsAgentCard()
+ {
+ var capabilities = new AgentCapabilities()
+ {
+ Streaming = false,
+ PushNotifications = false,
+ };
+
+ var logisticsQuery = new AgentSkill()
+ {
+ Id = "id_logistics_agent",
+ Name = "LogisticsQuery",
+ Description = "Handles requests relating to logistics.",
+ Tags = ["logistics", "semantic-kernel"],
+ Examples =
+ [
+ "What is the status for SHPMT-SAP-001",
+ ],
+ };
+
+ return new AgentCard()
+ {
+ Name = "LogisticsAgent",
+ Description = "Handles requests relating to logistics.",
+ Version = "1.0.0",
+ DefaultInputModes = ["text"],
+ DefaultOutputModes = ["text"],
+ Capabilities = capabilities,
+ Skills = [logisticsQuery],
+ };
+ }
+ #endregion
+}
diff --git a/dotnet/samples/A2AClientServer/A2AServer/Models/InvoiceQuery.cs b/dotnet/samples/A2AClientServer/A2AServer/Models/InvoiceQuery.cs
new file mode 100644
index 0000000..2b2d142
--- /dev/null
+++ b/dotnet/samples/A2AClientServer/A2AServer/Models/InvoiceQuery.cs
@@ -0,0 +1,167 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.ComponentModel;
+
+namespace A2A;
+
+///
+/// A simple invoice plugin that returns mock data.
+///
+public class Product
+{
+ public string Name { get; set; }
+ public int Quantity { get; set; }
+ public decimal Price { get; set; } // Price per unit
+
+ public Product(string name, int quantity, decimal price)
+ {
+ this.Name = name;
+ this.Quantity = quantity;
+ this.Price = price;
+ }
+
+ public decimal TotalPrice() => this.Quantity * this.Price; // Total price for this product
+}
+
+public class Invoice
+{
+ public string TransactionId { get; set; }
+ public string InvoiceId { get; set; }
+ public string CompanyName { get; set; }
+ public DateTime InvoiceDate { get; set; }
+ public List Products { get; set; } // List of products
+
+ public Invoice(string transactionId, string invoiceId, string companyName, DateTime invoiceDate, List products)
+ {
+ this.TransactionId = transactionId;
+ this.InvoiceId = invoiceId;
+ this.CompanyName = companyName;
+ this.InvoiceDate = invoiceDate;
+ this.Products = products;
+ }
+
+ public decimal TotalInvoicePrice() => this.Products.Sum(product => product.TotalPrice()); // Total price of all products in the invoice
+}
+
+public class InvoiceQuery
+{
+ private readonly List _invoices;
+
+ public InvoiceQuery()
+ {
+ // Extended mock data with quantities and prices
+ this._invoices =
+ [
+ new("TICKET-XYZ987", "INV789", "Contoso", GetRandomDateWithinLastTwoMonths(),
+ [
+ new("T-Shirts", 150, 10.00m),
+ new("Hats", 200, 15.00m),
+ new("Glasses", 300, 5.00m)
+ ]),
+ new("TICKET-XYZ111", "INV111", "XStore", GetRandomDateWithinLastTwoMonths(),
+ [
+ new("T-Shirts", 2500, 12.00m),
+ new("Hats", 1500, 8.00m),
+ new("Glasses", 200, 20.00m)
+ ]),
+ new("TICKET-XYZ222", "INV222", "Cymbal Direct", GetRandomDateWithinLastTwoMonths(),
+ [
+ new("T-Shirts", 1200, 14.00m),
+ new("Hats", 800, 7.00m),
+ new("Glasses", 500, 25.00m)
+ ]),
+ new("TICKET-XYZ333", "INV333", "Contoso", GetRandomDateWithinLastTwoMonths(),
+ [
+ new("T-Shirts", 400, 11.00m),
+ new("Hats", 600, 15.00m),
+ new("Glasses", 700, 5.00m)
+ ]),
+ new("TICKET-XYZ444", "INV444", "XStore", GetRandomDateWithinLastTwoMonths(),
+ [
+ new("T-Shirts", 800, 10.00m),
+ new("Hats", 500, 18.00m),
+ new("Glasses", 300, 22.00m)
+ ]),
+ new("TICKET-XYZ555", "INV555", "Cymbal Direct", GetRandomDateWithinLastTwoMonths(),
+ [
+ new("T-Shirts", 1100, 9.00m),
+ new("Hats", 900, 12.00m),
+ new("Glasses", 1200, 15.00m)
+ ]),
+ new("TICKET-XYZ666", "INV666", "Contoso", GetRandomDateWithinLastTwoMonths(),
+ [
+ new("T-Shirts", 2500, 8.00m),
+ new("Hats", 1200, 10.00m),
+ new("Glasses", 1000, 6.00m)
+ ]),
+ new("TICKET-XYZ777", "INV777", "XStore", GetRandomDateWithinLastTwoMonths(),
+ [
+ new("T-Shirts", 1900, 13.00m),
+ new("Hats", 1300, 16.00m),
+ new("Glasses", 800, 19.00m)
+ ]),
+ new("TICKET-XYZ888", "INV888", "Cymbal Direct", GetRandomDateWithinLastTwoMonths(),
+ [
+ new("T-Shirts", 2200, 11.00m),
+ new("Hats", 1700, 8.50m),
+ new("Glasses", 600, 21.00m)
+ ]),
+ new("TICKET-XYZ999", "INV999", "Contoso", GetRandomDateWithinLastTwoMonths(),
+ [
+ new("T-Shirts", 1400, 10.50m),
+ new("Hats", 1100, 9.00m),
+ new("Glasses", 950, 12.00m)
+ ])
+ ];
+ }
+
+ public static DateTime GetRandomDateWithinLastTwoMonths()
+ {
+ // Get the current date and time
+ DateTime endDate = DateTime.UtcNow;
+
+ // Calculate the start date, which is two months before the current date
+ DateTime startDate = endDate.AddMonths(-2);
+
+ // Generate a random number of days between 0 and the total number of days in the range
+ int totalDays = (endDate - startDate).Days;
+ int randomDays = Random.Shared.Next(0, totalDays + 1); // +1 to include the end date
+
+ // Return the random date
+ return startDate.AddDays(randomDays);
+ }
+
+ [Description("Retrieves invoices for the specified company and optionally within the specified time range")]
+ public IEnumerable QueryInvoices(string companyName, DateTime? startDate = null, DateTime? endDate = null)
+ {
+ var query = this._invoices.Where(i => i.CompanyName.Equals(companyName, StringComparison.OrdinalIgnoreCase));
+
+ if (startDate.HasValue)
+ {
+ query = query.Where(i => i.InvoiceDate >= startDate.Value);
+ }
+
+ if (endDate.HasValue)
+ {
+ query = query.Where(i => i.InvoiceDate <= endDate.Value);
+ }
+
+ return query.ToList();
+ }
+
+ [Description("Retrieves invoice using the transaction id")]
+ public IEnumerable QueryByTransactionId(string transactionId)
+ {
+ var query = this._invoices.Where(i => i.TransactionId.Equals(transactionId, StringComparison.OrdinalIgnoreCase));
+
+ return query.ToList();
+ }
+
+ [Description("Retrieves invoice using the invoice id")]
+ public IEnumerable QueryByInvoiceId(string invoiceId)
+ {
+ var query = this._invoices.Where(i => i.InvoiceId.Equals(invoiceId, StringComparison.OrdinalIgnoreCase));
+
+ return query.ToList();
+ }
+}
diff --git a/dotnet/samples/A2AClientServer/A2AServer/Program.cs b/dotnet/samples/A2AClientServer/A2AServer/Program.cs
new file mode 100644
index 0000000..bd344c4
--- /dev/null
+++ b/dotnet/samples/A2AClientServer/A2AServer/Program.cs
@@ -0,0 +1,113 @@
+// Copyright (c) Microsoft. All rights reserved.
+using A2A;
+using A2A.AspNetCore;
+using A2AServer;
+using Microsoft.Agents.AI;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+
+string agentId = string.Empty;
+string agentType = string.Empty;
+
+for (var i = 0; i < args.Length; i++)
+{
+ if (args[i].StartsWith("--agentId", StringComparison.InvariantCultureIgnoreCase) && i + 1 < args.Length)
+ {
+ agentId = args[++i];
+ }
+ else if (args[i].StartsWith("--agentType", StringComparison.InvariantCultureIgnoreCase) && i + 1 < args.Length)
+ {
+ agentType = args[++i];
+ }
+}
+
+var builder = WebApplication.CreateBuilder(args);
+builder.Services.AddHttpClient().AddLogging();
+var app = builder.Build();
+
+var httpClient = app.Services.GetRequiredService().CreateClient();
+var logger = app.Logger;
+
+IConfigurationRoot configuration = new ConfigurationBuilder()
+ .AddEnvironmentVariables()
+ .AddUserSecrets()
+ .Build();
+
+string? apiKey = configuration["OPENAI_API_KEY"];
+string model = configuration["OPENAI_MODEL"] ?? "gpt-4o-mini";
+string? endpoint = configuration["AZURE_FOUNDRY_PROJECT_ENDPOINT"];
+
+var invoiceQueryPlugin = new InvoiceQuery();
+IList tools =
+ [
+ AIFunctionFactory.Create(invoiceQueryPlugin.QueryInvoices),
+ AIFunctionFactory.Create(invoiceQueryPlugin.QueryByTransactionId),
+ AIFunctionFactory.Create(invoiceQueryPlugin.QueryByInvoiceId)
+ ];
+
+AIAgent hostA2AAgent;
+AgentCard hostA2AAgentCard;
+
+if (!string.IsNullOrEmpty(endpoint) && !string.IsNullOrEmpty(agentId))
+{
+ (hostA2AAgent, hostA2AAgentCard) = agentType.ToUpperInvariant() switch
+ {
+ "INVOICE" => await HostAgentFactory.CreateFoundryHostAgentAsync(agentType, model, endpoint, agentId, tools),
+ "POLICY" => await HostAgentFactory.CreateFoundryHostAgentAsync(agentType, model, endpoint, agentId),
+ "LOGISTICS" => await HostAgentFactory.CreateFoundryHostAgentAsync(agentType, model, endpoint, agentId),
+ _ => throw new ArgumentException($"Unsupported agent type: {agentType}"),
+ };
+}
+else if (!string.IsNullOrEmpty(apiKey))
+{
+ (hostA2AAgent, hostA2AAgentCard) = agentType.ToUpperInvariant() switch
+ {
+ "INVOICE" => await HostAgentFactory.CreateChatCompletionHostAgentAsync(
+ agentType, model, apiKey, "InvoiceAgent",
+ """
+ You specialize in handling queries related to invoices.
+ """, tools),
+ "POLICY" => await HostAgentFactory.CreateChatCompletionHostAgentAsync(
+ agentType, model, apiKey, "PolicyAgent",
+ """
+ You specialize in handling queries related to policies and customer communications.
+
+ Always reply with exactly this text:
+
+ Policy: Short Shipment Dispute Handling Policy V2.1
+
+ Summary: "For short shipments reported by customers, first verify internal shipment records
+ (SAP) and physical logistics scan data (BigQuery). If discrepancy is confirmed and logistics data
+ shows fewer items packed than invoiced, issue a credit for the missing items. Document the
+ resolution in SAP CRM and notify the customer via email within 2 business days, referencing the
+ original invoice and the credit memo number. Use the 'Formal Credit Notification' email
+ template."
+ """),
+ "LOGISTICS" => await HostAgentFactory.CreateChatCompletionHostAgentAsync(
+ agentType, model, apiKey, "LogisticsAgent",
+ """
+ You specialize in handling queries related to logistics.
+
+ Always reply with exactly:
+
+ Shipment number: SHPMT-SAP-001
+ Item: TSHIRT-RED-L
+ Quantity: 900
+ """),
+ _ => throw new ArgumentException($"Unsupported agent type: {agentType}"),
+ };
+}
+else
+{
+ throw new ArgumentException("Either A2AServer:ApiKey or A2AServer:ConnectionString & agentId must be provided");
+}
+
+var a2aTaskManager = app.MapA2A(
+ hostA2AAgent,
+ path: "/",
+ agentCard: hostA2AAgentCard,
+ taskManager => app.MapWellKnownAgentCard(taskManager, "/"));
+
+await app.RunAsync();
diff --git a/dotnet/samples/A2AClientServer/README.md b/dotnet/samples/A2AClientServer/README.md
new file mode 100644
index 0000000..04b9968
--- /dev/null
+++ b/dotnet/samples/A2AClientServer/README.md
@@ -0,0 +1,235 @@
+# A2A Client and Server samples
+
+> **Warning**
+> The [A2A protocol](https://google.github.io/A2A/) is still under development and changing fast.
+> We will try to keep these samples updated as the protocol evolves.
+
+These samples are built with [official A2A C# SDK](https://www.nuget.org/packages/A2A) and demonstrates:
+
+1. Creating an A2A Server which makes an agent available via the A2A protocol.
+2. Creating an A2A Client with a command line interface which invokes agents using the A2A protocol.
+
+The demonstration has two components:
+
+1. `A2AServer` - You will run three instances of the server to correspond to three A2A servers each providing a single Agent i.e., the Invoice, Policy and Logistics agents.
+2. `A2AClient` - This represents a client application which will connect to the remote A2A servers using the A2A protocol so that it can use those agents when answering questions you will ask.
+
+
+
+## Configuring Environment Variables
+
+The samples can be configured to use chat completion agents or Azure AI agents.
+
+### Configuring for use with Chat Completion Agents
+
+Provide your OpenAI API key via an environment variable
+
+```powershell
+$env:OPENAI_API_KEY=""
+```
+
+Use the following commands to run each A2A server:
+
+Execute the following command to build the sample:
+
+```powershell
+cd A2AServer
+dotnet build
+```
+
+```bash
+dotnet run --urls "http://localhost:5000;https://localhost:5010" --agentType "invoice" --no-build
+```
+
+```bash
+dotnet run --urls "http://localhost:5001;https://localhost:5011" --agentType "policy" --no-build
+```
+
+```bash
+dotnet run --urls "http://localhost:5002;https://localhost:5012" --agentType "logistics" --no-build
+```
+
+### Configuring for use with Azure AI Agents
+
+You must create the agents in an Azure AI Foundry project and then provide the project endpoint and agents ids. The instructions for each agent are as follows:
+
+- Invoice Agent
+ ```
+ You specialize in handling queries related to invoices.
+ ```
+- Policy Agent
+ ```
+ You specialize in handling queries related to policies and customer communications.
+
+ Always reply with exactly this text:
+
+ Policy: Short Shipment Dispute Handling Policy V2.1
+
+ Summary: "For short shipments reported by customers, first verify internal shipment records
+ (SAP) and physical logistics scan data (BigQuery). If discrepancy is confirmed and logistics data
+ shows fewer items packed than invoiced, issue a credit for the missing items. Document the
+ resolution in SAP CRM and notify the customer via email within 2 business days, referencing the
+ original invoice and the credit memo number. Use the 'Formal Credit Notification' email
+ template."
+ ```
+- Logistics Agent
+ ```
+ You specialize in handling queries related to logistics.
+
+ Always reply with exactly:
+
+ Shipment number: SHPMT-SAP-001
+ Item: TSHIRT-RED-L
+ Quantity: 900"
+ ```
+
+```powershell
+$env:AZURE_FOUNDRY_PROJECT_ENDPOINT="https://ai-foundry-your-project.services.ai.azure.com/api/projects/ai-proj-ga-your-project" # Replace with your Foundry Project endpoint
+```
+
+Use the following commands to run each A2A server
+
+```bash
+dotnet run --urls "http://localhost:5000;https://localhost:5010" --agentId "" --agentType "invoice" --no-build
+```
+
+```bash
+dotnet run --urls "http://localhost:5001;https://localhost:5011" --agentId "" --agentType "policy" --no-build
+```
+
+```bash
+dotnet run --urls "http://localhost:5002;https://localhost:5012" --agentId "" --agentType "logistics" --no-build
+```
+
+### Testing the Agents using the Rest Client
+
+This sample contains a [.http file](https://learn.microsoft.com/aspnet/core/test/http-files?view=aspnetcore-10.0) which can be used to test the agent.
+
+1. In Visual Studio open [./A2AServer/A2AServer.http](./A2AServer/A2AServer.http)
+1. There are two sent requests for each agent, e.g., for the invoice agent:
+ 1. Query agent card for the invoice agent
+ `GET {{hostInvoice}}/.well-known/agent-card.json`
+ 1. Send a message to the invoice agent
+ ```
+ POST {{hostInvoice}}
+ Content-Type: application/json
+
+ {
+ "id": "1",
+ "jsonrpc": "2.0",
+ "method": "message/send",
+ "params": {
+ "id": "12345",
+ "message": {
+ "kind": "message",
+ "role": "user",
+ "messageId": "msg_1",
+ "parts": [
+ {
+ "kind": "text",
+ "text": "Show me all invoices for Contoso?"
+ }
+ ]
+ }
+ }
+ }
+ ```
+
+Sample output from the request to display the agent card:
+
+
+
+Sample output from the request to send a message to the agent via A2A protocol:
+
+
+
+### Testing the Agents using the A2A Inspector
+
+The A2A Inspector is a web-based tool designed to help developers inspect, debug, and validate servers that implement the Google A2A (Agent2Agent) protocol. It provides a user-friendly interface to interact with an A2A agent, view communication, and ensure specification compliance.
+
+For more information go [here](https://github.com/a2aproject/a2a-inspector).
+
+Running the [inspector with Docker](https://github.com/a2aproject/a2a-inspector?tab=readme-ov-file#option-two-run-with-docker) is the easiest way to get started.
+
+1. Navigate to the A2A Inspector in your browser: [http://127.0.0.1:8080/](http://127.0.0.1:8080/)
+1. Enter the URL of the Agent you are running e.g., [http://host.docker.internal:5000](http://host.docker.internal:5000)
+1. Connect to the agent and the agent card will be displayed and validated.
+1. Type a message and send it to the agent using A2A protocol.
+ 1. The response will be validated automatically and then displayed in the UI.
+ 1. You can select the response to view the raw json.
+
+Agent card after connecting to an agent using the A2A protocol:
+
+
+
+Sample response after sending a message to the agent via A2A protocol:
+
+
+
+Raw JSON response from an A2A agent:
+
+
+
+### Configuring Agents for the A2A Client
+
+The A2A client will connect to remote agents using the A2A protocol.
+
+By default the client will connect to the invoice, policy and logistics agents provided by the sample A2A Server.
+
+These are available at the following URL's:
+
+- Invoice Agent: http://localhost:5000/
+- Policy Agent: http://localhost:5001/
+- Logistics Agent: http://localhost:5002/
+
+If you want to change which agents are using then set the agents url as a space delimited string as follows:
+
+```powershell
+$env:A2A_AGENT_URLS="http://localhost:5000/;http://localhost:5001/;http://localhost:5002/"
+```
+
+## Run the Sample
+
+To run the sample, follow these steps:
+
+1. Run the A2A server's using the commands shown earlier
+2. Run the A2A client:
+ ```bash
+ cd A2AClient
+ dotnet run
+ ```
+3. Enter your request e.g. "Customer is disputing transaction TICKET-XYZ987 as they claim the received fewer t-shirts than ordered."
+4. The host client agent will call the remote agents, these calls will be displayed as console output. The final answer will use information from the remote agents. The sample below includes all three agents but in your case you may only see the policy and invoice agent.
+
+Sample output from the A2A client:
+
+```
+A2AClient> dotnet run
+info: HostClientAgent[0]
+ Initializing Agent Framework agent with model: gpt-4o-mini
+
+User (:q or quit to exit): Customer is disputing transaction TICKET-XYZ987 as they claim the received fewer t-shirts than ordered.
+
+Agent:
+
+Agent:
+
+Agent: The transaction details for **TICKET-XYZ987** are as follows:
+
+- **Invoice ID:** INV789
+- **Company Name:** Contoso
+- **Invoice Date:** September 4, 2025
+- **Products:**
+ - **T-Shirts:** 150 units at $10.00 each
+ - **Hats:** 200 units at $15.00 each
+ - **Glasses:** 300 units at $5.00 each
+
+To proceed with the dispute regarding the quantity of t-shirts delivered, please specify the exact quantity issue � how many t-shirts were actually received compared to the ordered amount.
+
+### Customer Service Policy for Handling Disputes
+**Short Shipment Dispute Handling Policy V2.1**
+- **Summary:** For short shipments reported by customers, first verify internal shipment records and physical logistics scan data. If a discrepancy is confirmed and the logistics data shows fewer items were packed than invoiced, a credit for the missing items will be issued.
+- **Follow-up Actions:** Document the resolution in the SAP CRM and notify the customer via email within 2 business days, referencing the original invoice and the credit memo number, using the 'Formal Credit Notification' email template.
+
+Please provide me with the information regarding the specific quantity issue so I can assist you further.
+```
diff --git a/dotnet/samples/A2AClientServer/a2a-inspector-agent-card.png b/dotnet/samples/A2AClientServer/a2a-inspector-agent-card.png
new file mode 100644
index 0000000..8385a2b
Binary files /dev/null and b/dotnet/samples/A2AClientServer/a2a-inspector-agent-card.png differ
diff --git a/dotnet/samples/A2AClientServer/a2a-inspector-raw-json-response.png b/dotnet/samples/A2AClientServer/a2a-inspector-raw-json-response.png
new file mode 100644
index 0000000..038ef34
Binary files /dev/null and b/dotnet/samples/A2AClientServer/a2a-inspector-raw-json-response.png differ
diff --git a/dotnet/samples/A2AClientServer/a2a-inspector-send-message.png b/dotnet/samples/A2AClientServer/a2a-inspector-send-message.png
new file mode 100644
index 0000000..49fa857
Binary files /dev/null and b/dotnet/samples/A2AClientServer/a2a-inspector-send-message.png differ
diff --git a/dotnet/samples/A2AClientServer/demo-architecture.png b/dotnet/samples/A2AClientServer/demo-architecture.png
new file mode 100644
index 0000000..6ae3519
Binary files /dev/null and b/dotnet/samples/A2AClientServer/demo-architecture.png differ
diff --git a/dotnet/samples/A2AClientServer/rest-client-agent-card.png b/dotnet/samples/A2AClientServer/rest-client-agent-card.png
new file mode 100644
index 0000000..4465148
Binary files /dev/null and b/dotnet/samples/A2AClientServer/rest-client-agent-card.png differ
diff --git a/dotnet/samples/A2AClientServer/rest-client-send-message.png b/dotnet/samples/A2AClientServer/rest-client-send-message.png
new file mode 100644
index 0000000..fe65f5c
Binary files /dev/null and b/dotnet/samples/A2AClientServer/rest-client-send-message.png differ
diff --git a/dotnet/samples/AGUIClientServer/AGUIClient/AGUIClient.csproj b/dotnet/samples/AGUIClientServer/AGUIClient/AGUIClient.csproj
new file mode 100644
index 0000000..7d80fa7
--- /dev/null
+++ b/dotnet/samples/AGUIClientServer/AGUIClient/AGUIClient.csproj
@@ -0,0 +1,21 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+ a8b2e9f0-1ea3-4f18-9d41-42d1a6f8fe10
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/AGUIClientServer/AGUIClient/AGUIClientSerializerContext.cs b/dotnet/samples/AGUIClientServer/AGUIClient/AGUIClientSerializerContext.cs
new file mode 100644
index 0000000..1cc4fb8
--- /dev/null
+++ b/dotnet/samples/AGUIClientServer/AGUIClient/AGUIClientSerializerContext.cs
@@ -0,0 +1,12 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// This sample demonstrates how to use the AG-UI client to connect to a remote AG-UI server
+// and display streaming updates including conversation/response metadata, text content, and errors.
+
+using System.Text.Json.Serialization;
+
+namespace AGUIClient;
+
+[JsonSerializable(typeof(SensorRequest))]
+[JsonSerializable(typeof(SensorResponse))]
+internal sealed partial class AGUIClientSerializerContext : JsonSerializerContext;
diff --git a/dotnet/samples/AGUIClientServer/AGUIClient/Program.cs b/dotnet/samples/AGUIClientServer/AGUIClient/Program.cs
new file mode 100644
index 0000000..1906b4d
--- /dev/null
+++ b/dotnet/samples/AGUIClientServer/AGUIClient/Program.cs
@@ -0,0 +1,213 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// This sample demonstrates how to use the AG-UI client to connect to a remote AG-UI server
+// and display streaming updates including conversation/response metadata, text content, and errors.
+
+using System.CommandLine;
+using System.ComponentModel;
+using System.Reflection;
+using System.Text;
+using Microsoft.Agents.AI;
+using Microsoft.Agents.AI.AGUI;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+
+namespace AGUIClient;
+
+public static class Program
+{
+ public static async Task Main(string[] args)
+ {
+ // Create root command with options
+ RootCommand rootCommand = new("AGUIClient");
+ rootCommand.SetAction((_, ct) => HandleCommandsAsync(ct));
+
+ // Run the command
+ return await rootCommand.Parse(args).InvokeAsync();
+ }
+
+ private static async Task HandleCommandsAsync(CancellationToken cancellationToken)
+ {
+ // Set up the logging
+ using ILoggerFactory loggerFactory = LoggerFactory.Create(builder =>
+ {
+ builder.AddConsole();
+ builder.SetMinimumLevel(LogLevel.Information);
+ });
+ ILogger logger = loggerFactory.CreateLogger("AGUIClient");
+
+ // Retrieve configuration settings
+ IConfigurationRoot configRoot = new ConfigurationBuilder()
+ .AddEnvironmentVariables()
+ .AddUserSecrets(Assembly.GetExecutingAssembly())
+ .Build();
+
+ string serverUrl = configRoot["AGUI_SERVER_URL"] ?? "http://localhost:5100";
+
+ logger.LogInformation("Connecting to AG-UI server at: {ServerUrl}", serverUrl);
+
+ // Create the AG-UI client agent
+ using HttpClient httpClient = new()
+ {
+ Timeout = TimeSpan.FromSeconds(60)
+ };
+
+ var changeBackground = AIFunctionFactory.Create(
+ () =>
+ {
+ Console.ForegroundColor = ConsoleColor.DarkBlue;
+ Console.WriteLine("Changing color to blue");
+ },
+ name: "change_background_color",
+ description: "Change the console background color to dark blue."
+ );
+
+ var readClientClimateSensors = AIFunctionFactory.Create(
+ ([Description("The sensors measurements to include in the response")] SensorRequest request) =>
+ {
+ return new SensorResponse()
+ {
+ Temperature = 22.5,
+ Humidity = 45.0,
+ AirQualityIndex = 75
+ };
+ },
+ name: "read_client_climate_sensors",
+ description: "Reads the climate sensor data from the client device.",
+ serializerOptions: AGUIClientSerializerContext.Default.Options
+ );
+
+ var chatClient = new AGUIChatClient(
+ httpClient,
+ serverUrl,
+ jsonSerializerOptions: AGUIClientSerializerContext.Default.Options);
+
+ AIAgent agent = chatClient.AsAIAgent(
+ name: "agui-client",
+ description: "AG-UI Client Agent",
+ tools: [changeBackground, readClientClimateSensors]);
+
+ AgentThread thread = await agent.GetNewThreadAsync(cancellationToken);
+ List messages = [new(ChatRole.System, "You are a helpful assistant.")];
+ try
+ {
+ while (true)
+ {
+ // Get user message
+ Console.Write("\nUser (:q or quit to exit): ");
+ string? message = Console.ReadLine();
+ if (string.IsNullOrWhiteSpace(message))
+ {
+ Console.WriteLine("Request cannot be empty.");
+ continue;
+ }
+
+ if (message is ":q" or "quit")
+ {
+ break;
+ }
+
+ messages.Add(new(ChatRole.User, message));
+
+ // Call RunStreamingAsync to get streaming updates
+ bool isFirstUpdate = true;
+ string? threadId = null;
+ var updates = new List();
+ 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)
+ {
+ threadId = chatUpdate.ConversationId;
+ }
+
+ // Display run started information from the first update
+ if (isFirstUpdate && threadId != null && update.ResponseId != null)
+ {
+ Console.ForegroundColor = ConsoleColor.Yellow;
+ Console.WriteLine($"\n[Run Started - Thread: {threadId}, Run: {update.ResponseId}]");
+ Console.ResetColor();
+ isFirstUpdate = false;
+ }
+
+ // Display different content types with appropriate formatting
+ foreach (AIContent content in update.Contents)
+ {
+ switch (content)
+ {
+ case TextContent textContent:
+ Console.ForegroundColor = ConsoleColor.Cyan;
+ Console.Write(textContent.Text);
+ Console.ResetColor();
+ break;
+
+ case FunctionCallContent functionCallContent:
+ Console.ForegroundColor = ConsoleColor.Green;
+ Console.WriteLine($"\n[Function Call - Name: {functionCallContent.Name}, Arguments: {PrintArguments(functionCallContent.Arguments)}]");
+ Console.ResetColor();
+ break;
+
+ case FunctionResultContent functionResultContent:
+ Console.ForegroundColor = ConsoleColor.Magenta;
+ if (functionResultContent.Exception != null)
+ {
+ Console.WriteLine($"\n[Function Result - Exception: {functionResultContent.Exception}]");
+ }
+ else
+ {
+ Console.WriteLine($"\n[Function Result - Result: {functionResultContent.Result}]");
+ }
+ Console.ResetColor();
+ break;
+
+ case ErrorContent errorContent:
+ Console.ForegroundColor = ConsoleColor.Red;
+ string code = errorContent.AdditionalProperties?["Code"] as string ?? "Unknown";
+ Console.WriteLine($"\n[Error - Code: {code}, Message: {errorContent.Message}]");
+ Console.ResetColor();
+ break;
+ }
+ }
+ }
+ if (updates.Count > 0 && !updates[^1].Contents.Any(c => c is TextContent))
+ {
+ var lastUpdate = updates[^1];
+ Console.ForegroundColor = ConsoleColor.Yellow;
+ Console.WriteLine();
+ Console.WriteLine($"[Run Ended - Thread: {threadId}, Run: {lastUpdate.ResponseId}]");
+ Console.ResetColor();
+ }
+ messages.Clear();
+ Console.WriteLine();
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ logger.LogInformation("AGUIClient operation was canceled.");
+ }
+ catch (Exception ex) when (ex is not OutOfMemoryException and not StackOverflowException and not ThreadAbortException and not AccessViolationException)
+ {
+ logger.LogError(ex, "An error occurred while running the AGUIClient");
+ return;
+ }
+ }
+
+ private static string PrintArguments(IDictionary? arguments)
+ {
+ if (arguments == null)
+ {
+ return "";
+ }
+ var builder = new StringBuilder().AppendLine();
+ foreach (var kvp in arguments)
+ {
+ builder
+ .AppendLine($" Name: {kvp.Key}")
+ .AppendLine($" Value: {kvp.Value}");
+ }
+ return builder.ToString();
+ }
+}
diff --git a/dotnet/samples/AGUIClientServer/AGUIClient/README.md b/dotnet/samples/AGUIClientServer/AGUIClient/README.md
new file mode 100644
index 0000000..f0f6052
--- /dev/null
+++ b/dotnet/samples/AGUIClientServer/AGUIClient/README.md
@@ -0,0 +1,34 @@
+# AG-UI Client
+
+This is a console application that demonstrates how to connect to an AG-UI server and interact with remote agents using the AG-UI protocol.
+
+## Features
+
+- Connects to an AG-UI server endpoint
+- Displays streaming updates with color-coded output:
+ - **Yellow**: Run started notifications
+ - **Cyan**: Agent text responses (streamed)
+ - **Green**: Run finished notifications
+ - **Red**: Error messages (if any)
+- Interactive prompt loop for sending messages
+
+## Configuration
+
+Set the following environment variable to specify the AG-UI server URL:
+
+```powershell
+$env:AGUI_SERVER_URL="http://localhost:5100"
+```
+
+If not set, the default is `http://localhost:5100`.
+
+## Running the Client
+
+1. Make sure the AG-UI server is running
+2. Run the client:
+ ```bash
+ cd AGUIClient
+ dotnet run
+ ```
+3. Enter your messages and observe the streaming updates
+4. Type `:q` or `quit` to exit
diff --git a/dotnet/samples/AGUIClientServer/AGUIClient/SensorRequest.cs b/dotnet/samples/AGUIClientServer/AGUIClient/SensorRequest.cs
new file mode 100644
index 0000000..76e6efa
--- /dev/null
+++ b/dotnet/samples/AGUIClientServer/AGUIClient/SensorRequest.cs
@@ -0,0 +1,13 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// This sample demonstrates how to use the AG-UI client to connect to a remote AG-UI server
+// and display streaming updates including conversation/response metadata, text content, and errors.
+
+namespace AGUIClient;
+
+internal sealed class SensorRequest
+{
+ public bool IncludeTemperature { get; set; } = true;
+ public bool IncludeHumidity { get; set; } = true;
+ public bool IncludeAirQualityIndex { get; set; } = true;
+}
diff --git a/dotnet/samples/AGUIClientServer/AGUIClient/SensorResponse.cs b/dotnet/samples/AGUIClientServer/AGUIClient/SensorResponse.cs
new file mode 100644
index 0000000..09ade6a
--- /dev/null
+++ b/dotnet/samples/AGUIClientServer/AGUIClient/SensorResponse.cs
@@ -0,0 +1,13 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// This sample demonstrates how to use the AG-UI client to connect to a remote AG-UI server
+// and display streaming updates including conversation/response metadata, text content, and errors.
+
+namespace AGUIClient;
+
+internal sealed class SensorResponse
+{
+ public double Temperature { get; set; }
+ public double Humidity { get; set; }
+ public int AirQualityIndex { get; set; }
+}
diff --git a/dotnet/samples/AGUIClientServer/AGUIDojoServer/AGUIDojoServer.csproj b/dotnet/samples/AGUIClientServer/AGUIDojoServer/AGUIDojoServer.csproj
new file mode 100644
index 0000000..cea8eff
--- /dev/null
+++ b/dotnet/samples/AGUIClientServer/AGUIDojoServer/AGUIDojoServer.csproj
@@ -0,0 +1,22 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+ b9c3f1e1-2fb4-5g29-0e52-53e2b7g9gf21
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/AGUIClientServer/AGUIDojoServer/AGUIDojoServerSerializerContext.cs b/dotnet/samples/AGUIClientServer/AGUIDojoServer/AGUIDojoServerSerializerContext.cs
new file mode 100644
index 0000000..c60db0e
--- /dev/null
+++ b/dotnet/samples/AGUIClientServer/AGUIDojoServer/AGUIDojoServerSerializerContext.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+using AGUIDojoServer.AgenticUI;
+using AGUIDojoServer.BackendToolRendering;
+using AGUIDojoServer.PredictiveStateUpdates;
+using AGUIDojoServer.SharedState;
+
+namespace AGUIDojoServer;
+
+[JsonSerializable(typeof(WeatherInfo))]
+[JsonSerializable(typeof(Recipe))]
+[JsonSerializable(typeof(Ingredient))]
+[JsonSerializable(typeof(RecipeResponse))]
+[JsonSerializable(typeof(Plan))]
+[JsonSerializable(typeof(Step))]
+[JsonSerializable(typeof(StepStatus))]
+[JsonSerializable(typeof(StepStatus?))]
+[JsonSerializable(typeof(JsonPatchOperation))]
+[JsonSerializable(typeof(List))]
+[JsonSerializable(typeof(List))]
+[JsonSerializable(typeof(DocumentState))]
+internal sealed partial class AGUIDojoServerSerializerContext : JsonSerializerContext;
diff --git a/dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/AgenticPlanningTools.cs b/dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/AgenticPlanningTools.cs
new file mode 100644
index 0000000..98fe96b
--- /dev/null
+++ b/dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/AgenticPlanningTools.cs
@@ -0,0 +1,52 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.ComponentModel;
+
+namespace AGUIDojoServer.AgenticUI;
+
+internal static class AgenticPlanningTools
+{
+ [Description("Create a plan with multiple steps.")]
+ public static Plan CreatePlan([Description("List of step descriptions to create the plan.")] List steps)
+ {
+ return new Plan
+ {
+ Steps = [.. steps.Select(s => new Step { Description = s, Status = StepStatus.Pending })]
+ };
+ }
+
+ [Description("Update a step in the plan with new description or status.")]
+ public static async Task> UpdatePlanStepAsync(
+ [Description("The index of the step to update.")] int index,
+ [Description("The new description for the step (optional).")] string? description = null,
+ [Description("The new status for the step (optional).")] StepStatus? status = null)
+ {
+ var changes = new List();
+
+ if (description is not null)
+ {
+ changes.Add(new JsonPatchOperation
+ {
+ Op = "replace",
+ Path = $"/steps/{index}/description",
+ Value = description
+ });
+ }
+
+ if (status.HasValue)
+ {
+ // Status must be lowercase to match AG-UI frontend expectations: "pending" or "completed"
+ string statusValue = status.Value == StepStatus.Pending ? "pending" : "completed";
+ changes.Add(new JsonPatchOperation
+ {
+ Op = "replace",
+ Path = $"/steps/{index}/status",
+ Value = statusValue
+ });
+ }
+
+ await Task.Delay(1000);
+
+ return changes;
+ }
+}
diff --git a/dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/AgenticUIAgent.cs b/dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/AgenticUIAgent.cs
new file mode 100644
index 0000000..da08248
--- /dev/null
+++ b/dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/AgenticUIAgent.cs
@@ -0,0 +1,88 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+using System.Text.Json;
+using Microsoft.Agents.AI;
+using Microsoft.Extensions.AI;
+
+namespace AGUIDojoServer.AgenticUI;
+
+[SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Instantiated by ChatClientAgentFactory.CreateAgenticUI")]
+internal sealed class AgenticUIAgent : DelegatingAIAgent
+{
+ private readonly JsonSerializerOptions _jsonSerializerOptions;
+
+ public AgenticUIAgent(AIAgent innerAgent, JsonSerializerOptions jsonSerializerOptions)
+ : base(innerAgent)
+ {
+ this._jsonSerializerOptions = jsonSerializerOptions;
+ }
+
+ protected override Task RunCoreAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
+ {
+ return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken).ToAgentResponseAsync(cancellationToken);
+ }
+
+ protected override async IAsyncEnumerable RunCoreStreamingAsync(
+ IEnumerable messages,
+ AgentThread? thread = null,
+ AgentRunOptions? options = null,
+ [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ // Track function calls that should trigger state events
+ var trackedFunctionCalls = new Dictionary();
+
+ 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 stateEventsToEmit = new();
+ foreach (var content in update.Contents)
+ {
+ if (content is FunctionCallContent callContent)
+ {
+ if (callContent.Name == "create_plan" || callContent.Name == "update_plan_step")
+ {
+ trackedFunctionCalls[callContent.CallId] = callContent;
+ break;
+ }
+ }
+ else if (content is FunctionResultContent resultContent)
+ {
+ // Check if this result matches a tracked function call
+ if (trackedFunctionCalls.TryGetValue(resultContent.CallId, out var matchedCall))
+ {
+ var bytes = JsonSerializer.SerializeToUtf8Bytes((JsonElement)resultContent.Result!, this._jsonSerializerOptions);
+
+ // Determine event type based on the function name
+ if (matchedCall.Name == "create_plan")
+ {
+ stateEventsToEmit.Add(new DataContent(bytes, "application/json"));
+ }
+ else if (matchedCall.Name == "update_plan_step")
+ {
+ stateEventsToEmit.Add(new DataContent(bytes, "application/json-patch+json"));
+ }
+ }
+ }
+ }
+
+ yield return update;
+
+ yield return new AgentResponseUpdate(
+ new ChatResponseUpdate(role: ChatRole.System, stateEventsToEmit)
+ {
+ MessageId = "delta_" + Guid.NewGuid().ToString("N"),
+ CreatedAt = update.CreatedAt,
+ ResponseId = update.ResponseId,
+ AuthorName = update.AuthorName,
+ Role = update.Role,
+ ContinuationToken = update.ContinuationToken,
+ AdditionalProperties = update.AdditionalProperties,
+ })
+ {
+ AgentId = update.AgentId
+ };
+ }
+ }
+}
diff --git a/dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/JsonPatchOperation.cs b/dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/JsonPatchOperation.cs
new file mode 100644
index 0000000..1cd8f5d
--- /dev/null
+++ b/dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/JsonPatchOperation.cs
@@ -0,0 +1,20 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+namespace AGUIDojoServer.AgenticUI;
+
+internal sealed class JsonPatchOperation
+{
+ [JsonPropertyName("op")]
+ public required string Op { get; set; }
+
+ [JsonPropertyName("path")]
+ public required string Path { get; set; }
+
+ [JsonPropertyName("value")]
+ public object? Value { get; set; }
+
+ [JsonPropertyName("from")]
+ public string? From { get; set; }
+}
diff --git a/dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/Plan.cs b/dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/Plan.cs
new file mode 100644
index 0000000..a8ffcc6
--- /dev/null
+++ b/dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/Plan.cs
@@ -0,0 +1,11 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+namespace AGUIDojoServer.AgenticUI;
+
+internal sealed class Plan
+{
+ [JsonPropertyName("steps")]
+ public List Steps { get; set; } = [];
+}
diff --git a/dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/Step.cs b/dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/Step.cs
new file mode 100644
index 0000000..26bc986
--- /dev/null
+++ b/dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/Step.cs
@@ -0,0 +1,14 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+namespace AGUIDojoServer.AgenticUI;
+
+internal sealed class Step
+{
+ [JsonPropertyName("description")]
+ public required string Description { get; set; }
+
+ [JsonPropertyName("status")]
+ public StepStatus Status { get; set; } = StepStatus.Pending;
+}
diff --git a/dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/StepStatus.cs b/dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/StepStatus.cs
new file mode 100644
index 0000000..f88d71b
--- /dev/null
+++ b/dotnet/samples/AGUIClientServer/AGUIDojoServer/AgenticUI/StepStatus.cs
@@ -0,0 +1,12 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+namespace AGUIDojoServer.AgenticUI;
+
+[JsonConverter(typeof(JsonStringEnumConverter))]
+internal enum StepStatus
+{
+ Pending,
+ Completed
+}
diff --git a/dotnet/samples/AGUIClientServer/AGUIDojoServer/BackendToolRendering/WeatherInfo.cs b/dotnet/samples/AGUIClientServer/AGUIDojoServer/BackendToolRendering/WeatherInfo.cs
new file mode 100644
index 0000000..d6e3be9
--- /dev/null
+++ b/dotnet/samples/AGUIClientServer/AGUIDojoServer/BackendToolRendering/WeatherInfo.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+namespace AGUIDojoServer.BackendToolRendering;
+
+internal sealed class WeatherInfo
+{
+ [JsonPropertyName("temperature")]
+ public int Temperature { get; init; }
+
+ [JsonPropertyName("conditions")]
+ public string Conditions { get; init; } = string.Empty;
+
+ [JsonPropertyName("humidity")]
+ public int Humidity { get; init; }
+
+ [JsonPropertyName("wind_speed")]
+ public int WindSpeed { get; init; }
+
+ [JsonPropertyName("feelsLike")]
+ public int FeelsLike { get; init; }
+}
diff --git a/dotnet/samples/AGUIClientServer/AGUIDojoServer/ChatClientAgentFactory.cs b/dotnet/samples/AGUIClientServer/AGUIDojoServer/ChatClientAgentFactory.cs
new file mode 100644
index 0000000..d14755d
--- /dev/null
+++ b/dotnet/samples/AGUIClientServer/AGUIDojoServer/ChatClientAgentFactory.cs
@@ -0,0 +1,180 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.ComponentModel;
+using System.Text.Json;
+using AGUIDojoServer.AgenticUI;
+using AGUIDojoServer.BackendToolRendering;
+using AGUIDojoServer.PredictiveStateUpdates;
+using AGUIDojoServer.SharedState;
+using Azure.AI.OpenAI;
+using Azure.Identity;
+using Microsoft.Agents.AI;
+using Microsoft.Extensions.AI;
+using ChatClient = OpenAI.Chat.ChatClient;
+
+namespace AGUIDojoServer;
+
+internal static class ChatClientAgentFactory
+{
+ private static AzureOpenAIClient? s_azureOpenAIClient;
+ private static string? s_deploymentName;
+
+ public static void Initialize(IConfiguration configuration)
+ {
+ string endpoint = configuration["AZURE_OPENAI_ENDPOINT"] ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
+ s_deploymentName = configuration["AZURE_OPENAI_DEPLOYMENT_NAME"] ?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set.");
+
+ s_azureOpenAIClient = new AzureOpenAIClient(
+ new Uri(endpoint),
+ new DefaultAzureCredential());
+ }
+
+ public static ChatClientAgent CreateAgenticChat()
+ {
+ ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!);
+
+ return chatClient.AsIChatClient().AsAIAgent(
+ name: "AgenticChat",
+ description: "A simple chat agent using Azure OpenAI");
+ }
+
+ public static ChatClientAgent CreateBackendToolRendering()
+ {
+ ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!);
+
+ return chatClient.AsIChatClient().AsAIAgent(
+ name: "BackendToolRenderer",
+ description: "An agent that can render backend tools using Azure OpenAI",
+ tools: [AIFunctionFactory.Create(
+ GetWeather,
+ name: "get_weather",
+ description: "Get the weather for a given location.",
+ AGUIDojoServerSerializerContext.Default.Options)]);
+ }
+
+ public static ChatClientAgent CreateHumanInTheLoop()
+ {
+ ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!);
+
+ return chatClient.AsIChatClient().AsAIAgent(
+ name: "HumanInTheLoopAgent",
+ description: "An agent that involves human feedback in its decision-making process using Azure OpenAI");
+ }
+
+ public static ChatClientAgent CreateToolBasedGenerativeUI()
+ {
+ ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!);
+
+ return chatClient.AsIChatClient().AsAIAgent(
+ name: "ToolBasedGenerativeUIAgent",
+ description: "An agent that uses tools to generate user interfaces using Azure OpenAI");
+ }
+
+ public static AIAgent CreateAgenticUI(JsonSerializerOptions options)
+ {
+ ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!);
+ var baseAgent = chatClient.AsIChatClient().AsAIAgent(new ChatClientAgentOptions
+ {
+ Name = "AgenticUIAgent",
+ Description = "An agent that generates agentic user interfaces using Azure OpenAI",
+ ChatOptions = new ChatOptions
+ {
+ Instructions = """
+ When planning use tools only, without any other messages.
+ IMPORTANT:
+ - Use the `create_plan` tool to set the initial state of the steps
+ - Use the `update_plan_step` tool to update the status of each step
+ - Do NOT repeat the plan or summarise it in a message
+ - Do NOT confirm the creation or updates in a message
+ - Do NOT ask the user for additional information or next steps
+ - Do NOT leave a plan hanging, always complete the plan via `update_plan_step` if one is ongoing.
+ - Continue calling update_plan_step until all steps are marked as completed.
+
+ Only one plan can be active at a time, so do not call the `create_plan` tool
+ again until all the steps in current plan are completed.
+ """,
+ Tools = [
+ AIFunctionFactory.Create(
+ AgenticPlanningTools.CreatePlan,
+ name: "create_plan",
+ description: "Create a plan with multiple steps.",
+ AGUIDojoServerSerializerContext.Default.Options),
+ AIFunctionFactory.Create(
+ AgenticPlanningTools.UpdatePlanStepAsync,
+ name: "update_plan_step",
+ description: "Update a step in the plan with new description or status.",
+ AGUIDojoServerSerializerContext.Default.Options)
+ ],
+ AllowMultipleToolCalls = false
+ }
+ });
+
+ return new AgenticUIAgent(baseAgent, options);
+ }
+
+ public static AIAgent CreateSharedState(JsonSerializerOptions options)
+ {
+ ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!);
+
+ var baseAgent = chatClient.AsIChatClient().AsAIAgent(
+ name: "SharedStateAgent",
+ description: "An agent that demonstrates shared state patterns using Azure OpenAI");
+
+ return new SharedStateAgent(baseAgent, options);
+ }
+
+ public static AIAgent CreatePredictiveStateUpdates(JsonSerializerOptions options)
+ {
+ ChatClient chatClient = s_azureOpenAIClient!.GetChatClient(s_deploymentName!);
+
+ var baseAgent = chatClient.AsIChatClient().AsAIAgent(new ChatClientAgentOptions
+ {
+ Name = "PredictiveStateUpdatesAgent",
+ Description = "An agent that demonstrates predictive state updates using Azure OpenAI",
+ ChatOptions = new ChatOptions
+ {
+ Instructions = """
+ You are a document editor assistant. When asked to write or edit content:
+
+ IMPORTANT:
+ - Use the `write_document` tool with the full document text in Markdown format
+ - Format the document extensively so it's easy to read
+ - You can use all kinds of markdown (headings, lists, bold, etc.)
+ - However, do NOT use italic or strike-through formatting
+ - You MUST write the full document, even when changing only a few words
+ - When making edits to the document, try to make them minimal - do not change every word
+ - Keep stories SHORT!
+ - After you are done writing the document you MUST call a confirm_changes tool after you call write_document
+
+ After the user confirms the changes, provide a brief summary of what you wrote.
+ """,
+ Tools = [
+ AIFunctionFactory.Create(
+ WriteDocument,
+ name: "write_document",
+ description: "Write a document. Use markdown formatting to format the document.",
+ AGUIDojoServerSerializerContext.Default.Options)
+ ]
+ }
+ });
+
+ return new PredictiveStateUpdatesAgent(baseAgent, options);
+ }
+
+ [Description("Get the weather for a given location.")]
+ private static WeatherInfo GetWeather([Description("The location to get the weather for.")] string location) => new()
+ {
+ Temperature = 20,
+ Conditions = "sunny",
+ Humidity = 50,
+ WindSpeed = 10,
+ FeelsLike = 25
+ };
+
+ [Description("Write a document in markdown format.")]
+ private static string WriteDocument([Description("The document content to write.")] string document)
+ {
+ // Simply return success - the document is tracked via state updates
+ return "Document written successfully";
+ }
+}
diff --git a/dotnet/samples/AGUIClientServer/AGUIDojoServer/PredictiveStateUpdates/DocumentState.cs b/dotnet/samples/AGUIClientServer/AGUIDojoServer/PredictiveStateUpdates/DocumentState.cs
new file mode 100644
index 0000000..ad053fe
--- /dev/null
+++ b/dotnet/samples/AGUIClientServer/AGUIDojoServer/PredictiveStateUpdates/DocumentState.cs
@@ -0,0 +1,11 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+namespace AGUIDojoServer.PredictiveStateUpdates;
+
+internal sealed class DocumentState
+{
+ [JsonPropertyName("document")]
+ public string Document { get; set; } = string.Empty;
+}
diff --git a/dotnet/samples/AGUIClientServer/AGUIDojoServer/PredictiveStateUpdates/PredictiveStateUpdatesAgent.cs b/dotnet/samples/AGUIClientServer/AGUIDojoServer/PredictiveStateUpdates/PredictiveStateUpdatesAgent.cs
new file mode 100644
index 0000000..2e994d8
--- /dev/null
+++ b/dotnet/samples/AGUIClientServer/AGUIDojoServer/PredictiveStateUpdates/PredictiveStateUpdatesAgent.cs
@@ -0,0 +1,104 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+using System.Text.Json;
+using Microsoft.Agents.AI;
+using Microsoft.Extensions.AI;
+
+namespace AGUIDojoServer.PredictiveStateUpdates;
+
+[SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Instantiated by ChatClientAgentFactory.CreatePredictiveStateUpdates")]
+internal sealed class PredictiveStateUpdatesAgent : DelegatingAIAgent
+{
+ private readonly JsonSerializerOptions _jsonSerializerOptions;
+ private const int ChunkSize = 10; // Characters per chunk for streaming effect
+
+ public PredictiveStateUpdatesAgent(AIAgent innerAgent, JsonSerializerOptions jsonSerializerOptions)
+ : base(innerAgent)
+ {
+ this._jsonSerializerOptions = jsonSerializerOptions;
+ }
+
+ protected override Task RunCoreAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
+ {
+ return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken).ToAgentResponseAsync(cancellationToken);
+ }
+
+ protected override async IAsyncEnumerable RunCoreStreamingAsync(
+ IEnumerable messages,
+ 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, thread, options, cancellationToken).ConfigureAwait(false))
+ {
+ // Check if we're seeing a write_document tool call and emit predictive state
+ bool hasToolCall = false;
+ string? documentContent = null;
+
+ foreach (var content in update.Contents)
+ {
+ if (content is FunctionCallContent callContent && callContent.Name == "write_document")
+ {
+ hasToolCall = true;
+ // Try to extract the document argument directly from the dictionary
+ if (callContent.Arguments?.TryGetValue("document", out var documentValue) == true)
+ {
+ documentContent = documentValue?.ToString();
+ }
+ }
+ }
+
+ // Always yield the original update first
+ yield return update;
+
+ // If we got a complete tool call with document content, "fake" stream it in chunks
+ if (hasToolCall && documentContent != null && documentContent != lastEmittedDocument)
+ {
+ // Chunk the document content and emit progressive state updates
+ int startIndex = 0;
+ if (lastEmittedDocument != null && documentContent.StartsWith(lastEmittedDocument, StringComparison.Ordinal))
+ {
+ // Only stream the new portion that was added
+ startIndex = lastEmittedDocument.Length;
+ }
+
+ // Stream the document in chunks
+ for (int i = startIndex; i < documentContent.Length; i += ChunkSize)
+ {
+ int length = Math.Min(ChunkSize, documentContent.Length - i);
+ string chunk = documentContent.Substring(0, i + length);
+
+ // Prepare predictive state update as DataContent
+ var stateUpdate = new DocumentState { Document = chunk };
+ byte[] stateBytes = JsonSerializer.SerializeToUtf8Bytes(
+ stateUpdate,
+ this._jsonSerializerOptions.GetTypeInfo(typeof(DocumentState)));
+
+ yield return new AgentResponseUpdate(
+ new ChatResponseUpdate(role: ChatRole.Assistant, [new DataContent(stateBytes, "application/json")])
+ {
+ MessageId = "snapshot" + Guid.NewGuid().ToString("N"),
+ CreatedAt = update.CreatedAt,
+ ResponseId = update.ResponseId,
+ AdditionalProperties = update.AdditionalProperties,
+ AuthorName = update.AuthorName,
+ ContinuationToken = update.ContinuationToken,
+ })
+ {
+ AgentId = update.AgentId
+ };
+
+ // Small delay to simulate streaming
+ await Task.Delay(50, cancellationToken).ConfigureAwait(false);
+ }
+
+ lastEmittedDocument = documentContent;
+ }
+ }
+ }
+}
diff --git a/dotnet/samples/AGUIClientServer/AGUIDojoServer/Program.cs b/dotnet/samples/AGUIClientServer/AGUIDojoServer/Program.cs
new file mode 100644
index 0000000..e3b0020
--- /dev/null
+++ b/dotnet/samples/AGUIClientServer/AGUIDojoServer/Program.cs
@@ -0,0 +1,47 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using AGUIDojoServer;
+using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore;
+using Microsoft.AspNetCore.HttpLogging;
+using Microsoft.Extensions.Options;
+
+WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
+
+builder.Services.AddHttpLogging(logging =>
+{
+ logging.LoggingFields = HttpLoggingFields.RequestPropertiesAndHeaders | HttpLoggingFields.RequestBody
+ | HttpLoggingFields.ResponsePropertiesAndHeaders | HttpLoggingFields.ResponseBody;
+ logging.RequestBodyLogLimit = int.MaxValue;
+ logging.ResponseBodyLogLimit = int.MaxValue;
+});
+
+builder.Services.AddHttpClient().AddLogging();
+builder.Services.ConfigureHttpJsonOptions(options => options.SerializerOptions.TypeInfoResolverChain.Add(AGUIDojoServerSerializerContext.Default));
+builder.Services.AddAGUI();
+
+WebApplication app = builder.Build();
+
+app.UseHttpLogging();
+
+// Initialize the factory
+ChatClientAgentFactory.Initialize(app.Configuration);
+
+// Map the AG-UI agent endpoints for different scenarios
+app.MapAGUI("/agentic_chat", ChatClientAgentFactory.CreateAgenticChat());
+
+app.MapAGUI("/backend_tool_rendering", ChatClientAgentFactory.CreateBackendToolRendering());
+
+app.MapAGUI("/human_in_the_loop", ChatClientAgentFactory.CreateHumanInTheLoop());
+
+app.MapAGUI("/tool_based_generative_ui", ChatClientAgentFactory.CreateToolBasedGenerativeUI());
+
+var jsonOptions = app.Services.GetRequiredService>();
+app.MapAGUI("/agentic_generative_ui", ChatClientAgentFactory.CreateAgenticUI(jsonOptions.Value.SerializerOptions));
+
+app.MapAGUI("/shared_state", ChatClientAgentFactory.CreateSharedState(jsonOptions.Value.SerializerOptions));
+
+app.MapAGUI("/predictive_state_updates", ChatClientAgentFactory.CreatePredictiveStateUpdates(jsonOptions.Value.SerializerOptions));
+
+await app.RunAsync();
+
+public partial class Program;
diff --git a/dotnet/samples/AGUIClientServer/AGUIDojoServer/Properties/launchSettings.json b/dotnet/samples/AGUIClientServer/AGUIDojoServer/Properties/launchSettings.json
new file mode 100644
index 0000000..d1c2dbf
--- /dev/null
+++ b/dotnet/samples/AGUIClientServer/AGUIDojoServer/Properties/launchSettings.json
@@ -0,0 +1,12 @@
+{
+ "profiles": {
+ "AGUIDojoServer": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "applicationUrl": "http://localhost:5018"
+ }
+ }
+}
\ No newline at end of file
diff --git a/dotnet/samples/AGUIClientServer/AGUIDojoServer/SharedState/Ingredient.cs b/dotnet/samples/AGUIClientServer/AGUIDojoServer/SharedState/Ingredient.cs
new file mode 100644
index 0000000..d56d88d
--- /dev/null
+++ b/dotnet/samples/AGUIClientServer/AGUIDojoServer/SharedState/Ingredient.cs
@@ -0,0 +1,17 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+namespace AGUIDojoServer.SharedState;
+
+internal sealed class Ingredient
+{
+ [JsonPropertyName("icon")]
+ public string Icon { get; set; } = string.Empty;
+
+ [JsonPropertyName("name")]
+ public string Name { get; set; } = string.Empty;
+
+ [JsonPropertyName("amount")]
+ public string Amount { get; set; } = string.Empty;
+}
diff --git a/dotnet/samples/AGUIClientServer/AGUIDojoServer/SharedState/Recipe.cs b/dotnet/samples/AGUIClientServer/AGUIDojoServer/SharedState/Recipe.cs
new file mode 100644
index 0000000..a8485da
--- /dev/null
+++ b/dotnet/samples/AGUIClientServer/AGUIDojoServer/SharedState/Recipe.cs
@@ -0,0 +1,26 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+namespace AGUIDojoServer.SharedState;
+
+internal sealed class Recipe
+{
+ [JsonPropertyName("title")]
+ public string Title { get; set; } = string.Empty;
+
+ [JsonPropertyName("skill_level")]
+ public string SkillLevel { get; set; } = string.Empty;
+
+ [JsonPropertyName("cooking_time")]
+ public string CookingTime { get; set; } = string.Empty;
+
+ [JsonPropertyName("special_preferences")]
+ public List SpecialPreferences { get; set; } = [];
+
+ [JsonPropertyName("ingredients")]
+ public List Ingredients { get; set; } = [];
+
+ [JsonPropertyName("instructions")]
+ public List Instructions { get; set; } = [];
+}
diff --git a/dotnet/samples/AGUIClientServer/AGUIDojoServer/SharedState/RecipeResponse.cs b/dotnet/samples/AGUIClientServer/AGUIDojoServer/SharedState/RecipeResponse.cs
new file mode 100644
index 0000000..dadf3b7
--- /dev/null
+++ b/dotnet/samples/AGUIClientServer/AGUIDojoServer/SharedState/RecipeResponse.cs
@@ -0,0 +1,13 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+namespace AGUIDojoServer.SharedState;
+
+#pragma warning disable CA1812 // Used for the JsonSchema response format
+internal sealed class RecipeResponse
+#pragma warning restore CA1812
+{
+ [JsonPropertyName("recipe")]
+ public Recipe Recipe { get; set; } = new();
+}
diff --git a/dotnet/samples/AGUIClientServer/AGUIDojoServer/SharedState/SharedStateAgent.cs b/dotnet/samples/AGUIClientServer/AGUIDojoServer/SharedState/SharedStateAgent.cs
new file mode 100644
index 0000000..36a629d
--- /dev/null
+++ b/dotnet/samples/AGUIClientServer/AGUIDojoServer/SharedState/SharedStateAgent.cs
@@ -0,0 +1,106 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+using System.Text.Json;
+using Microsoft.Agents.AI;
+using Microsoft.Extensions.AI;
+
+namespace AGUIDojoServer.SharedState;
+
+[SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Instantiated by ChatClientAgentFactory.CreateSharedState")]
+internal sealed class SharedStateAgent : DelegatingAIAgent
+{
+ private readonly JsonSerializerOptions _jsonSerializerOptions;
+
+ public SharedStateAgent(AIAgent innerAgent, JsonSerializerOptions jsonSerializerOptions)
+ : base(innerAgent)
+ {
+ this._jsonSerializerOptions = jsonSerializerOptions;
+ }
+
+ protected override Task RunCoreAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
+ {
+ return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken).ToAgentResponseAsync(cancellationToken);
+ }
+
+ protected override async IAsyncEnumerable RunCoreStreamingAsync(
+ IEnumerable messages,
+ 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, thread, options, cancellationToken).ConfigureAwait(false))
+ {
+ yield return update;
+ }
+ yield break;
+ }
+
+ var firstRunOptions = new ChatClientAgentRunOptions
+ {
+ ChatOptions = chatRunOptions.ChatOptions.Clone(),
+ AllowBackgroundResponses = chatRunOptions.AllowBackgroundResponses,
+ ContinuationToken = chatRunOptions.ContinuationToken,
+ ChatClientFactory = chatRunOptions.ChatClientFactory,
+ };
+
+ // Configure JSON schema response format for structured state output
+ firstRunOptions.ChatOptions.ResponseFormat = ChatResponseFormat.ForJsonSchema(
+ schemaName: "RecipeResponse",
+ schemaDescription: "A response containing a recipe with title, skill level, cooking time, preferences, ingredients, and instructions");
+
+ ChatMessage stateUpdateMessage = new(
+ ChatRole.System,
+ [
+ new TextContent("Here is the current state in JSON format:"),
+ new TextContent(state.GetRawText()),
+ new TextContent("The new state is:")
+ ]);
+
+ var firstRunMessages = messages.Append(stateUpdateMessage);
+
+ var allUpdates = new List();
+ await foreach (var update in this.InnerAgent.RunStreamingAsync(firstRunMessages, thread, firstRunOptions, cancellationToken).ConfigureAwait(false))
+ {
+ allUpdates.Add(update);
+
+ // Yield all non-text updates (tool calls, etc.)
+ bool hasNonTextContent = update.Contents.Any(c => c is not TextContent);
+ if (hasNonTextContent)
+ {
+ yield return update;
+ }
+ }
+
+ var response = allUpdates.ToAgentResponse();
+
+ if (response.TryDeserialize(this._jsonSerializerOptions, out JsonElement stateSnapshot))
+ {
+ byte[] stateBytes = JsonSerializer.SerializeToUtf8Bytes(
+ stateSnapshot,
+ this._jsonSerializerOptions.GetTypeInfo(typeof(JsonElement)));
+ yield return new AgentResponseUpdate
+ {
+ Contents = [new DataContent(stateBytes, "application/json")]
+ };
+ }
+ else
+ {
+ yield break;
+ }
+
+ var secondRunMessages = messages.Concat(response.Messages).Append(
+ new ChatMessage(
+ 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, thread, options, cancellationToken).ConfigureAwait(false))
+ {
+ yield return update;
+ }
+ }
+}
diff --git a/dotnet/samples/AGUIClientServer/AGUIDojoServer/appsettings.Development.json b/dotnet/samples/AGUIClientServer/AGUIDojoServer/appsettings.Development.json
new file mode 100644
index 0000000..3e805ed
--- /dev/null
+++ b/dotnet/samples/AGUIClientServer/AGUIDojoServer/appsettings.Development.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning",
+ "Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware": "Information"
+ }
+ }
+}
diff --git a/dotnet/samples/AGUIClientServer/AGUIDojoServer/appsettings.json b/dotnet/samples/AGUIClientServer/AGUIDojoServer/appsettings.json
new file mode 100644
index 0000000..bb20fb6
--- /dev/null
+++ b/dotnet/samples/AGUIClientServer/AGUIDojoServer/appsettings.json
@@ -0,0 +1,10 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning",
+ "Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware": "Information"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/dotnet/samples/AGUIClientServer/AGUIServer/AGUIServer.csproj b/dotnet/samples/AGUIClientServer/AGUIServer/AGUIServer.csproj
new file mode 100644
index 0000000..ccfe229
--- /dev/null
+++ b/dotnet/samples/AGUIClientServer/AGUIServer/AGUIServer.csproj
@@ -0,0 +1,22 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+ a8b2e9f0-1ea3-4f18-9d41-42d1a6f8fe10
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/AGUIClientServer/AGUIServer/AGUIServer.http b/dotnet/samples/AGUIClientServer/AGUIServer/AGUIServer.http
new file mode 100644
index 0000000..b3f5831
--- /dev/null
+++ b/dotnet/samples/AGUIClientServer/AGUIServer/AGUIServer.http
@@ -0,0 +1,17 @@
+@host = http://localhost:5100
+
+### Send a message to the AG-UI agent
+POST {{host}}/
+Content-Type: application/json
+
+{
+ "threadId": "thread_123",
+ "runId": "run_456",
+ "messages": [
+ {
+ "role": "user",
+ "content": "What is the capital of France?"
+ }
+ ],
+ "context": {}
+}
diff --git a/dotnet/samples/AGUIClientServer/AGUIServer/AGUIServerSerializerContext.cs b/dotnet/samples/AGUIClientServer/AGUIServer/AGUIServerSerializerContext.cs
new file mode 100644
index 0000000..1ca6ad7
--- /dev/null
+++ b/dotnet/samples/AGUIClientServer/AGUIServer/AGUIServerSerializerContext.cs
@@ -0,0 +1,9 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+namespace AGUIServer;
+
+[JsonSerializable(typeof(ServerWeatherForecastRequest))]
+[JsonSerializable(typeof(ServerWeatherForecastResponse))]
+internal sealed partial class AGUIServerSerializerContext : JsonSerializerContext;
diff --git a/dotnet/samples/AGUIClientServer/AGUIServer/Program.cs b/dotnet/samples/AGUIClientServer/AGUIServer/Program.cs
new file mode 100644
index 0000000..418f72a
--- /dev/null
+++ b/dotnet/samples/AGUIClientServer/AGUIServer/Program.cs
@@ -0,0 +1,51 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.ComponentModel;
+using AGUIServer;
+using Azure.AI.OpenAI;
+using Azure.Identity;
+using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore;
+using Microsoft.Extensions.AI;
+using OpenAI.Chat;
+
+WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
+builder.Services.AddHttpClient().AddLogging();
+builder.Services.ConfigureHttpJsonOptions(options => options.SerializerOptions.TypeInfoResolverChain.Add(AGUIServerSerializerContext.Default));
+builder.Services.AddAGUI();
+
+WebApplication app = builder.Build();
+
+string endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"] ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
+string deploymentName = builder.Configuration["AZURE_OPENAI_DEPLOYMENT_NAME"] ?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set.");
+
+// Create the AI agent with tools
+var agent = new AzureOpenAIClient(
+ new Uri(endpoint),
+ new DefaultAzureCredential())
+ .GetChatClient(deploymentName)
+ .AsAIAgent(
+ name: "AGUIAssistant",
+ tools: [
+ AIFunctionFactory.Create(
+ () => DateTimeOffset.UtcNow,
+ name: "get_current_time",
+ description: "Get the current UTC time."
+ ),
+ AIFunctionFactory.Create(
+ ([Description("The weather forecast request")]ServerWeatherForecastRequest request) => {
+ return new ServerWeatherForecastResponse()
+ {
+ Summary = "Sunny",
+ TemperatureC = 25,
+ Date = request.Date
+ };
+ },
+ name: "get_server_weather_forecast",
+ description: "Gets the forecast for a specific location and date",
+ AGUIServerSerializerContext.Default.Options)
+ ]);
+
+// Map the AG-UI agent endpoint
+app.MapAGUI("/", agent);
+
+await app.RunAsync();
diff --git a/dotnet/samples/AGUIClientServer/AGUIServer/Properties/launchSettings.json b/dotnet/samples/AGUIClientServer/AGUIServer/Properties/launchSettings.json
new file mode 100644
index 0000000..6e38bd9
--- /dev/null
+++ b/dotnet/samples/AGUIClientServer/AGUIServer/Properties/launchSettings.json
@@ -0,0 +1,12 @@
+{
+ "profiles": {
+ "AGUIServer": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "applicationUrl": "http://localhost:5100;https://localhost:5101"
+ }
+ }
+}
\ No newline at end of file
diff --git a/dotnet/samples/AGUIClientServer/AGUIServer/ServerWeatherForecastRequest.cs b/dotnet/samples/AGUIClientServer/AGUIServer/ServerWeatherForecastRequest.cs
new file mode 100644
index 0000000..a4e3d98
--- /dev/null
+++ b/dotnet/samples/AGUIClientServer/AGUIServer/ServerWeatherForecastRequest.cs
@@ -0,0 +1,9 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace AGUIServer;
+
+internal sealed class ServerWeatherForecastRequest
+{
+ public DateTime Date { get; set; }
+ public string Location { get; set; } = "Seattle";
+}
diff --git a/dotnet/samples/AGUIClientServer/AGUIServer/ServerWeatherForecastResponse.cs b/dotnet/samples/AGUIClientServer/AGUIServer/ServerWeatherForecastResponse.cs
new file mode 100644
index 0000000..2bc5d8f
--- /dev/null
+++ b/dotnet/samples/AGUIClientServer/AGUIServer/ServerWeatherForecastResponse.cs
@@ -0,0 +1,12 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace AGUIServer;
+
+internal sealed class ServerWeatherForecastResponse
+{
+ public string Summary { get; set; } = "";
+
+ public int TemperatureC { get; set; }
+
+ public DateTime Date { get; set; }
+}
diff --git a/dotnet/samples/AGUIClientServer/README.md b/dotnet/samples/AGUIClientServer/README.md
new file mode 100644
index 0000000..2e4887c
--- /dev/null
+++ b/dotnet/samples/AGUIClientServer/README.md
@@ -0,0 +1,208 @@
+# AG-UI Client and Server Sample
+
+This sample demonstrates how to use the AG-UI (Agent UI) protocol to enable communication between a client application and a remote agent server. The AG-UI protocol provides a standardized way for clients to interact with AI agents.
+
+## Overview
+
+The demonstration has two components:
+
+1. **AGUIServer** - An ASP.NET Core web server that hosts an AI agent and exposes it via the AG-UI protocol
+2. **AGUIClient** - A console application that connects to the AG-UI server and displays streaming updates
+
+> **Warning**
+> The AG-UI protocol is still under development and changing.
+> We will try to keep these samples updated as the protocol evolves.
+
+## Configuring Environment Variables
+
+Configure the required Azure OpenAI environment variables:
+
+```powershell
+$env:AZURE_OPENAI_ENDPOINT="<>"
+$env:AZURE_OPENAI_DEPLOYMENT_NAME="gpt-4.1-mini"
+```
+
+> **Note:** This sample uses `DefaultAzureCredential` for authentication. Make sure you're authenticated with Azure (e.g., via `az login`, Visual Studio, or environment variables).
+
+## Running the Sample
+
+### Step 1: Start the AG-UI Server
+
+```bash
+cd AGUIServer
+dotnet build
+dotnet run --urls "http://localhost:5100"
+```
+
+The server will start and listen on `http://localhost:5100`.
+
+### Step 2: Testing with the REST Client (Optional)
+
+Before running the client, you can test the server using the included `.http` file:
+
+1. Open [./AGUIServer/AGUIServer.http](./AGUIServer/AGUIServer.http) in Visual Studio or VS Code with the REST Client extension
+2. Send a test request to verify the server is working
+3. Observe the server-sent events stream in the response
+
+Sample request:
+```http
+POST http://localhost:5100/
+Content-Type: application/json
+
+{
+ "threadId": "thread_123",
+ "runId": "run_456",
+ "messages": [
+ {
+ "role": "user",
+ "content": "What is the capital of France?"
+ }
+ ],
+ "context": {}
+}
+```
+
+### Step 3: Run the AG-UI Client
+
+In a new terminal window:
+
+```bash
+cd AGUIClient
+dotnet run
+```
+
+Optionally, configure a different server URL:
+
+```powershell
+$env:AGUI_SERVER_URL="http://localhost:5100"
+```
+
+### Step 4: Interact with the Agent
+
+1. The client will connect to the AG-UI server
+2. Enter your message at the prompt
+3. Observe the streaming updates with color-coded output:
+ - **Yellow**: Run started notification showing thread and run IDs
+ - **Cyan**: Agent's text response (streamed character by character)
+ - **Green**: Run finished notification
+ - **Red**: Error messages (if any occur)
+4. Type `:q` or `quit` to exit
+
+## Sample Output
+
+```
+AGUIClient> dotnet run
+info: AGUIClient[0]
+ Connecting to AG-UI server at: http://localhost:5100
+
+User (:q or quit to exit): What is the capital of France?
+
+[Run Started - Thread: thread_abc123, Run: run_xyz789]
+The capital of France is Paris. It is known for its rich history, culture, and iconic landmarks such as the Eiffel Tower and the Louvre Museum.
+[Run Finished - Thread: thread_abc123, Run: run_xyz789]
+
+User (:q or quit to exit): Tell me a fun fact about space
+
+[Run Started - Thread: thread_abc123, Run: run_def456]
+Here's a fun fact: A day on Venus is longer than its year! Venus takes about 243 Earth days to rotate once on its axis, but only about 225 Earth days to orbit the Sun.
+[Run Finished - Thread: thread_abc123, Run: run_def456]
+
+User (:q or quit to exit): :q
+```
+
+## How It Works
+
+### Server Side
+
+The `AGUIServer` uses the `MapAGUI` extension method to expose an agent through the AG-UI protocol:
+
+```csharp
+AIAgent agent = new OpenAIClient(apiKey)
+ .GetChatClient(model)
+ .AsAIAgent(
+ instructions: "You are a helpful assistant.",
+ name: "AGUIAssistant");
+
+app.MapAGUI("/", agent);
+```
+
+This automatically handles:
+- HTTP POST requests with message payloads
+- Converting agent responses to AG-UI event streams
+- Server-sent events (SSE) formatting
+- Thread and run management
+
+### Client Side
+
+The `AGUIClient` uses the `AGUIChatClient` to connect to the remote server:
+
+```csharp
+using HttpClient httpClient = new();
+var chatClient = new AGUIChatClient(
+ httpClient,
+ endpoint: serverUrl,
+ modelId: "agui-client",
+ jsonSerializerOptions: null);
+
+AIAgent agent = chatClient.AsAIAgent(
+ instructions: null,
+ name: "agui-client",
+ description: "AG-UI Client Agent",
+ tools: []);
+
+bool isFirstUpdate = true;
+AgentResponseUpdate? currentUpdate = null;
+
+await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(messages, thread))
+{
+ // First update indicates run started
+ if (isFirstUpdate)
+ {
+ Console.WriteLine($"[Run Started - Thread: {update.ConversationId}, Run: {update.ResponseId}]");
+ isFirstUpdate = false;
+ }
+
+ currentUpdate = update;
+
+ foreach (AIContent content in update.Contents)
+ {
+ switch (content)
+ {
+ case TextContent textContent:
+ // Display streaming text
+ Console.Write(textContent.Text);
+ break;
+ case ErrorContent errorContent:
+ // Display error notification
+ Console.WriteLine($"[Error: {errorContent.Message}]");
+ break;
+ }
+ }
+}
+
+// Last update indicates run finished
+if (currentUpdate != null)
+{
+ Console.WriteLine($"\n[Run Finished - Thread: {currentUpdate.ConversationId}, Run: {currentUpdate.ResponseId}]");
+}
+```
+
+The `RunStreamingAsync` method:
+1. Sends messages to the server via HTTP POST
+2. Receives server-sent events (SSE) stream
+3. Parses events into `AgentResponseUpdate` objects
+4. Yields updates as they arrive for real-time display
+
+## Key Concepts
+
+- **Thread**: Represents a conversation context that persists across multiple runs (accessed via `ConversationId` property)
+- **Run**: A single execution of the agent for a given set of messages (identified by `ResponseId` property)
+- **AgentResponseUpdate**: Contains the response data with:
+ - `ResponseId`: The unique run identifier
+ - `ConversationId`: The thread/conversation identifier
+ - `Contents`: Collection of content items (TextContent, ErrorContent, etc.)
+- **Run Lifecycle**:
+ - The **first** `AgentResponseUpdate` in a run indicates the run has started
+ - Subsequent updates contain streaming content as the agent processes
+ - The **last** `AgentResponseUpdate` in a run indicates the run has finished
+ - If an error occurs, the update will contain `ErrorContent`
\ No newline at end of file
diff --git a/dotnet/samples/AGUIWebChat/Client/AGUIWebChatClient.csproj b/dotnet/samples/AGUIWebChat/Client/AGUIWebChatClient.csproj
new file mode 100644
index 0000000..b28e53d
--- /dev/null
+++ b/dotnet/samples/AGUIWebChat/Client/AGUIWebChatClient.csproj
@@ -0,0 +1,14 @@
+
+
+
+ net10.0
+ enable
+ enable
+ true
+
+
+
+
+
+
+
diff --git a/dotnet/samples/AGUIWebChat/Client/Components/App.razor b/dotnet/samples/AGUIWebChat/Client/Components/App.razor
new file mode 100644
index 0000000..a64d576
--- /dev/null
+++ b/dotnet/samples/AGUIWebChat/Client/Components/App.razor
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+@code {
+ private readonly IComponentRenderMode renderMode = new InteractiveServerRenderMode(prerender: false);
+}
diff --git a/dotnet/samples/AGUIWebChat/Client/Components/Layout/LoadingSpinner.razor b/dotnet/samples/AGUIWebChat/Client/Components/Layout/LoadingSpinner.razor
new file mode 100644
index 0000000..116455c
--- /dev/null
+++ b/dotnet/samples/AGUIWebChat/Client/Components/Layout/LoadingSpinner.razor
@@ -0,0 +1 @@
+
diff --git a/dotnet/samples/AGUIWebChat/Client/Components/Layout/LoadingSpinner.razor.css b/dotnet/samples/AGUIWebChat/Client/Components/Layout/LoadingSpinner.razor.css
new file mode 100644
index 0000000..e599d27
--- /dev/null
+++ b/dotnet/samples/AGUIWebChat/Client/Components/Layout/LoadingSpinner.razor.css
@@ -0,0 +1,89 @@
+/* Used under CC0 license */
+
+.lds-ellipsis {
+ color: #666;
+ animation: fade-in 1s;
+}
+
+@keyframes fade-in {
+ 0% {
+ opacity: 0;
+ }
+
+ 100% {
+ opacity: 1;
+ }
+}
+
+ .lds-ellipsis,
+ .lds-ellipsis div {
+ box-sizing: border-box;
+ }
+
+.lds-ellipsis {
+ margin: auto;
+ display: block;
+ position: relative;
+ width: 80px;
+ height: 80px;
+}
+
+ .lds-ellipsis div {
+ position: absolute;
+ top: 33.33333px;
+ width: 10px;
+ height: 10px;
+ border-radius: 50%;
+ background: currentColor;
+ animation-timing-function: cubic-bezier(0, 1, 1, 0);
+ }
+
+ .lds-ellipsis div:nth-child(1) {
+ left: 8px;
+ animation: lds-ellipsis1 0.6s infinite;
+ }
+
+ .lds-ellipsis div:nth-child(2) {
+ left: 8px;
+ animation: lds-ellipsis2 0.6s infinite;
+ }
+
+ .lds-ellipsis div:nth-child(3) {
+ left: 32px;
+ animation: lds-ellipsis2 0.6s infinite;
+ }
+
+ .lds-ellipsis div:nth-child(4) {
+ left: 56px;
+ animation: lds-ellipsis3 0.6s infinite;
+ }
+
+@keyframes lds-ellipsis1 {
+ 0% {
+ transform: scale(0);
+ }
+
+ 100% {
+ transform: scale(1);
+ }
+}
+
+@keyframes lds-ellipsis3 {
+ 0% {
+ transform: scale(1);
+ }
+
+ 100% {
+ transform: scale(0);
+ }
+}
+
+@keyframes lds-ellipsis2 {
+ 0% {
+ transform: translate(0, 0);
+ }
+
+ 100% {
+ transform: translate(24px, 0);
+ }
+}
diff --git a/dotnet/samples/AGUIWebChat/Client/Components/Layout/MainLayout.razor b/dotnet/samples/AGUIWebChat/Client/Components/Layout/MainLayout.razor
new file mode 100644
index 0000000..f3da3cb
--- /dev/null
+++ b/dotnet/samples/AGUIWebChat/Client/Components/Layout/MainLayout.razor
@@ -0,0 +1,9 @@
+@inherits LayoutComponentBase
+
+@Body
+
+
+ An unhandled error has occurred.
+
Reload
+
🗙
+
diff --git a/dotnet/samples/AGUIWebChat/Client/Components/Layout/MainLayout.razor.css b/dotnet/samples/AGUIWebChat/Client/Components/Layout/MainLayout.razor.css
new file mode 100644
index 0000000..60cec92
--- /dev/null
+++ b/dotnet/samples/AGUIWebChat/Client/Components/Layout/MainLayout.razor.css
@@ -0,0 +1,20 @@
+#blazor-error-ui {
+ color-scheme: light only;
+ background: lightyellow;
+ bottom: 0;
+ box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
+ box-sizing: border-box;
+ display: none;
+ left: 0;
+ padding: 0.6rem 1.25rem 0.7rem 1.25rem;
+ position: fixed;
+ width: 100%;
+ z-index: 1000;
+}
+
+ #blazor-error-ui .dismiss {
+ cursor: pointer;
+ position: absolute;
+ right: 0.75rem;
+ top: 0.5rem;
+ }
diff --git a/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/Chat.razor b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/Chat.razor
new file mode 100644
index 0000000..31eb7e4
--- /dev/null
+++ b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/Chat.razor
@@ -0,0 +1,94 @@
+@page "/"
+@using System.ComponentModel
+@inject IChatClient ChatClient
+@inject NavigationManager Nav
+@implements IDisposable
+
+Chat
+
+
+
+
+
+ Ask the assistant a question to start a conversation.
+
+
+
+
+
+
+
+@code {
+ private const string SystemPrompt = @"
+ You are a helpful assistant.
+ ";
+
+ private int statefulMessageCount;
+ private readonly ChatOptions chatOptions = new();
+ private readonly List messages = new();
+ private CancellationTokenSource? currentResponseCancellation;
+ private ChatMessage? currentResponseMessage;
+ private ChatInput? chatInput;
+ private ChatSuggestions? chatSuggestions;
+
+ protected override void OnInitialized()
+ {
+ statefulMessageCount = 0;
+ messages.Add(new(ChatRole.System, SystemPrompt));
+ }
+
+ private async Task AddUserMessageAsync(ChatMessage userMessage)
+ {
+ CancelAnyCurrentResponse();
+
+ // Add the user message to the conversation
+ messages.Add(userMessage);
+ chatSuggestions?.Clear();
+ await chatInput!.FocusAsync();
+
+ // Stream and display a new response from the IChatClient
+ var responseText = new TextContent("");
+ currentResponseMessage = new ChatMessage(ChatRole.Assistant, [responseText]);
+ StateHasChanged();
+ currentResponseCancellation = new();
+ await foreach (var update in ChatClient.GetStreamingResponseAsync(messages.Skip(statefulMessageCount), chatOptions, currentResponseCancellation.Token))
+ {
+ messages.AddMessages(update, filter: c => c is not TextContent);
+ responseText.Text += update.Text;
+ chatOptions.ConversationId = update.ConversationId;
+ ChatMessageItem.NotifyChanged(currentResponseMessage);
+ }
+
+ // Store the final response in the conversation, and begin getting suggestions
+ messages.Add(currentResponseMessage!);
+ statefulMessageCount = chatOptions.ConversationId is not null ? messages.Count : 0;
+ currentResponseMessage = null;
+ chatSuggestions?.Update(messages);
+ }
+
+ private void CancelAnyCurrentResponse()
+ {
+ // If a response was cancelled while streaming, include it in the conversation so it's not lost
+ if (currentResponseMessage is not null)
+ {
+ messages.Add(currentResponseMessage);
+ }
+
+ currentResponseCancellation?.Cancel();
+ currentResponseMessage = null;
+ }
+
+ private async Task ResetConversationAsync()
+ {
+ CancelAnyCurrentResponse();
+ messages.Clear();
+ messages.Add(new(ChatRole.System, SystemPrompt));
+ chatOptions.ConversationId = null;
+ statefulMessageCount = 0;
+ chatSuggestions?.Clear();
+ await chatInput!.FocusAsync();
+ }
+
+ public void Dispose()
+ => currentResponseCancellation?.Cancel();
+}
diff --git a/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/Chat.razor.css b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/Chat.razor.css
new file mode 100644
index 0000000..0884160
--- /dev/null
+++ b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/Chat.razor.css
@@ -0,0 +1,11 @@
+.chat-container {
+ position: sticky;
+ bottom: 0;
+ padding-left: 1.5rem;
+ padding-right: 1.5rem;
+ padding-top: 0.75rem;
+ padding-bottom: 1.5rem;
+ border-top-width: 1px;
+ background-color: #F3F4F6;
+ border-color: #E5E7EB;
+}
diff --git a/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatCitation.razor b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatCitation.razor
new file mode 100644
index 0000000..ccb5853
--- /dev/null
+++ b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatCitation.razor
@@ -0,0 +1,38 @@
+@using System.Web
+@if (!string.IsNullOrWhiteSpace(viewerUrl))
+{
+
+
+
+
+
+
+}
+
+@code {
+ [Parameter]
+ public required string File { get; set; }
+
+ [Parameter]
+ public int? PageNumber { get; set; }
+
+ [Parameter]
+ public required string Quote { get; set; }
+
+ private string? viewerUrl;
+
+ protected override void OnParametersSet()
+ {
+ viewerUrl = null;
+
+ // If you ingest other types of content besides PDF files, construct a URL to an appropriate viewer here
+ if (File.EndsWith(".pdf"))
+ {
+ var search = Quote?.Trim('.', ',', ' ', '\n', '\r', '\t', '"', '\'');
+ viewerUrl = $"lib/pdf_viewer/viewer.html?file=/Data/{HttpUtility.UrlEncode(File)}#page={PageNumber}&search={HttpUtility.UrlEncode(search)}&phrase=true";
+ }
+ }
+}
diff --git a/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatCitation.razor.css b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatCitation.razor.css
new file mode 100644
index 0000000..763c82a
--- /dev/null
+++ b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatCitation.razor.css
@@ -0,0 +1,37 @@
+.citation {
+ display: inline-flex;
+ padding-top: 0.5rem;
+ padding-bottom: 0.5rem;
+ padding-left: 0.75rem;
+ padding-right: 0.75rem;
+ margin-top: 1rem;
+ margin-right: 1rem;
+ border-bottom: 2px solid #a770de;
+ gap: 0.5rem;
+ border-radius: 0.25rem;
+ font-size: 0.875rem;
+ line-height: 1.25rem;
+ background-color: #ffffff;
+}
+
+ .citation[href]:hover {
+ outline: 1px solid #865cb1;
+ }
+
+ .citation svg {
+ width: 1.5rem;
+ height: 1.5rem;
+ }
+
+ .citation:active {
+ background-color: rgba(0,0,0,0.05);
+ }
+
+.citation-content {
+ display: flex;
+ flex-direction: column;
+}
+
+.citation-file {
+ font-weight: 600;
+}
diff --git a/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatHeader.razor b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatHeader.razor
new file mode 100644
index 0000000..a339038
--- /dev/null
+++ b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatHeader.razor
@@ -0,0 +1,17 @@
+
+
+
+
AGUI WebChat
+
+
+@code {
+ [Parameter]
+ public EventCallback OnNewChat { get; set; }
+}
diff --git a/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatHeader.razor.css b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatHeader.razor.css
new file mode 100644
index 0000000..97f0a8d
--- /dev/null
+++ b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatHeader.razor.css
@@ -0,0 +1,25 @@
+.chat-header-container {
+ top: 0;
+ padding: 1.5rem;
+}
+
+.chat-header-controls {
+ margin-bottom: 1.5rem;
+}
+
+h1 {
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.new-chat-icon {
+ width: 1.25rem;
+ height: 1.25rem;
+ color: rgb(55, 65, 81);
+}
+
+@media (min-width: 768px) {
+ .chat-header-container {
+ position: sticky;
+ }
+}
diff --git a/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatInput.razor b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatInput.razor
new file mode 100644
index 0000000..e87ac6c
--- /dev/null
+++ b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatInput.razor
@@ -0,0 +1,51 @@
+@inject IJSRuntime JS
+
+
+
+
+
+
+
+
+
+@code {
+ private ElementReference textArea;
+ private string? messageText;
+
+ [Parameter]
+ public EventCallback OnSend { get; set; }
+
+ public ValueTask FocusAsync()
+ => textArea.FocusAsync();
+
+ private async Task SendMessageAsync()
+ {
+ if (messageText is { Length: > 0 } text)
+ {
+ messageText = null;
+ await OnSend.InvokeAsync(new ChatMessage(ChatRole.User, text));
+ }
+ }
+
+ protected override async Task OnAfterRenderAsync(bool firstRender)
+ {
+ if (firstRender)
+ {
+ try
+ {
+ var module = await JS.InvokeAsync("import", "./Components/Pages/Chat/ChatInput.razor.js");
+ await module.InvokeVoidAsync("init", textArea);
+ await module.DisposeAsync();
+ }
+ catch (JSDisconnectedException)
+ {
+ }
+ }
+ }
+}
diff --git a/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatInput.razor.css b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatInput.razor.css
new file mode 100644
index 0000000..375dd71
--- /dev/null
+++ b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatInput.razor.css
@@ -0,0 +1,57 @@
+.input-box {
+ display: flex;
+ flex-direction: column;
+ background: white;
+ border: 1px solid rgb(229, 231, 235);
+ border-radius: 8px;
+ padding: 0.5rem 0.75rem;
+ margin-top: 0.75rem;
+}
+
+ .input-box:focus-within {
+ outline: 2px solid #4152d5;
+ }
+
+textarea {
+ resize: none;
+ border: none;
+ outline: none;
+ flex-grow: 1;
+}
+
+ textarea:placeholder-shown + .tools {
+ --send-button-color: #aaa;
+ }
+
+.tools {
+ display: flex;
+ margin-top: 1rem;
+ align-items: center;
+}
+
+.tool-icon {
+ width: 1.25rem;
+ height: 1.25rem;
+}
+
+.send-button {
+ color: var(--send-button-color);
+ margin-left: auto;
+}
+
+ .send-button:hover {
+ color: black;
+ }
+
+.attach {
+ background-color: white;
+ border-style: dashed;
+ color: #888;
+ border-color: #888;
+ padding: 3px 8px;
+}
+
+ .attach:hover {
+ background-color: #f0f0f0;
+ color: black;
+ }
diff --git a/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatInput.razor.js b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatInput.razor.js
new file mode 100644
index 0000000..e4bd8af
--- /dev/null
+++ b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatInput.razor.js
@@ -0,0 +1,43 @@
+export function init(elem) {
+ elem.focus();
+
+ // Auto-resize whenever the user types or if the value is set programmatically
+ elem.addEventListener('input', () => resizeToFit(elem));
+ afterPropertyWritten(elem, 'value', () => resizeToFit(elem));
+
+ // Auto-submit the form on 'enter' keypress
+ elem.addEventListener('keydown', (e) => {
+ if (e.key === 'Enter' && !e.shiftKey) {
+ e.preventDefault();
+ elem.dispatchEvent(new CustomEvent('change', { bubbles: true }));
+ elem.closest('form').dispatchEvent(new CustomEvent('submit', { bubbles: true, cancelable: true }));
+ }
+ });
+}
+
+function resizeToFit(elem) {
+ const lineHeight = parseFloat(getComputedStyle(elem).lineHeight);
+
+ elem.rows = 1;
+ const numLines = Math.ceil(elem.scrollHeight / lineHeight);
+ elem.rows = Math.min(5, Math.max(1, numLines));
+}
+
+function afterPropertyWritten(target, propName, callback) {
+ const descriptor = getPropertyDescriptor(target, propName);
+ Object.defineProperty(target, propName, {
+ get: function () {
+ return descriptor.get.apply(this, arguments);
+ },
+ set: function () {
+ const result = descriptor.set.apply(this, arguments);
+ callback();
+ return result;
+ }
+ });
+}
+
+function getPropertyDescriptor(target, propertyName) {
+ return Object.getOwnPropertyDescriptor(target, propertyName)
+ || getPropertyDescriptor(Object.getPrototypeOf(target), propertyName);
+}
diff --git a/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatMessageItem.razor b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatMessageItem.razor
new file mode 100644
index 0000000..6f4e135
--- /dev/null
+++ b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatMessageItem.razor
@@ -0,0 +1,73 @@
+@using System.Runtime.CompilerServices
+@using System.Text.RegularExpressions
+@using System.Linq
+
+@if (Message.Role == ChatRole.User)
+{
+
+ @Message.Text
+
+}
+else if (Message.Role == ChatRole.Assistant)
+{
+ foreach (var content in Message.Contents)
+ {
+ if (content is TextContent { Text: { Length: > 0 } text })
+ {
+
+
+
+
+
@((MarkupString)text)
+
+
+ }
+ else if (content is FunctionCallContent { Name: "Search" } fcc && fcc.Arguments?.TryGetValue("searchPhrase", out var searchPhrase) is true)
+ {
+
+
+
+ Searching:
+ @searchPhrase
+ @if (fcc.Arguments?.TryGetValue("filenameFilter", out var filenameObj) is true && filenameObj is string filename && !string.IsNullOrEmpty(filename))
+ {
+ in @filename
+ }
+
+
+ }
+ }
+}
+
+@code {
+ private static readonly ConditionalWeakTable SubscribersLookup = new();
+
+ [Parameter, EditorRequired]
+ public required ChatMessage Message { get; set; }
+
+ [Parameter]
+ public bool InProgress { get; set;}
+
+ protected override void OnInitialized()
+ {
+ SubscribersLookup.AddOrUpdate(Message, this);
+ }
+
+ public static void NotifyChanged(ChatMessage source)
+ {
+ if (SubscribersLookup.TryGetValue(source, out var subscriber))
+ {
+ subscriber.StateHasChanged();
+ }
+ }
+}
diff --git a/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatMessageItem.razor.css b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatMessageItem.razor.css
new file mode 100644
index 0000000..16443cf
--- /dev/null
+++ b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatMessageItem.razor.css
@@ -0,0 +1,67 @@
+.user-message {
+ background: rgb(182 215 232);
+ align-self: flex-end;
+ min-width: 25%;
+ max-width: calc(100% - 5rem);
+ padding: 0.5rem 1.25rem;
+ border-radius: 0.25rem;
+ color: #1F2937;
+ white-space: pre-wrap;
+}
+
+.assistant-message, .assistant-search {
+ display: grid;
+ grid-template-rows: min-content;
+ grid-template-columns: 2rem minmax(0, 1fr);
+ gap: 0.25rem;
+}
+
+.assistant-message-header {
+ font-weight: 600;
+}
+
+.assistant-message-text {
+ grid-column-start: 2;
+}
+
+.assistant-message-icon {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ border-radius: 9999px;
+ width: 1.5rem;
+ height: 1.5rem;
+ color: #ffffff;
+ background: #9b72ce;
+}
+
+ .assistant-message-icon svg {
+ width: 1rem;
+ height: 1rem;
+ }
+
+.assistant-search {
+ font-size: 0.875rem;
+ line-height: 1.25rem;
+}
+
+.assistant-search-icon {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 1.5rem;
+ height: 1.5rem;
+}
+
+ .assistant-search-icon svg {
+ width: 1rem;
+ height: 1rem;
+ }
+
+.assistant-search-content {
+ align-content: center;
+}
+
+.assistant-search-phrase {
+ font-weight: 600;
+}
diff --git a/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatMessageList.razor b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatMessageList.razor
new file mode 100644
index 0000000..d245f45
--- /dev/null
+++ b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatMessageList.razor
@@ -0,0 +1,42 @@
+@inject IJSRuntime JS
+
+
+
+ @foreach (var message in Messages)
+ {
+
+ }
+
+ @if (InProgressMessage is not null)
+ {
+
+
+ }
+ else if (IsEmpty)
+ {
+ @NoMessagesContent
+ }
+
+
+
+@code {
+ [Parameter]
+ public required IEnumerable Messages { get; set; }
+
+ [Parameter]
+ public ChatMessage? InProgressMessage { get; set; }
+
+ [Parameter]
+ public RenderFragment? NoMessagesContent { get; set; }
+
+ private bool IsEmpty => !Messages.Any(m => (m.Role == ChatRole.User || m.Role == ChatRole.Assistant) && !string.IsNullOrEmpty(m.Text));
+
+ protected override async Task OnAfterRenderAsync(bool firstRender)
+ {
+ if (firstRender)
+ {
+ // Activates the auto-scrolling behavior
+ await JS.InvokeVoidAsync("import", "./Components/Pages/Chat/ChatMessageList.razor.js");
+ }
+ }
+}
diff --git a/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatMessageList.razor.css b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatMessageList.razor.css
new file mode 100644
index 0000000..4be50dd
--- /dev/null
+++ b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatMessageList.razor.css
@@ -0,0 +1,22 @@
+.message-list-container {
+ margin: 2rem 1.5rem;
+ flex-grow: 1;
+}
+
+.message-list {
+ display: flex;
+ flex-direction: column;
+ gap: 1.25rem;
+}
+
+.no-messages {
+ text-align: center;
+ font-size: 1.25rem;
+ color: #999;
+ margin-top: calc(40vh - 18rem);
+}
+
+chat-messages > ::deep div:last-of-type {
+ /* Adds some vertical buffer to so that suggestions don't overlap the output when they appear */
+ margin-bottom: 2rem;
+}
diff --git a/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatMessageList.razor.js b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatMessageList.razor.js
new file mode 100644
index 0000000..9755d47
--- /dev/null
+++ b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatMessageList.razor.js
@@ -0,0 +1,34 @@
+// The following logic provides auto-scroll behavior for the chat messages list.
+// If you don't want that behavior, you can simply not load this module.
+
+window.customElements.define('chat-messages', class ChatMessages extends HTMLElement {
+ static _isFirstAutoScroll = true;
+
+ connectedCallback() {
+ this._observer = new MutationObserver(mutations => this._scheduleAutoScroll(mutations));
+ this._observer.observe(this, { childList: true, attributes: true });
+ }
+
+ disconnectedCallback() {
+ this._observer.disconnect();
+ }
+
+ _scheduleAutoScroll(mutations) {
+ // Debounce the calls in case multiple DOM updates occur together
+ cancelAnimationFrame(this._nextAutoScroll);
+ this._nextAutoScroll = requestAnimationFrame(() => {
+ const addedUserMessage = mutations.some(m => Array.from(m.addedNodes).some(n => n.parentElement === this && n.classList?.contains('user-message')));
+ const elem = this.lastElementChild;
+ if (ChatMessages._isFirstAutoScroll || addedUserMessage || this._elemIsNearScrollBoundary(elem, 300)) {
+ elem.scrollIntoView({ behavior: ChatMessages._isFirstAutoScroll ? 'instant' : 'smooth' });
+ ChatMessages._isFirstAutoScroll = false;
+ }
+ });
+ }
+
+ _elemIsNearScrollBoundary(elem, threshold) {
+ const maxScrollPos = document.body.scrollHeight - window.innerHeight;
+ const remainingScrollDistance = maxScrollPos - window.scrollY;
+ return remainingScrollDistance < elem.offsetHeight + threshold;
+ }
+});
diff --git a/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatSuggestions.razor b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatSuggestions.razor
new file mode 100644
index 0000000..69ca922
--- /dev/null
+++ b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatSuggestions.razor
@@ -0,0 +1,78 @@
+@inject IChatClient ChatClient
+
+@if (suggestions is not null)
+{
+
+ @foreach (var suggestion in suggestions)
+ {
+ AddSuggestionAsync(suggestion))">
+ @suggestion
+
+ }
+
+}
+
+@code {
+ private static string Prompt = @"
+ Suggest up to 3 follow-up questions that I could ask you to help me complete my task.
+ Each suggestion must be a complete sentence, maximum 6 words.
+ Each suggestion must be phrased as something that I (the user) would ask you (the assistant) in response to your previous message,
+ for example 'How do I do that?' or 'Explain ...'.
+ If there are no suggestions, reply with an empty list.
+ ";
+
+ private string[]? suggestions;
+ private CancellationTokenSource? cancellation;
+
+ [Parameter]
+ public EventCallback OnSelected { get; set; }
+
+ public void Clear()
+ {
+ suggestions = null;
+ cancellation?.Cancel();
+ }
+
+ public void Update(IReadOnlyList messages)
+ {
+ // Runs in the background and handles its own cancellation/errors
+ _ = UpdateSuggestionsAsync(messages);
+ }
+
+ private async Task UpdateSuggestionsAsync(IReadOnlyList messages)
+ {
+ cancellation?.Cancel();
+ cancellation = new CancellationTokenSource();
+
+ try
+ {
+ var response = await ChatClient.GetResponseAsync(
+ [.. ReduceMessages(messages), new(ChatRole.User, Prompt)],
+ cancellationToken: cancellation.Token);
+ if (!response.TryGetResult(out suggestions))
+ {
+ suggestions = null;
+ }
+
+ StateHasChanged();
+ }
+ catch (Exception ex) when (ex is not OperationCanceledException)
+ {
+ await DispatchExceptionAsync(ex);
+ }
+ }
+
+ private async Task AddSuggestionAsync(string text)
+ {
+ await OnSelected.InvokeAsync(new(ChatRole.User, text));
+ }
+
+ private IEnumerable ReduceMessages(IReadOnlyList messages)
+ {
+ // Get any leading system messages, plus up to 5 user/assistant messages
+ // This should be enough context to generate suggestions without unnecessarily resending entire conversations when long
+ var systemMessages = messages.TakeWhile(m => m.Role == ChatRole.System);
+ var otherMessages = messages.Where((m, index) => m.Role == ChatRole.User || m.Role == ChatRole.Assistant).Where(m => !string.IsNullOrEmpty(m.Text)).TakeLast(5);
+ return systemMessages.Concat(otherMessages);
+ }
+}
diff --git a/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatSuggestions.razor.css b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatSuggestions.razor.css
new file mode 100644
index 0000000..dcc7ee8
--- /dev/null
+++ b/dotnet/samples/AGUIWebChat/Client/Components/Pages/Chat/ChatSuggestions.razor.css
@@ -0,0 +1,9 @@
+.suggestions {
+ text-align: right;
+ white-space: nowrap;
+ gap: 0.5rem;
+ justify-content: flex-end;
+ flex-wrap: wrap;
+ display: flex;
+ margin-bottom: 0.75rem;
+}
diff --git a/dotnet/samples/AGUIWebChat/Client/Components/Routes.razor b/dotnet/samples/AGUIWebChat/Client/Components/Routes.razor
new file mode 100644
index 0000000..faa2a8c
--- /dev/null
+++ b/dotnet/samples/AGUIWebChat/Client/Components/Routes.razor
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/dotnet/samples/AGUIWebChat/Client/Components/_Imports.razor b/dotnet/samples/AGUIWebChat/Client/Components/_Imports.razor
new file mode 100644
index 0000000..82be3d4
--- /dev/null
+++ b/dotnet/samples/AGUIWebChat/Client/Components/_Imports.razor
@@ -0,0 +1,12 @@
+@using System.Net.Http
+@using System.Net.Http.Json
+@using Microsoft.AspNetCore.Components.Forms
+@using Microsoft.AspNetCore.Components.Routing
+@using Microsoft.AspNetCore.Components.Web
+@using static Microsoft.AspNetCore.Components.Web.RenderMode
+@using Microsoft.AspNetCore.Components.Web.Virtualization
+@using Microsoft.JSInterop
+@using AGUIWebChatClient
+@using AGUIWebChatClient.Components
+@using AGUIWebChatClient.Components.Layout
+@using Microsoft.Extensions.AI
diff --git a/dotnet/samples/AGUIWebChat/Client/Program.cs b/dotnet/samples/AGUIWebChat/Client/Program.cs
new file mode 100644
index 0000000..c145227
--- /dev/null
+++ b/dotnet/samples/AGUIWebChat/Client/Program.cs
@@ -0,0 +1,34 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using AGUIWebChatClient.Components;
+using Microsoft.Agents.AI.AGUI;
+
+WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
+
+// Add services to the container.
+builder.Services.AddRazorComponents()
+ .AddInteractiveServerComponents();
+
+string serverUrl = builder.Configuration["SERVER_URL"] ?? "http://localhost:5100";
+
+builder.Services.AddHttpClient("aguiserver", httpClient => httpClient.BaseAddress = new Uri(serverUrl));
+
+builder.Services.AddChatClient(sp => new AGUIChatClient(
+ sp.GetRequiredService().CreateClient("aguiserver"), "ag-ui"));
+
+WebApplication app = builder.Build();
+
+// Configure the HTTP request pipeline.
+if (!app.Environment.IsDevelopment())
+{
+ app.UseExceptionHandler("/Error", createScopeForErrors: true);
+ app.UseHsts();
+}
+
+app.UseHttpsRedirection();
+app.UseAntiforgery();
+app.MapStaticAssets();
+app.MapRazorComponents()
+ .AddInteractiveServerRenderMode();
+
+app.Run();
diff --git a/dotnet/samples/AGUIWebChat/Client/Properties/launchSettings.json b/dotnet/samples/AGUIWebChat/Client/Properties/launchSettings.json
new file mode 100644
index 0000000..348e16b
--- /dev/null
+++ b/dotnet/samples/AGUIWebChat/Client/Properties/launchSettings.json
@@ -0,0 +1,15 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:5000",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "SERVER_URL": "http://localhost:5100"
+ }
+ }
+ }
+}
diff --git a/dotnet/samples/AGUIWebChat/Client/wwwroot/app.css b/dotnet/samples/AGUIWebChat/Client/wwwroot/app.css
new file mode 100644
index 0000000..5fd82f3
--- /dev/null
+++ b/dotnet/samples/AGUIWebChat/Client/wwwroot/app.css
@@ -0,0 +1,93 @@
+html {
+ min-height: 100vh;
+}
+
+html, .main-background-gradient {
+ background: linear-gradient(to bottom, rgb(225 227 233), #f4f4f4 25rem);
+}
+
+body {
+ display: flex;
+ flex-direction: column;
+ min-height: 100vh;
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+}
+
+html::after {
+ content: '';
+ background-image: linear-gradient(to right, #3a4ed5, #3acfd5 15%, #d53abf 85%, red);
+ width: 100%;
+ height: 2px;
+ position: fixed;
+ top: 0;
+}
+
+h1 {
+ font-size: 2.25rem;
+ line-height: 2.5rem;
+ font-weight: 600;
+}
+
+h1:focus {
+ outline: none;
+}
+
+.valid.modified:not([type=checkbox]) {
+ outline: 1px solid #26b050;
+}
+
+.invalid {
+ outline: 1px solid #e50000;
+}
+
+.validation-message {
+ color: #e50000;
+}
+
+.blazor-error-boundary {
+ background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
+ padding: 1rem 1rem 1rem 3.7rem;
+ color: white;
+}
+
+ .blazor-error-boundary::after {
+ content: "An error has occurred."
+ }
+
+.btn-default {
+ display: flex;
+ padding: 0.25rem 0.75rem;
+ gap: 0.25rem;
+ align-items: center;
+ border-radius: 0.25rem;
+ border: 1px solid #9CA3AF;
+ font-size: 0.875rem;
+ line-height: 1.25rem;
+ font-weight: 600;
+ background-color: #D1D5DB;
+}
+
+ .btn-default:hover {
+ background-color: #E5E7EB;
+ }
+
+.btn-subtle {
+ display: flex;
+ padding: 0.25rem 0.75rem;
+ gap: 0.25rem;
+ align-items: center;
+ border-radius: 0.25rem;
+ border: 1px solid #D1D5DB;
+ font-size: 0.875rem;
+ line-height: 1.25rem;
+}
+
+ .btn-subtle:hover {
+ border-color: #93C5FD;
+ background-color: #DBEAFE;
+ }
+
+.page-width {
+ max-width: 1024px;
+ margin: auto;
+}
diff --git a/dotnet/samples/AGUIWebChat/Client/wwwroot/favicon.png b/dotnet/samples/AGUIWebChat/Client/wwwroot/favicon.png
new file mode 100644
index 0000000..8422b59
Binary files /dev/null and b/dotnet/samples/AGUIWebChat/Client/wwwroot/favicon.png differ
diff --git a/dotnet/samples/AGUIWebChat/README.md b/dotnet/samples/AGUIWebChat/README.md
new file mode 100644
index 0000000..bdb8ae2
--- /dev/null
+++ b/dotnet/samples/AGUIWebChat/README.md
@@ -0,0 +1,185 @@
+# AGUI WebChat Sample
+
+This sample demonstrates a Blazor-based web chat application using the AG-UI protocol to communicate with an AI agent server.
+
+The sample consists of two projects:
+
+1. **Server** - An ASP.NET Core server that hosts a simple chat agent using the AG-UI protocol
+2. **Client** - A Blazor Server application with a rich chat UI for interacting with the agent
+
+## Prerequisites
+
+### Azure OpenAI Configuration
+
+The server requires Azure OpenAI credentials. Set the following environment variables:
+
+```powershell
+$env:AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com/"
+$env:AZURE_OPENAI_DEPLOYMENT_NAME="your-deployment-name" # e.g., "gpt-4o"
+```
+
+The server uses `DefaultAzureCredential` for authentication. Ensure you are logged in using one of the following methods:
+
+- Azure CLI: `az login`
+- Azure PowerShell: `Connect-AzAccount`
+- Visual Studio or VS Code with Azure extensions
+- Environment variables with service principal credentials
+
+## Running the Sample
+
+### Step 1: Start the Server
+
+Open a terminal and navigate to the Server directory:
+
+```powershell
+cd Server
+dotnet run
+```
+
+The server will start on `http://localhost:5100` and expose the AG-UI endpoint at `/ag-ui`.
+
+### Step 2: Start the Client
+
+Open a new terminal and navigate to the Client directory:
+
+```powershell
+cd Client
+dotnet run
+```
+
+The client will start on `http://localhost:5000`. Open your browser and navigate to `http://localhost:5000` to access the chat interface.
+
+### Step 3: Chat with the Agent
+
+Type your message in the text box at the bottom of the page and press Enter or click the send button. The assistant will respond with streaming text that appears in real-time.
+
+Features:
+- **Streaming responses**: Watch the assistant's response appear word by word
+- **Conversation suggestions**: The assistant may offer follow-up questions after responding
+- **New chat**: Click the "New chat" button to start a fresh conversation
+- **Auto-scrolling**: The chat automatically scrolls to show new messages
+
+## How It Works
+
+### Server (AG-UI Host)
+
+The server (`Server/Program.cs`) creates a simple chat agent:
+
+```csharp
+// Create Azure OpenAI client
+AzureOpenAIClient azureOpenAIClient = new AzureOpenAIClient(
+ new Uri(endpoint),
+ new DefaultAzureCredential());
+
+ChatClient chatClient = azureOpenAIClient.GetChatClient(deploymentName);
+
+// Create AI agent
+ChatClientAgent agent = chatClient.AsIChatClient().AsAIAgent(
+ name: "ChatAssistant",
+ instructions: "You are a helpful assistant.");
+
+// Map AG-UI endpoint
+app.MapAGUI("/ag-ui", agent);
+```
+
+The server exposes the agent via the AG-UI protocol at `http://localhost:5100/ag-ui`.
+
+### Client (Blazor Web App)
+
+The client (`Client/Program.cs`) configures an `AGUIChatClient` to connect to the server:
+
+```csharp
+string serverUrl = builder.Configuration["SERVER_URL"] ?? "http://localhost:5100";
+
+builder.Services.AddHttpClient("aguiserver", httpClient => httpClient.BaseAddress = new Uri(serverUrl));
+
+builder.Services.AddChatClient(sp => new AGUIChatClient(
+ sp.GetRequiredService().CreateClient("aguiserver"), "ag-ui"));
+```
+
+The Blazor UI (`Client/Components/Pages/Chat/Chat.razor`) uses the `IChatClient` to:
+- Send user messages to the agent
+- Stream responses back in real-time
+- Maintain conversation history
+- Display messages with appropriate styling
+
+### UI Components
+
+The chat interface is built from several Blazor components:
+
+- **Chat.razor** - Main chat page coordinating the conversation flow
+- **ChatHeader.razor** - Header with "New chat" button
+- **ChatMessageList.razor** - Scrollable list of messages with auto-scroll
+- **ChatMessageItem.razor** - Individual message rendering (user vs assistant)
+- **ChatInput.razor** - Text input with auto-resize and keyboard shortcuts
+- **ChatSuggestions.razor** - AI-generated follow-up question suggestions
+- **LoadingSpinner.razor** - Animated loading indicator during streaming
+
+## Configuration
+
+### Server Configuration
+
+The server URL and port are configured in `Server/Properties/launchSettings.json`:
+
+```json
+{
+ "profiles": {
+ "http": {
+ "applicationUrl": "http://localhost:5100"
+ }
+ }
+}
+```
+
+### Client Configuration
+
+The client connects to the server URL specified in `Client/Properties/launchSettings.json`:
+
+```json
+{
+ "profiles": {
+ "http": {
+ "applicationUrl": "http://localhost:5000",
+ "environmentVariables": {
+ "SERVER_URL": "http://localhost:5100"
+ }
+ }
+ }
+}
+```
+
+To change the server URL, modify the `SERVER_URL` environment variable in the client's launch settings or provide it at runtime:
+
+```powershell
+$env:SERVER_URL="http://your-server:5100"
+dotnet run
+```
+
+## Customization
+
+### Changing the Agent Instructions
+
+Edit the instructions in `Server/Program.cs`:
+
+```csharp
+ChatClientAgent agent = chatClient.AsIChatClient().AsAIAgent(
+ name: "ChatAssistant",
+ instructions: "You are a helpful coding assistant specializing in C# and .NET.");
+```
+
+### Styling the UI
+
+The chat interface uses CSS files colocated with each Razor component. Key styles:
+
+- `wwwroot/app.css` - Global styles, buttons, color scheme
+- `Components/Pages/Chat/Chat.razor.css` - Chat container layout
+- `Components/Pages/Chat/ChatMessageItem.razor.css` - Message bubbles and icons
+- `Components/Pages/Chat/ChatInput.razor.css` - Input box styling
+
+### Disabling Suggestions
+
+To disable the AI-generated follow-up suggestions, comment out the suggestions component in `Chat.razor`:
+
+```razor
+@* *@
+```
diff --git a/dotnet/samples/AGUIWebChat/Server/AGUIWebChatServer.csproj b/dotnet/samples/AGUIWebChat/Server/AGUIWebChatServer.csproj
new file mode 100644
index 0000000..c45adfd
--- /dev/null
+++ b/dotnet/samples/AGUIWebChat/Server/AGUIWebChatServer.csproj
@@ -0,0 +1,21 @@
+
+
+