Compare commits

..

5 Commits

Author SHA1 Message Date
copilot-swe-agent[bot] 197788ef9e Add [1.8.1] changelog compare link and update [Unreleased] base 2026-06-09 20:05:32 +00:00
copilot-swe-agent[bot] a2d9b6c77d Remove incorrect agent-framework-foundry changelog entry for #6259 2026-06-09 18:37:20 +00:00
copilot-swe-agent[bot] 0f328177b1 Python: bump ag-ui and azurefunctions for 1.8.1 release 2026-06-09 18:14:41 +00:00
copilot-swe-agent[bot] cdb2ba56cc Python: bump agent-framework-foundry-hosting for 1.8.1 release 2026-06-09 18:12:45 +00:00
copilot-swe-agent[bot] 9b568d24cb Python: bump package versions for 1.8.1 release 2026-06-09 18:02:27 +00:00
3293 changed files with 106519 additions and 236877 deletions
+2 -6
View File
@@ -2,7 +2,6 @@ dirs:
- .
excludedFiles:
- ./python/CHANGELOG.md
- "**/SKILL.md"
ignorePatterns:
- pattern: "/github/"
- pattern: "./actions"
@@ -21,13 +20,10 @@ ignorePatterns:
- pattern: "https://your-resource.openai.azure.com/"
- pattern: "http://host.docker.internal"
- pattern: "https://openai.github.io/openai-agents-js/openai/agents/classes/"
# dotnet.microsoft.com bot-blocks CI link checkers with intermittent 403s on any
# path (including localized variants like /en-us/download/...), so ignore the
# whole domain rather than just /download.
- pattern: "https:\/\/dotnet.microsoft.com"
- pattern: "https:\/\/dotnet.microsoft.com\/download"
- pattern: "https://github.com/Rel1cx/eslint-react"
# excludedDirs:
# Folders which include links to localhost, since it's not ignored with regular expressions
# Folders which include links to localhost, since it's not ignored with regular expressions
baseUrl: https://github.com/microsoft/agent-framework/
aliveStatusCodes:
- 200
+4 -127
View File
@@ -1,130 +1,7 @@
# Code ownership assignments
# https://docs.github.com/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
#
# Policy: a PR needs one approval, and it must come from a code owner of the changed
# files ("Require review from Code Owners" + "Required approvals: 1" on `main`).
# A PR touching several CODEOWNERS patterns will request review from the applicable
# code owners, but an approval from any applicable code owner is sufficient to satisfy
# GitHub's required-code-owner review.
#
# Order matters: the LAST matching pattern wins, so a module rule fully replaces the
# catch-all rather than adding to it. @chetantoshniwal is included on every line as a
# repository-wide fallback owner. All owners on a line have equal approval authority.
#
# CONVENTION: owners are written in the order
# @chetantoshniwal <owner A> <owner B> [...]
# @chetantoshniwal is at the beginning for aesthetics. The owners share equal approval
# power and responsibility.
#
# RULE: every path must list at least two owners besides @chetantoshniwal. An author
# cannot approve their own PR, so a path with a single module owner leaves only Chetan
# to review whenever that owner is the author, which defeats the point of naming a
# module owner.
#
# Samples: owned by all core developers of that language, not by the module a sample
# demonstrates. Any core Python developer can approve any Python sample, and any core
# .NET developer can approve any .NET sample. No dedicated rule is needed -- samples
# fall through to the /python and /dotnet rules, which already list those developers.
#
# Tests: same as samples. Tests that live inside a package (python/packages/<pkg>/tests)
# are covered by that package's rule instead, since they sit under its path.
# Default owners for everything not matched by a module rule below.
* @chetantoshniwal @westey-m
# Repository-level paths: every core Agent Framework developer is a code owner, so any
# one of them can approve. Be explicit now and we can use the AgentFramework team in the future.
/docs/ @chetantoshniwal @westey-m @eavanvalkenburg @giles17 @moonbox3 @TaoChenOSU @peibekwe @rogerbarreto @SergeyMenshykh
/*.md @chetantoshniwal @westey-m @eavanvalkenburg @giles17 @moonbox3 @TaoChenOSU @peibekwe @rogerbarreto @SergeyMenshykh
/LICENSE @chetantoshniwal @westey-m @eavanvalkenburg @giles17 @moonbox3 @TaoChenOSU @peibekwe @rogerbarreto @SergeyMenshykh
/.gitattributes @chetantoshniwal @westey-m @eavanvalkenburg @giles17 @moonbox3 @TaoChenOSU @peibekwe @rogerbarreto @SergeyMenshykh
/.gitignore @chetantoshniwal @westey-m @eavanvalkenburg @giles17 @moonbox3 @TaoChenOSU @peibekwe @rogerbarreto @SergeyMenshykh
/.github @chetantoshniwal @westey-m @eavanvalkenburg @giles17 @moonbox3 @TaoChenOSU @peibekwe @rogerbarreto @SergeyMenshykh
# Repository-level paths that require specific owners
/.devcontainer @chetantoshniwal @westey-m @rogerbarreto @SergeyMenshykh
/declarative-agents @chetantoshniwal @moonbox3 @peibekwe
# Core Python developers: @eavanvalkenburg @giles17 @moonbox3 @TaoChenOSU
/python @chetantoshniwal @eavanvalkenburg @giles17 @moonbox3 @TaoChenOSU
# Python packages
/python/packages/a2a/ @chetantoshniwal @giles17 @eavanvalkenburg @moonbox3
/python/packages/ag-ui/ @chetantoshniwal @moonbox3 @giles17
/python/packages/anthropic/ @chetantoshniwal @eavanvalkenburg @giles17 @moonbox3
/python/packages/azure-ai-search/ @chetantoshniwal @eavanvalkenburg @giles17
/python/packages/azure-contentunderstanding/ @chetantoshniwal @giles17 @eavanvalkenburg
/python/packages/azure-cosmos/ @chetantoshniwal @eavanvalkenburg @giles17 @moonbox3
/python/packages/azure-cosmos-memory/ @chetantoshniwal @eavanvalkenburg @giles17 @moonbox3
/python/packages/bedrock/ @chetantoshniwal @eavanvalkenburg @giles17 @moonbox3
/python/packages/chatkit/ @chetantoshniwal @moonbox3 @giles17
/python/packages/claude/ @chetantoshniwal @eavanvalkenburg @giles17 @moonbox3
/python/packages/copilotstudio/ @chetantoshniwal @eavanvalkenburg @giles17 @moonbox3
/python/packages/core/ @chetantoshniwal @eavanvalkenburg @moonbox3 @TaoChenOSU @giles17
/python/packages/core/agent_framework/_workflows/ @chetantoshniwal @moonbox3 @TaoChenOSU
/python/packages/core/agent_framework/_harness/ @chetantoshniwal @westey-m @eavanvalkenburg @moonbox3
/python/packages/declarative/ @chetantoshniwal @moonbox3 @peibekwe
/python/packages/devui/ @chetantoshniwal @eavanvalkenburg @moonbox3
/python/packages/foundry/ @chetantoshniwal @eavanvalkenburg @TaoChenOSU @moonbox3 @giles17
/python/packages/foundry_hosting/ @chetantoshniwal @TaoChenOSU @eavanvalkenburg @moonbox3
/python/packages/foundry_local/ @chetantoshniwal @eavanvalkenburg @giles17 @moonbox3
/python/packages/gemini/ @chetantoshniwal @giles17 @eavanvalkenburg @moonbox3
/python/packages/github_copilot/ @chetantoshniwal @giles17 @eavanvalkenburg @moonbox3
/python/packages/hosting/ @chetantoshniwal @eavanvalkenburg @TaoChenOSU @moonbox3
/python/packages/hosting-a2a/ @chetantoshniwal @eavanvalkenburg @TaoChenOSU @moonbox3
/python/packages/hosting-mcp/ @chetantoshniwal @eavanvalkenburg @TaoChenOSU @moonbox3
/python/packages/hosting-responses/ @chetantoshniwal @eavanvalkenburg @TaoChenOSU @moonbox3
/python/packages/hosting-telegram/ @chetantoshniwal @eavanvalkenburg @TaoChenOSU @moonbox3
/python/packages/hyperlight/ @chetantoshniwal @eavanvalkenburg @giles17 @moonbox3
/python/packages/lab/ @chetantoshniwal @eavanvalkenburg @giles17 @moonbox3 @TaoChenOSU
/python/packages/mem0/ @chetantoshniwal @eavanvalkenburg @giles17 @moonbox3
/python/packages/mistral/ @chetantoshniwal @eavanvalkenburg @giles17 @moonbox3
/python/packages/monty/ @chetantoshniwal @eavanvalkenburg @giles17 @moonbox3
/python/packages/ollama/ @chetantoshniwal @eavanvalkenburg @giles17 @moonbox3
/python/packages/openai/ @chetantoshniwal @eavanvalkenburg @moonbox3 @TaoChenOSU @giles17
/python/packages/orchestrations/ @chetantoshniwal @moonbox3 @TaoChenOSU
/python/packages/purview/ @chetantoshniwal @eavanvalkenburg @giles17 @moonbox3
/python/packages/redis/ @chetantoshniwal @eavanvalkenburg @giles17 @moonbox3
/python/packages/tools/ @chetantoshniwal @eavanvalkenburg @giles17 @moonbox3
# Core .NET developers: @peibekwe @rogerbarreto @SergeyMenshykh @westey-m
/dotnet @chetantoshniwal @peibekwe @rogerbarreto @SergeyMenshykh @westey-m
# .NET projects
/dotnet/src/Aspire.Hosting.AgentFramework.DevUI/ @chetantoshniwal @peibekwe @rogerbarreto @SergeyMenshykh @westey-m
/dotnet/src/LegacySupport/ @chetantoshniwal @peibekwe @rogerbarreto @SergeyMenshykh @westey-m
/dotnet/src/Shared/ @chetantoshniwal @peibekwe @rogerbarreto @SergeyMenshykh @westey-m
/dotnet/src/Microsoft.Agents.AI/ @chetantoshniwal @rogerbarreto @SergeyMenshykh @westey-m
/dotnet/src/Microsoft.Agents.AI.A2A/ @chetantoshniwal @rogerbarreto @SergeyMenshykh @westey-m
/dotnet/src/Microsoft.Agents.AI.Abstractions/ @chetantoshniwal @rogerbarreto @SergeyMenshykh @westey-m
/dotnet/src/Microsoft.Agents.AI.AGUI/ @chetantoshniwal @peibekwe @rogerbarreto @SergeyMenshykh @westey-m
/dotnet/src/Microsoft.Agents.AI.Anthropic/ @chetantoshniwal @rogerbarreto @westey-m
/dotnet/src/Microsoft.Agents.AI.AzureAI.Persistent/ @chetantoshniwal @rogerbarreto @westey-m
/dotnet/src/Microsoft.Agents.AI.CopilotStudio/ @chetantoshniwal @rogerbarreto @westey-m
/dotnet/src/Microsoft.Agents.AI.CosmosNoSql/ @chetantoshniwal @rogerbarreto @westey-m
/dotnet/src/Microsoft.Agents.AI.Declarative/ @chetantoshniwal @peibekwe @westey-m
/dotnet/src/Microsoft.Agents.AI.DevUI/ @chetantoshniwal @peibekwe @rogerbarreto @SergeyMenshykh @westey-m
/dotnet/src/Microsoft.Agents.AI.Foundry/ @chetantoshniwal @rogerbarreto @westey-m
/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/ @chetantoshniwal @rogerbarreto @westey-m
/dotnet/src/Microsoft.Agents.AI.GitHub.Copilot/ @chetantoshniwal @rogerbarreto @westey-m
/dotnet/src/Microsoft.Agents.AI.Harness/ @chetantoshniwal @westey-m @SergeyMenshykh
/dotnet/src/Microsoft.Agents.AI.Hosting/ @chetantoshniwal @rogerbarreto @SergeyMenshykh @westey-m
/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/ @chetantoshniwal @rogerbarreto @SergeyMenshykh @westey-m
/dotnet/src/Microsoft.Agents.AI.Hosting.A2A.AspNetCore/ @chetantoshniwal @rogerbarreto @SergeyMenshykh @westey-m
/dotnet/src/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore/ @chetantoshniwal @rogerbarreto @SergeyMenshykh @westey-m
/dotnet/src/Microsoft.Agents.AI.Hosting.AspNetCore/ @chetantoshniwal @rogerbarreto @SergeyMenshykh @westey-m
/dotnet/src/Microsoft.Agents.AI.Hosting.AzureStorage/ @chetantoshniwal @rogerbarreto @SergeyMenshykh @westey-m
/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/ @chetantoshniwal @rogerbarreto @SergeyMenshykh @westey-m
/dotnet/src/Microsoft.Agents.AI.Hyperlight/ @chetantoshniwal @westey-m @SergeyMenshykh
/dotnet/src/Microsoft.Agents.AI.LocalCodeAct/ @chetantoshniwal @westey-m @SergeyMenshykh
/dotnet/src/Microsoft.Agents.AI.Mcp/ @chetantoshniwal @westey-m @peibekwe
/dotnet/src/Microsoft.Agents.AI.Mem0/ @chetantoshniwal @westey-m @SergeyMenshykh
/dotnet/src/Microsoft.Agents.AI.OpenAI/ @chetantoshniwal @westey-m @rogerbarreto
/dotnet/src/Microsoft.Agents.AI.Purview/ @chetantoshniwal @westey-m @SergeyMenshykh
/dotnet/src/Microsoft.Agents.AI.Tools.Shell/ @chetantoshniwal @westey-m @SergeyMenshykh
/dotnet/src/Microsoft.Agents.AI.Valkey/ @chetantoshniwal @westey-m @SergeyMenshykh
/dotnet/src/Microsoft.Agents.AI.Workflows/ @chetantoshniwal @peibekwe @rogerbarreto
/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ @chetantoshniwal @peibekwe @rogerbarreto
/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative.Foundry/ @chetantoshniwal @peibekwe @rogerbarreto
/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative.Mcp/ @chetantoshniwal @peibekwe @rogerbarreto
/dotnet/src/Microsoft.Agents.AI.Workflows.Generators/ @chetantoshniwal @peibekwe @rogerbarreto
python/packages/azurefunctions/ @microsoft/agentframework-durabletask-developers
python/packages/durabletask/ @microsoft/agentframework-durabletask-developers
python/samples/getting_started/azure_functions/ @microsoft/agentframework-durabletask-developers
python/samples/getting_started/durabletask/ @microsoft/agentframework-durabletask-developers
+1 -1
View File
@@ -1,7 +1,7 @@
name: .NET Bug Report
description: Report a bug in the Agent Framework .NET SDK
title: ".NET: [Bug]: "
labels: [".NET"]
labels: ["bug", ".NET"]
type: bug
body:
- type: textarea
+1 -1
View File
@@ -1,7 +1,7 @@
name: Python Bug Report
description: Report a bug in the Agent Framework Python SDK
title: "Python: [Bug]: "
labels: ["Python"]
labels: ["bug", "Python"]
type: bug
body:
- type: textarea
@@ -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
-112
View File
@@ -1,112 +0,0 @@
name: Get GitHub automation token
description: Creates a GitHub App installation token with a temporary PAT fallback
inputs:
mode:
description: Authentication mode (app, app-with-fallback, or pat)
required: false
default: app-with-fallback
azure-client-id:
description: Client ID of the Azure workload identity
required: false
azure-tenant-id:
description: Azure tenant ID
required: false
azure-subscription-id:
description: Azure subscription containing the Key Vault
required: false
key-vault-name:
description: Azure Key Vault name
required: false
key-name:
description: Key Vault key used to sign the GitHub App JWT
required: false
github-app-client-id:
description: GitHub App client ID
required: false
github-app-installation-id:
description: GitHub App installation ID
required: false
repository:
description: Repository to include in the installation token
required: false
fallback-token:
description: PAT used temporarily when app authentication is unavailable
required: false
outputs:
token:
description: GitHub App installation token or fallback PAT
value: ${{ steps.select-token.outputs.token }}
source:
description: Selected authentication source
value: ${{ steps.select-token.outputs.source }}
runs:
using: composite
steps:
- name: Validate authentication mode
shell: bash
env:
AUTH_MODE: ${{ inputs.mode || 'app-with-fallback' }}
run: |
if [[ "$AUTH_MODE" != "app" && "$AUTH_MODE" != "app-with-fallback" && "$AUTH_MODE" != "pat" ]]; then
echo "::error::Unsupported GitHub authentication mode."
exit 1
fi
- name: Sign in to Azure
id: azure-login
if: ${{ (inputs.mode || 'app-with-fallback') != 'pat' }}
continue-on-error: true
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
with:
client-id: ${{ inputs.azure-client-id }}
tenant-id: ${{ inputs.azure-tenant-id }}
subscription-id: ${{ inputs.azure-subscription-id }}
- name: Create GitHub App installation token
id: app-token
if: ${{ (inputs.mode || 'app-with-fallback') != 'pat' && steps.azure-login.outcome == 'success' }}
continue-on-error: true
shell: bash
env:
AZURE_SUBSCRIPTION_ID: ${{ inputs.azure-subscription-id }}
KEY_VAULT_NAME: ${{ inputs.key-vault-name }}
KEY_NAME: ${{ inputs.key-name }}
GITHUB_APP_CLIENT_ID: ${{ inputs.github-app-client-id }}
GITHUB_APP_INSTALLATION_ID: ${{ inputs.github-app-installation-id }}
TARGET_REPOSITORY: ${{ inputs.repository }}
run: |
token="$(node "$GITHUB_ACTION_PATH/create-token.js")"
echo "::add-mask::$token"
echo "token=$token" >> "$GITHUB_OUTPUT"
- name: Select authentication token
id: select-token
shell: bash
env:
AUTH_MODE: ${{ inputs.mode || 'app-with-fallback' }}
APP_TOKEN: ${{ steps.app-token.outputs.token }}
FALLBACK_TOKEN: ${{ inputs.fallback-token }}
run: |
if [[ "$AUTH_MODE" != "pat" && -n "$APP_TOKEN" ]]; then
token="$APP_TOKEN"
source="app"
echo "::notice::GitHub authentication source: app"
elif [[ "$AUTH_MODE" == "app-with-fallback" && -n "$FALLBACK_TOKEN" ]]; then
token="$FALLBACK_TOKEN"
source="pat-fallback"
echo "::warning::GitHub authentication source: PAT fallback"
elif [[ "$AUTH_MODE" == "pat" && -n "$FALLBACK_TOKEN" ]]; then
token="$FALLBACK_TOKEN"
source="pat-forced"
echo "::warning::GitHub authentication source: PAT (forced rollout mode)"
else
echo "::error::GitHub App authentication is unavailable and no fallback PAT was provided."
exit 1
fi
echo "::add-mask::$token"
echo "token=$token" >> "$GITHUB_OUTPUT"
echo "source=$source" >> "$GITHUB_OUTPUT"
@@ -1,133 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
const crypto = require('node:crypto');
const { execFileSync } = require('node:child_process');
function base64Url(value) {
return Buffer.from(value).toString('base64url');
}
function base64ToBase64Url(value) {
return Buffer.from(value, 'base64').toString('base64url');
}
function createJwtSigningInput(clientId, nowSeconds) {
const header = base64Url(JSON.stringify({ alg: 'RS256', typ: 'JWT' }));
const payload = base64Url(JSON.stringify({
iat: nowSeconds - 60,
exp: nowSeconds + 540,
iss: clientId,
}));
return `${header}.${payload}`;
}
function signJwt(signingInput, config, execute = execFileSync) {
const digest = crypto.createHash('sha256').update(signingInput).digest('base64');
const signature = execute(
'az',
[
'keyvault', 'key', 'sign',
'--subscription', config.azureSubscriptionId,
'--vault-name', config.keyVaultName,
'--name', config.keyName,
'--algorithm', 'RS256',
'--digest', digest,
'--query', 'signature',
'--output', 'tsv',
'--only-show-errors',
],
{ encoding: 'utf8' },
).trim();
if (!signature) {
throw new Error('Key Vault returned an empty signature.');
}
return `${signingInput}.${base64ToBase64Url(signature)}`;
}
async function createInstallationToken(config, dependencies = {}) {
const execute = dependencies.execute ?? execFileSync;
const request = dependencies.fetch ?? fetch;
const nowSeconds = dependencies.nowSeconds ?? Math.floor(Date.now() / 1000);
const repositoryParts = config.targetRepository.split('/');
if (repositoryParts.length !== 2 || repositoryParts.some((part) => part.length === 0)) {
throw new Error('TARGET_REPOSITORY must use the owner/repository format.');
}
const [, repository] = repositoryParts;
const signingInput = createJwtSigningInput(config.githubAppClientId, nowSeconds);
const jwt = signJwt(signingInput, config, execute);
const response = await request(
`https://api.github.com/app/installations/${config.githubAppInstallationId}/access_tokens`,
{
method: 'POST',
headers: {
Accept: 'application/vnd.github+json',
Authorization: `Bearer ${jwt}`,
'X-GitHub-Api-Version': '2022-11-28',
},
body: JSON.stringify({
repositories: [repository],
permissions: {
contents: 'read',
issues: 'write',
members: 'read',
pull_requests: 'write',
},
}),
},
);
if (!response.ok) {
throw new Error(`GitHub installation token request failed with HTTP ${response.status}.`);
}
const result = await response.json();
if (typeof result.token !== 'string' || result.token.length === 0) {
throw new Error('GitHub returned an empty installation token.');
}
return result.token;
}
function readConfig(environment) {
const config = {
azureSubscriptionId: environment.AZURE_SUBSCRIPTION_ID,
keyVaultName: environment.KEY_VAULT_NAME,
keyName: environment.KEY_NAME,
githubAppClientId: environment.GITHUB_APP_CLIENT_ID,
githubAppInstallationId: environment.GITHUB_APP_INSTALLATION_ID,
targetRepository: environment.TARGET_REPOSITORY,
};
if (Object.values(config).some((value) => !value)) {
throw new Error('Required GitHub App authentication configuration is missing.');
}
return config;
}
async function main() {
try {
const token = await createInstallationToken(readConfig(process.env));
process.stdout.write(token);
} catch {
console.error('GitHub App token generation failed.');
process.exitCode = 1;
}
}
if (require.main === module) {
void main();
}
module.exports = {
base64ToBase64Url,
createInstallationToken,
createJwtSigningInput,
readConfig,
signJwt,
};
+2 -2
View File
@@ -17,7 +17,7 @@ runs:
using: "composite"
steps:
- name: Set up uv
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
with:
version-file: "python/pyproject.toml"
enable-cache: true
@@ -46,4 +46,4 @@ runs:
- name: Install the project
shell: bash
run: |
cd python && uv sync --all-packages --all-extras --all-groups --prerelease=if-necessary-or-explicit
cd python && uv sync --all-packages --all-extras --dev --prerelease=if-necessary-or-explicit
@@ -1,19 +0,0 @@
name: Save Sample Playbooks
description: >
Save the cached sample-validation playbooks. Split out from
sample-validation-setup (which only restores) so the save runs even when the
validation step fails. Combining restore+save via actions/cache would skip the
save on a failing job (post-if: success()), so freshly authored playbooks for
samples that failed validation would never persist. Invoke this with
'if: not-cancelled' after the validation step in each job.
runs:
using: "composite"
steps:
- name: Save sample playbooks cache
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
# Must match the restore path/key in sample-validation-setup/action.yml and the
# sample_validation --playbooks-dir default (samples/sample_validation/playbooks).
path: python/samples/sample_validation/playbooks/
key: sample-playbooks-${{ github.job }}-${{ github.run_id }}
@@ -24,7 +24,7 @@ runs:
using: "composite"
steps:
- name: Set up Node.js environment
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 22
@@ -36,31 +36,15 @@ runs:
shell: bash
run: copilot --version && copilot -p "What can you do in one sentence?"
- name: Azure CLI Login
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2
with:
client-id: ${{ inputs.azure-client-id }}
tenant-id: ${{ inputs.azure-tenant-id }}
subscription-id: ${{ inputs.azure-subscription-id }}
- name: Set up python and install the project
uses: ./.github/actions/python-setup
with:
python-version: ${{ inputs.python-version }}
os: ${{ inputs.os }}
- name: Restore sample playbooks
# Restore-only. The matching save is a separate step in each job that runs with
# `if: ${{ !cancelled() }}` (see .github/actions/sample-validation-save-playbooks).
# A combined actions/cache would skip its post-job save on a failing job
# (post-if: success()), so playbooks authored for samples that failed validation
# would never persist. Keyed per job so each validate-* job keeps its own playbooks.
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
# Must match the sample_validation --playbooks-dir default, which resolves to
# samples/sample_validation/playbooks (see python/scripts/sample_validation/__main__.py).
# If a job overrides --playbooks-dir, update this path to match.
path: python/samples/sample_validation/playbooks/
key: sample-playbooks-${{ github.job }}-${{ github.run_id }}
restore-keys: |
sample-playbooks-${{ github.job }}-
- name: Azure CLI Login
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
with:
client-id: ${{ inputs.azure-client-id }}
tenant-id: ${{ inputs.azure-tenant-id }}
subscription-id: ${{ inputs.azure-subscription-id }}
+3 -7
View File
@@ -24,14 +24,12 @@ updates:
- ".NET"
- "dependencies"
# Maintain dependencies for python.
# TODO: Remove these Python Dependabot entries after we have confidence in the
# Python dependency-maintenance workflow.
# Maintain dependencies for python
- package-ecosystem: "pip"
directory: "python/"
schedule:
interval: "weekly"
day: "thursday"
day: "monday"
labels:
- "python"
- "dependencies"
@@ -39,7 +37,7 @@ updates:
directory: "python/"
schedule:
interval: "weekly"
day: "thursday"
day: "monday"
labels:
- "python"
- "dependencies"
@@ -58,5 +56,3 @@ updates:
schedule:
interval: "weekly"
day: "sunday"
cooldown:
default-days: 7
@@ -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]".
+5 -25
View File
@@ -1,43 +1,23 @@
### Motivation & Context
### Motivation and Context
<!-- Thank you for your contribution to the Agent Framework repo!
Please help reviewers and future users, providing the following information:
1. Why is this change required?
2. What problem does it solve?
3. What scenario does it contribute to?
4. If it fixes an open issue, please link to the issue below.
4. If it fixes an open issue, please link to the issue here.
-->
### Description & Review Guide
### Description
<!-- Describe your changes, the overall approach, the underlying design.
Highlight what you want the reviewers to focus on.
These notes will help understanding how your code works. Thanks! -->
- **What are the major changes?**
- **What is the impact of these changes?**
- **What do you want reviewers to focus on?**
<!-- NOTE FOR AUTOMATED/AI REVIEWERS: the "What do you want reviewers to focus on?"
item above is intended for human reviewers only. Automated/AI reviewers should
ignore it and review the entire change rather than narrowing scope to it. -->
### Related Issue
<!-- Which issue does this PR fix? Link it using a GitHub closing keyword so it is
closed automatically when this PR is merged, e.g. "Fixes #123" or "Closes #123".
PRs that are not linked to an issue may be closed, no matter how valid the change is.
Also check whether an open PR already exists for this issue; if so,
explain how this PR is different. -->
Fixes #
### Contribution Checklist
<!-- Before submitting this PR, please make sure: -->
- [ ] The code builds clean without any errors or warnings
- [ ] All unit tests pass, and I have added new tests where possible
- [ ] The PR follows the [Contribution Guidelines](https://github.com/microsoft/agent-framework/blob/main/CONTRIBUTING.md)
- [ ] This PR is linked to an issue and there is no other open PR for this issue (see Related Issue above).
- [x] **This is not a breaking change.** If it _is_ a breaking change, add the `breaking change` label (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.
- [ ] 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.
+10 -25
View File
@@ -1,40 +1,25 @@
// Copyright (c) Microsoft. All rights reserved.
/**
* Resolve the issue or pull request author and check their team membership.
* Resolve the issue author and check their team membership.
*
* @param {object} opts
* @param {object} opts.github - Octokit REST client from actions/github-script
* @param {object} opts.context - GitHub Actions context
* @param {object} opts.core - GitHub Actions core toolkit
* @param {string} opts.teamSlug - Team slug to check membership against
* @param {string|number} opts.issueNumber - Issue or pull request number to resolve author for
* @param {string} [opts.username] - Explicit user to check instead of the issue or pull request author
* @param {string|number} opts.issueNumber - Issue number to resolve author for
* @returns {Promise<{author: string|null, isTeamMember: boolean}>}
*/
async function checkTeamMembership({ github, context, core, teamSlug, issueNumber, username = '' }) {
let author = username.trim() || (
context.payload.issue?.user?.login ??
context.payload.pull_request?.user?.login
);
async function checkTeamMembership({ github, context, core, teamSlug, issueNumber }) {
let author = context.payload.issue?.user?.login;
if (!author) {
const number = Number(issueNumber);
if (context.payload.pull_request) {
const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: number,
});
author = pr.user?.login;
} else {
const { data: issue } = await github.rest.issues.get({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: number,
});
author = issue.user?.login;
}
const { data: issue } = await github.rest.issues.get({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: Number(issueNumber),
});
author = issue.user?.login;
}
if (!author) {
-212
View File
@@ -1,212 +0,0 @@
# Copyright (c) Microsoft. All rights reserved.
"""Enforce Python package coverage according to package lifecycle."""
# ruff:file-ignore[print]
# ruff:file-ignore[implicit-namespace-package]
from __future__ import annotations
import re
import sys
import xml.etree.ElementTree as ET # ruff:ignore[suspicious-xml-etree-import]
from dataclasses import dataclass
from pathlib import Path
import tomllib
DEVELOPMENT_STATUS_PREFIX = "Development Status :: "
ENFORCED_DEVELOPMENT_STATUS = 4
EXEMPT_PACKAGES = {"devui", "lab"}
@dataclass(frozen=True)
class PackagePolicy:
"""Coverage policy derived from a package's project metadata."""
directory: str
distribution_name: str
development_status: int
development_status_label: str
enforced: bool
exempt: bool
@dataclass
class CoverageStats:
"""Line and branch coverage counters."""
lines_valid: int = 0
lines_covered: int = 0
branches_valid: int = 0
branches_covered: int = 0
@property
def line_coverage_percent(self) -> float:
"""Return line coverage as a percentage."""
if not self.lines_valid:
return 0
return self.lines_covered / self.lines_valid * 100
def normalize_coverage_path(path: str) -> str:
"""Normalize a coverage path for matching."""
return path.replace("\\", "/").lstrip("./")
def load_package_policies(packages_dir: Path) -> list[PackagePolicy]:
"""Load lifecycle-based coverage policies from package pyproject files."""
policies: list[PackagePolicy] = []
for pyproject_path in sorted(packages_dir.glob("*/pyproject.toml")):
with pyproject_path.open("rb") as pyproject_file:
pyproject = tomllib.load(pyproject_file)
project = pyproject.get("project", {})
distribution_name = str(project.get("name", "")).strip()
if not distribution_name:
raise ValueError(f"{pyproject_path}: project.name is required")
status_classifiers = [
classifier
for classifier in project.get("classifiers", [])
if classifier.startswith(DEVELOPMENT_STATUS_PREFIX)
]
if len(status_classifiers) != 1:
raise ValueError(
f"{pyproject_path}: expected exactly one Development Status classifier, found {len(status_classifiers)}"
)
match = re.fullmatch(r"Development Status :: (\d+) - (.+)", status_classifiers[0])
if match is None:
raise ValueError(f"{pyproject_path}: malformed Development Status classifier")
directory = pyproject_path.parent.name
development_status = int(match.group(1))
exempt = directory in EXEMPT_PACKAGES
policies.append(
PackagePolicy(
directory=directory,
distribution_name=distribution_name,
development_status=development_status,
development_status_label=match.group(2),
enforced=development_status >= ENFORCED_DEVELOPMENT_STATUS and not exempt,
exempt=exempt,
)
)
if not policies:
raise ValueError(f"No package pyproject.toml files found below {packages_dir}")
return policies
def parse_coverage_xml(xml_path: Path) -> tuple[dict[str, CoverageStats], float, float]:
"""Parse Cobertura XML and aggregate coverage by package directory."""
root = ET.parse(xml_path).getroot() # ruff:ignore[suspicious-xml-element-tree-usage] # Trusted CI-generated coverage report.
package_stats: dict[str, CoverageStats] = {}
for class_elem in root.findall(".//class"):
file_path = normalize_coverage_path(class_elem.get("filename", ""))
path_parts = file_path.split("/")
try:
packages_index = path_parts.index("packages")
package_directory = path_parts[packages_index + 1]
except (ValueError, IndexError):
continue
stats = package_stats.setdefault(package_directory, CoverageStats())
for line in class_elem.findall(".//line"):
stats.lines_valid += 1
if int(line.get("hits", 0)) > 0:
stats.lines_covered += 1
if line.get("branch") != "true":
continue
condition_coverage = line.get("condition-coverage", "")
match = re.search(r"\((\d+)/(\d+)\)", condition_coverage)
if match is not None:
stats.branches_covered += int(match.group(1))
stats.branches_valid += int(match.group(2))
return (
package_stats,
float(root.get("line-rate", 0)) * 100,
float(root.get("branch-rate", 0)) * 100,
)
def check_coverage(xml_path: Path, threshold: float, packages_dir: Path) -> bool:
"""Check all lifecycle-enforced packages against the coverage threshold."""
policies = load_package_policies(packages_dir)
package_stats, overall_line_coverage, overall_branch_coverage = parse_coverage_xml(xml_path)
print("\n" + "=" * 110)
print("PYTHON PACKAGE TEST COVERAGE")
print("=" * 110)
print(f"Overall Line Coverage: {overall_line_coverage:.1f}%")
print(f"Overall Branch Coverage: {overall_branch_coverage:.1f}%")
print(f"Enforced Threshold: {threshold:.1f}%")
print("-" * 110)
print(f"{'Package':<48} {'Stage':<20} {'Policy':<14} {'Lines':<12} {'Line Cov':<10}")
print("-" * 110)
failed_packages: list[str] = []
for policy in sorted(policies, key=lambda item: (not item.enforced, item.distribution_name)):
stats = package_stats.get(policy.directory)
if policy.exempt:
policy_label = "EXEMPT"
elif policy.enforced:
policy_label = "ENFORCED"
else:
policy_label = "REPORT ONLY"
if stats is None:
lines = "-"
coverage = "missing"
if policy.enforced:
failed_packages.append(f"{policy.distribution_name} (missing from coverage report)")
else:
lines = f"{stats.lines_covered}/{stats.lines_valid}"
coverage = f"{stats.line_coverage_percent:.1f}%"
if policy.enforced and stats.line_coverage_percent < threshold:
failed_packages.append(f"{policy.distribution_name} ({coverage})")
stage = f"{policy.development_status} - {policy.development_status_label}"
print(f"{policy.distribution_name:<48} {stage:<20} {policy_label:<14} {lines:<12} {coverage:<10}")
print("-" * 110)
if failed_packages:
print(f"\nFAILED: Enforced packages below {threshold:.1f}% or missing:")
for package in failed_packages:
print(f" - {package}")
return False
print(f"\nPASSED: All non-exempt Beta-or-higher packages meet {threshold:.1f}% line coverage.")
return True
def main() -> int:
"""Run the coverage policy check."""
if len(sys.argv) != 3:
print(f"Usage: {sys.argv[0]} <coverage-xml-path> <threshold>")
return 1
try:
threshold = float(sys.argv[2])
except ValueError:
print(f"Error: Invalid threshold value: {sys.argv[2]}")
return 1
repository_root = Path(__file__).resolve().parents[2]
try:
passed = check_coverage(
Path(sys.argv[1]),
threshold,
repository_root / "python" / "packages",
)
except (FileNotFoundError, ET.ParseError, ValueError) as error:
print(f"Error: {error}")
return 1
return 0 if passed else 1
if __name__ == "__main__":
sys.exit(main())
@@ -1,170 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
const DECISIVE_REVIEW_STATES = new Set(['APPROVED', 'CHANGES_REQUESTED', 'DISMISSED']);
const SHA_PATTERN = /^[0-9a-f]{40}$/;
const BRANCH_PATTERN = /^[a-zA-Z0-9_./-]+$/;
function assertValidSha(sha, description) {
if (!SHA_PATTERN.test(sha)) {
throw new Error(`GitHub returned an invalid ${description} SHA.`);
}
}
function hasWritePermission(permissionData) {
return permissionData.user?.permissions?.push === true
|| ['admin', 'maintain', 'write'].includes(permissionData.permission);
}
function latestDecisiveReviews(reviews) {
const latestByReviewer = new Map();
const sortedReviews = [...reviews].sort((left, right) => {
const submittedComparison = (left.submitted_at || '').localeCompare(right.submitted_at || '');
return submittedComparison || Number(left.id) - Number(right.id);
});
for (const review of sortedReviews) {
const state = review.state?.toUpperCase();
const reviewer = review.user?.login?.toLowerCase();
if (reviewer && DECISIVE_REVIEW_STATES.has(state)) {
latestByReviewer.set(reviewer, review);
}
}
return latestByReviewer;
}
async function resolvePullRequest({ github, context, core, prNumber, requiredApprovals }) {
if (!/^[0-9]+$/.test(prNumber)) {
throw new Error('Invalid PR number. Only numeric values are allowed.');
}
const pullNumber = Number(prNumber);
const { data: pullRequest } = await github.rest.pulls.get({
...context.repo,
pull_number: pullNumber,
});
if (pullRequest.state !== 'open') {
throw new Error(`PR #${pullNumber} is not open (state: ${pullRequest.state}).`);
}
const headSha = pullRequest.head.sha;
const baseSha = pullRequest.base.sha;
assertValidSha(headSha, 'PR head');
assertValidSha(baseSha, 'PR base');
const reviews = await github.paginate(github.rest.pulls.listReviews, {
...context.repo,
pull_number: pullNumber,
per_page: 100,
});
const latestReviews = latestDecisiveReviews(reviews);
const author = pullRequest.user?.login?.toLowerCase();
const approvalCandidates = [...latestReviews.entries()]
.filter(([, review]) => review.state.toUpperCase() === 'APPROVED')
.filter(([, review]) => review.commit_id === headSha)
.filter(([reviewer]) => reviewer !== author);
const approvedMaintainers = [];
for (const [reviewer] of approvalCandidates) {
const { data: permissionData } = await github.rest.repos.getCollaboratorPermissionLevel({
...context.repo,
username: reviewer,
});
if (hasWritePermission(permissionData)) {
approvedMaintainers.push(reviewer);
} else {
core.info(`Ignoring approval from ${reviewer}: reviewer does not have write permission.`);
}
}
if (approvedMaintainers.length < requiredApprovals) {
throw new Error(
`PR #${pullNumber} head ${headSha} requires ${requiredApprovals} approvals from unique `
+ `write-capable maintainers; found ${approvedMaintainers.length}.`,
);
}
core.info(
`PR #${pullNumber} head ${headSha} approved by: ${approvedMaintainers.join(', ')}.`,
);
return {
baseRef: baseSha,
checkoutRef: headSha,
description: `PR #${pullNumber}`,
};
}
async function resolveBranch({ github, context, core, branch }) {
if (!BRANCH_PATTERN.test(branch)) {
throw new Error(
'Invalid branch name. Only alphanumeric characters, hyphens, underscores, dots, and slashes '
+ 'are allowed.',
);
}
const [{ data: repository }, { data: targetBranch }] = await Promise.all([
github.rest.repos.get(context.repo),
github.rest.repos.getBranch({ ...context.repo, branch }),
]);
const { data: baseBranch } = await github.rest.repos.getBranch({
...context.repo,
branch: repository.default_branch,
});
const checkoutRef = targetBranch.commit.sha;
const baseRef = baseBranch.commit.sha;
assertValidSha(checkoutRef, 'branch head');
assertValidSha(baseRef, 'default branch');
core.info(`Branch ${branch} resolved to immutable commit ${checkoutRef}.`);
return {
baseRef,
checkoutRef,
description: `branch ${branch}`,
};
}
/**
* Resolve a manually requested integration-test target to an immutable commit.
*
* Pull requests must have fresh approvals from two unique write-capable
* maintainers for the exact head commit. Branches are limited to branches in
* the base repository and are pinned to their current commit.
*/
async function resolveIntegrationTestTarget({
github,
context,
core,
prNumber = '',
branch = '',
requiredApprovals = 2,
}) {
const normalizedPrNumber = prNumber.trim();
const normalizedBranch = branch.trim();
if (normalizedPrNumber && normalizedBranch) {
throw new Error('Please provide either a PR number or a branch name, not both.');
}
if (!normalizedPrNumber && !normalizedBranch) {
throw new Error('Please provide either a PR number or a branch name.');
}
if (normalizedPrNumber) {
return resolvePullRequest({
github,
context,
core,
prNumber: normalizedPrNumber,
requiredApprovals,
});
}
return resolveBranch({
github,
context,
core,
branch: normalizedBranch,
});
}
module.exports = resolveIntegrationTestTarget;
-13
View File
@@ -1,13 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
/**
* Check whether a comment contains only the DevFlow review command.
*
* @param {unknown} body - Issue comment body from the GitHub event payload.
* @returns {boolean} Whether the normalized comment is exactly `/review`.
*/
function isReviewCommand(body) {
return typeof body === 'string' && body.trim() === '/review';
}
module.exports = isReviewCommand;
-253
View File
@@ -1,253 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
const BREAKING_CHANGE_LABEL = 'breaking change';
const BREAKING_PREFIX = '[BREAKING]';
const DEFAULT_PREFIX_LABELS = Object.freeze({
python: 'Python',
'.NET': '.NET',
});
const DEFAULT_BRACKET_PREFIX_LABELS = Object.freeze({
[BREAKING_CHANGE_LABEL]: BREAKING_PREFIX,
});
function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function getMatchingValueByKey(valuesByKey, keyToFind) {
const matchingKey = Object.keys(valuesByKey).find((key) => key.toLowerCase() === keyToFind.toLowerCase());
return matchingKey === undefined ? null : valuesByKey[matchingKey];
}
function getPrefixPattern(prefixes) {
return prefixes.map(escapeRegExp).join('|');
}
function canonicalizePrefix(prefix, prefixes) {
return prefixes.find((knownPrefix) => knownPrefix.toLowerCase() === prefix.toLowerCase()) ?? prefix;
}
function normalizeLeadingBracketPrefix(title, bracketPrefixes) {
const bracketPattern = getPrefixPattern(bracketPrefixes);
if (!bracketPattern) {
return title;
}
const leadingBracketPrefix = new RegExp(`^(${bracketPattern})(?=\\s|$)`, 'i');
return title.replace(
leadingBracketPrefix,
(bracketPrefix) => canonicalizePrefix(bracketPrefix, bracketPrefixes),
);
}
function parseLeadingTitlePrefix(title, titlePrefixes) {
const titlePrefixPattern = getPrefixPattern(titlePrefixes);
if (!titlePrefixPattern) {
return null;
}
const match = title.match(new RegExp(`^(${titlePrefixPattern}):\\s*`, 'i'));
if (!match) {
return null;
}
return {
prefix: canonicalizePrefix(match[1], titlePrefixes),
rest: title.slice(match[0].length).trimStart(),
};
}
function removeBracketPrefixToken(title, bracketPrefix) {
const bracketPrefixPattern = escapeRegExp(bracketPrefix);
return title
.replace(new RegExp(`(^|\\s+)${bracketPrefixPattern}(?=\\s|$)`, 'ig'), '$1')
.replace(/\s{2,}/g, ' ')
.trim();
}
function addTitlePrefix(title, prefix, bracketPrefixes = Object.values(DEFAULT_BRACKET_PREFIX_LABELS)) {
const bracketPattern = getPrefixPattern(bracketPrefixes);
const prefixPattern = escapeRegExp(prefix);
if (bracketPattern) {
const bracketThenTitlePrefix = new RegExp(`^(${bracketPattern})(\\s+)(${prefixPattern})(?=:)`, 'i');
if (bracketThenTitlePrefix.test(title)) {
return title.replace(
bracketThenTitlePrefix,
(match, bracketPrefix, spacing) => `${canonicalizePrefix(bracketPrefix, bracketPrefixes)}${spacing}${prefix}`,
);
}
title = normalizeLeadingBracketPrefix(title, bracketPrefixes);
}
if (!title.startsWith(`${prefix}: `)) {
const existingTitlePrefix = new RegExp(`^${prefixPattern}:\\s*`, 'i');
if (existingTitlePrefix.test(title)) {
return title.replace(existingTitlePrefix, `${prefix}: `);
}
return `${prefix}: ${title}`;
}
return title;
}
function hasBracketPrefix(title, bracketPrefix, titlePrefixes = Object.values(DEFAULT_PREFIX_LABELS)) {
const bracketPrefixPattern = escapeRegExp(bracketPrefix);
const leadingBracketPrefix = new RegExp(`^${bracketPrefixPattern}(?=\\s|$)`, 'i');
if (leadingBracketPrefix.test(title)) {
return true;
}
const leadingTitlePrefix = parseLeadingTitlePrefix(title, titlePrefixes);
if (!leadingTitlePrefix) {
return false;
}
return leadingBracketPrefix.test(leadingTitlePrefix.rest);
}
function addBracketPrefix(title, bracketPrefix, titlePrefixes = Object.values(DEFAULT_PREFIX_LABELS)) {
const bracketPrefixPattern = escapeRegExp(bracketPrefix);
const leadingBracketPrefix = new RegExp(`^${bracketPrefixPattern}(?=\\s|$)`, 'i');
if (leadingBracketPrefix.test(title)) {
return title.replace(leadingBracketPrefix, bracketPrefix);
}
const leadingTitlePrefix = parseLeadingTitlePrefix(title, titlePrefixes);
if (leadingTitlePrefix) {
if (leadingBracketPrefix.test(leadingTitlePrefix.rest)) {
const normalizedRest = leadingTitlePrefix.rest.replace(leadingBracketPrefix, bracketPrefix);
return `${leadingTitlePrefix.prefix}: ${normalizedRest}`;
}
const titleWithoutBracketPrefix = removeBracketPrefixToken(leadingTitlePrefix.rest, bracketPrefix);
return `${leadingTitlePrefix.prefix}: ${bracketPrefix}`
+ (titleWithoutBracketPrefix ? ` ${titleWithoutBracketPrefix}` : '');
}
const titleWithoutBracketPrefix = removeBracketPrefixToken(title, bracketPrefix);
return `${bracketPrefix}${titleWithoutBracketPrefix ? ` ${titleWithoutBracketPrefix}` : ''}`;
}
function hasLabel(labels, labelName) {
return labels.some((label) => label.toLowerCase() === labelName.toLowerCase());
}
function getCurrentTitle(context) {
switch (context.eventName) {
case 'issues':
return context.payload.issue.title;
case 'pull_request_target':
return context.payload.pull_request.title;
default:
throw new Error(`Unrecognized eventName: ${context.eventName}`);
}
}
async function updateTitleForAddedLabel({
github,
context,
core,
prefixLabels = DEFAULT_PREFIX_LABELS,
bracketPrefixLabels = DEFAULT_BRACKET_PREFIX_LABELS,
}) {
const labelAdded = context.payload.label?.name;
if (!labelAdded) {
throw new Error('This script must be run from a labeled event.');
}
const currentTitle = getCurrentTitle(context);
let newTitle = null;
const titlePrefix = getMatchingValueByKey(prefixLabels, labelAdded);
if (titlePrefix !== null) {
newTitle = addTitlePrefix(currentTitle, titlePrefix, Object.values(bracketPrefixLabels));
}
const bracketPrefix = getMatchingValueByKey(bracketPrefixLabels, labelAdded);
if (bracketPrefix !== null) {
newTitle = addBracketPrefix(currentTitle, bracketPrefix, Object.values(prefixLabels));
}
if (newTitle === null) {
core.info(`No title prefix configured for label "${labelAdded}".`);
return { updated: false, newTitle: currentTitle };
}
if (newTitle === currentTitle) {
core.info(`Title already includes the prefix for label "${labelAdded}".`);
return { updated: false, newTitle };
}
switch (context.eventName) {
case 'issues':
await github.rest.issues.update({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
title: newTitle,
});
break;
case 'pull_request_target':
await github.rest.pulls.update({
pull_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
title: newTitle,
});
break;
default:
throw new Error(`Unrecognized eventName: ${context.eventName}`);
}
return { updated: true, newTitle };
}
async function syncBreakingChangeLabelFromTitle({
github,
context,
core,
labelName = BREAKING_CHANGE_LABEL,
bracketPrefix = BREAKING_PREFIX,
titlePrefixes = Object.values(DEFAULT_PREFIX_LABELS),
}) {
const pullRequest = context.payload.pull_request;
if (!pullRequest) {
throw new Error('This script must be run from a pull_request_target event.');
}
const title = pullRequest.title || '';
if (!hasBracketPrefix(title, bracketPrefix, titlePrefixes)) {
core.info(`Title does not include ${bracketPrefix} in the title prefix.`);
return { added: false };
}
const labels = pullRequest.labels?.map((label) => label.name).filter(Boolean) ?? [];
if (hasLabel(labels, labelName)) {
core.info(`PR already has the "${labelName}" label.`);
return { added: false };
}
await github.rest.issues.addLabels({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: [labelName],
});
return { added: true };
}
module.exports = {
addBracketPrefix,
addTitlePrefix,
hasBracketPrefix,
syncBreakingChangeLabelFromTitle,
updateTitleForAddedLabel,
};
-119
View File
@@ -1,119 +0,0 @@
---
name: pull-requests
description: >
Guidance for creating pull requests and handling PR review comments in the
Agent Framework repository. Use this when writing a PR description (filling out
the PR template) or when responding to and resolving review comments on an
existing PR.
---
# Pull Request Workflow
This skill covers two tasks: (1) writing a high-quality PR description, and
(2) handling review comments on an existing PR.
## 1. Writing the PR description
Always follow the repository PR template at
[`.github/pull_request_template.md`](../../pull_request_template.md). Keep its
exact structure and headings. Fill every section:
### `### Motivation & Context`
Explain *why* the change is needed: the problem it solves and the scenario it
contributes to. Describe the net change relative to `main` — this is implied, so
do **not** spell out "vs main" explicitly.
### `### Description & Review Guide`
Describe the changes, the overall approach, and the design. Answer the three
prompts:
- **What are the major changes?**
- **What is the impact of these changes?**
- **What do you want reviewers to focus on?** — This item is for **human
reviewers only**. Automated/AI reviewers must ignore it and review the entire
change rather than narrowing scope to it.
### `### Related Issue`
Link the issue the PR fixes using a GitHub closing keyword (`Fixes #123` /
`Closes #123`) so it closes automatically on merge. A PR with no linked issue may
be closed regardless of how valid the change is. Before opening, confirm there is
no other open PR for the same issue; if there is, explain how this PR differs.
### `### Contribution Checklist`
Check every item that applies. For the breaking-change item:
- Leave **"This is not a breaking change."** checked for the common case.
- If the change **is** breaking, add the `breaking change` label **or** put
`[BREAKING]` in the title prefix, before or after a language prefix such as
`Python:` or `.NET:` — workflows keep the label and the title prefix in sync
automatically (see `.github/workflows/label-title-prefix.yml` and
`.github/workflows/label-pr.yml`).
### Do not
- Do **not** add ad-hoc sections such as "Validation" or "Tests run"; CI/CD and
the checklist already cover validation status.
- Do **not** remove or reorder the template's headings.
### Creating the PR
Open new PRs as **drafts** until they are ready for review. Example:
```bash
gh pr create --repo microsoft/agent-framework --base main \
--head <your-fork-owner>:<branch> --draft \
--title "<concise title>" --body "<body following the template>"
```
## 2. Handling review comments
When a PR receives review comments, follow this sequence — **do not start editing
code before the user has reviewed the plan**:
1. **Review the comments.** Read every review comment and thread on the PR,
including inline code comments and general review summaries.
2. **Make a plan.** Produce a concrete plan describing how each comment will be
addressed (or why it should not be, with reasoning).
3. **Let the user review the plan.** Present the plan and wait for the user's
approval or adjustments before implementing anything.
4. **Implement.** Make the agreed changes.
5. **Reply to every comment.** Add a reply to **all** comments explaining how it
was addressed, preferably citing the commit containing the change. If the
feedback was not addressed, explain why. Leave no comment unanswered.
6. **Resolve completed threads yourself.** After replying and completing any
necessary discussion, resolve the review thread. Do not wait for the reviewer
or a maintainer to resolve it. Leave a thread open only while it has an
unanswered question or active discussion.
### Useful commands
List review comments and threads:
```bash
# Inline review comments
gh api repos/{owner}/{repo}/pulls/{pr}/comments
# Review threads with resolution state (GraphQL)
gh api graphql -f query='
query($owner:String!,$repo:String!,$pr:Int!){
repository(owner:$owner,name:$repo){
pullRequest(number:$pr){
reviewThreads(first:100){
nodes{ id isResolved comments(first:50){ nodes{ id body author{login} } } }
}
}
}
}' -F owner={owner} -F repo={repo} -F pr={pr}
```
Reply to an inline review comment:
```bash
gh api repos/{owner}/{repo}/pulls/{pr}/comments/{comment_id}/replies \
-f body="Addressed in <commit>: <explanation>"
```
Resolve a review thread (needs the thread node id from the GraphQL query above):
```bash
gh api graphql -f query='
mutation($threadId:ID!){
resolveReviewThread(input:{threadId:$threadId}){ thread{ isResolved } }
}' -F threadId={thread_id}
```
+2 -73
View File
@@ -16,12 +16,7 @@ const checkTeamMembership = require('../scripts/check_team_membership.js');
// Helpers
// ---------------------------------------------------------------------------
function createMocks({
payloadIssue = undefined,
payloadPullRequest = undefined,
apiUser = 'api-user',
teamState = 'active',
} = {}) {
function createMocks({ payloadIssue = undefined, apiUser = 'api-user', teamState = 'active' } = {}) {
const core = {
_infoMessages: [],
_failedMessages: [],
@@ -29,16 +24,8 @@ function createMocks({
setFailed(msg) { this._failedMessages.push(msg); },
};
const payload = {};
if (payloadIssue !== undefined) {
payload.issue = payloadIssue;
}
if (payloadPullRequest !== undefined) {
payload.pull_request = payloadPullRequest;
}
const context = {
payload,
payload: { issue: payloadIssue },
repo: { owner: 'test-org', repo: 'test-repo' },
};
@@ -49,11 +36,6 @@ function createMocks({
data: { user: apiUser ? { login: apiUser } : null },
}),
},
pulls: {
get: async () => ({
data: { user: apiUser ? { login: apiUser } : null },
}),
},
teams: {
getByName: async () => ({}),
getMembershipForUserInOrg: async () => ({
@@ -74,28 +56,6 @@ const BASE_OPTS = { teamSlug: 'my-team', issueNumber: '123' };
// ---------------------------------------------------------------------------
describe('author resolution', () => {
it('uses an explicit username instead of the issue author', async () => {
const { github, context, core } = createMocks({
payloadIssue: { user: { login: 'issue-author' } },
});
let issuesGetCalled = false;
github.rest.issues.get = async () => {
issuesGetCalled = true;
return { data: { user: { login: 'api-user' } } };
};
const result = await checkTeamMembership({
github,
context,
core,
...BASE_OPTS,
username: 'comment-author',
});
assert.equal(result.author, 'comment-author');
assert.equal(issuesGetCalled, false);
});
it('resolves author from event payload', async () => {
const { github, context, core } = createMocks({
payloadIssue: { user: { login: 'payload-user' } },
@@ -104,37 +64,6 @@ describe('author resolution', () => {
assert.equal(result.author, 'payload-user');
});
it('resolves author from pull_request event payload', async () => {
const { github, context, core } = createMocks({
payloadPullRequest: { user: { login: 'pr-author' } },
});
let issuesGetCalled = false;
github.rest.issues.get = async () => {
issuesGetCalled = true;
return { data: { user: { login: 'api-user' } } };
};
const result = await checkTeamMembership({ github, context, core, ...BASE_OPTS });
assert.equal(result.author, 'pr-author');
assert.equal(issuesGetCalled, false);
});
it('resolves author via pulls API when pull_request payload user is null', async () => {
const { github, context, core } = createMocks({
payloadPullRequest: { user: null },
apiUser: 'fetched-pr-author',
});
let pullsGetCalled = false;
github.rest.pulls.get = async () => {
pullsGetCalled = true;
return { data: { user: { login: 'fetched-pr-author' } } };
};
const result = await checkTeamMembership({ github, context, core, ...BASE_OPTS });
assert.equal(result.author, 'fetched-pr-author');
assert.equal(pullsGetCalled, true);
});
it('resolves author via API when payload issue is absent', async () => {
const { github, context, core } = createMocks({ apiUser: 'api-user' });
const result = await checkTeamMembership({ github, context, core, ...BASE_OPTS });
@@ -1,125 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
const { describe, it } = require('node:test');
const assert = require('node:assert/strict');
const {
base64ToBase64Url,
createInstallationToken,
createJwtSigningInput,
readConfig,
} = require('../actions/github-app-token/create-token.js');
const CONFIG = {
azureSubscriptionId: 'subscription-id',
keyVaultName: 'vault-name',
keyName: 'key-name',
githubAppClientId: 'client-id',
githubAppInstallationId: '12345',
targetRepository: 'microsoft/agent-framework',
};
describe('GitHub App token creation', () => {
it('creates a short-lived GitHub App JWT', () => {
const signingInput = createJwtSigningInput('client-id', 1_000);
const [encodedHeader, encodedPayload] = signingInput.split('.');
const header = JSON.parse(Buffer.from(encodedHeader, 'base64url').toString());
const payload = JSON.parse(Buffer.from(encodedPayload, 'base64url').toString());
assert.deepEqual(header, { alg: 'RS256', typ: 'JWT' });
assert.deepEqual(payload, { iat: 940, exp: 1_540, iss: 'client-id' });
});
it('converts Key Vault signatures to unpadded base64url', () => {
assert.equal(base64ToBase64Url('+/8='), '-_8');
});
it('requests a repository-scoped installation token', async () => {
let request;
const token = await createInstallationToken(CONFIG, {
nowSeconds: 1_000,
execute: (command, args) => {
assert.equal(command, 'az');
assert.ok(args.includes('RS256'));
return '+/8=\n';
},
fetch: async (url, options) => {
request = { url, options };
return {
ok: true,
json: async () => ({ token: 'installation-token' }),
};
},
});
assert.equal(token, 'installation-token');
assert.equal(request.url, 'https://api.github.com/app/installations/12345/access_tokens');
assert.match(request.options.headers.Authorization, /^Bearer [^.]+\.[^.]+\.-_8$/);
assert.deepEqual(JSON.parse(request.options.body), {
repositories: ['agent-framework'],
permissions: {
contents: 'read',
issues: 'write',
members: 'read',
pull_requests: 'write',
},
});
});
it('rejects incomplete configuration', () => {
assert.throws(
() => readConfig({}),
/Required GitHub App authentication configuration is missing/,
);
});
it('rejects repository values with extra path segments before signing', async () => {
let signed = false;
await assert.rejects(
createInstallationToken(
{ ...CONFIG, targetRepository: 'microsoft/agent-framework/extra' },
{
execute: () => {
signed = true;
return '+/8=\n';
},
},
),
/TARGET_REPOSITORY must use the owner\/repository format/,
);
assert.equal(signed, false);
});
it('rejects an empty Key Vault signature', async () => {
await assert.rejects(
createInstallationToken(CONFIG, {
execute: () => '\n',
}),
/Key Vault returned an empty signature/,
);
});
it('rejects a failed GitHub token request', async () => {
await assert.rejects(
createInstallationToken(CONFIG, {
execute: () => '+/8=\n',
fetch: async () => ({ ok: false, status: 403 }),
}),
/GitHub installation token request failed with HTTP 403/,
);
});
it('rejects an empty GitHub installation token', async () => {
await assert.rejects(
createInstallationToken(CONFIG, {
execute: () => '+/8=\n',
fetch: async () => ({
ok: true,
json: async () => ({ token: '' }),
}),
}),
/GitHub returned an empty installation token/,
);
});
});
-134
View File
@@ -1,134 +0,0 @@
# Copyright (c) Microsoft. All rights reserved.
# ruff:file-ignore[implicit-namespace-package, undocumented-public-class, undocumented-public-method]
from __future__ import annotations
import importlib.util
import sys
import tempfile
import unittest
from pathlib import Path
SCRIPT_PATH = Path(__file__).parents[1] / "scripts" / "python_check_coverage.py"
SPEC = importlib.util.spec_from_file_location("python_check_coverage", SCRIPT_PATH)
if SPEC is None or SPEC.loader is None:
raise RuntimeError(f"Unable to load {SCRIPT_PATH}")
coverage_checker = importlib.util.module_from_spec(SPEC)
sys.modules[SPEC.name] = coverage_checker
SPEC.loader.exec_module(coverage_checker)
class CoveragePolicyTests(unittest.TestCase):
def setUp(self) -> None:
self.temp_dir = tempfile.TemporaryDirectory()
self.root = Path(self.temp_dir.name)
self.packages_dir = self.root / "packages"
self.packages_dir.mkdir()
def tearDown(self) -> None:
self.temp_dir.cleanup()
def write_package(self, directory: str, name: str, status: str) -> None:
package_dir = self.packages_dir / directory
package_dir.mkdir()
(package_dir / "pyproject.toml").write_text(
f"""
[project]
name = "{name}"
classifiers = ["Development Status :: {status}"]
""".strip()
)
def write_coverage(self, files: dict[str, list[int]]) -> Path:
classes = []
total_lines = 0
covered_lines = 0
for file_path, hits in files.items():
lines = []
for line_number, hit_count in enumerate(hits, start=1):
total_lines += 1
covered_lines += hit_count > 0
lines.append(f'<line number="{line_number}" hits="{hit_count}"/>')
classes.append(f'<class filename="{file_path}"><lines>{"".join(lines)}</lines></class>')
line_rate = covered_lines / total_lines if total_lines else 0
xml_path = self.root / "coverage.xml"
xml_path.write_text(
f"""
<coverage line-rate="{line_rate}" branch-rate="0">
<packages>
<package name="test">
<classes>{"".join(classes)}</classes>
</package>
</packages>
</coverage>
""".strip()
)
return xml_path
def test_load_package_policies_uses_lifecycle_exemptions(self) -> None:
self.write_package("alpha", "agent-framework-alpha", "3 - Alpha")
self.write_package("beta", "agent-framework-beta", "4 - Beta")
self.write_package("stable", "agent-framework-stable", "5 - Production/Stable")
self.write_package("devui", "agent-framework-devui", "4 - Beta")
self.write_package("lab", "agent-framework-lab", "4 - Beta")
policies = {policy.directory: policy for policy in coverage_checker.load_package_policies(self.packages_dir)}
self.assertFalse(policies["alpha"].enforced)
self.assertTrue(policies["beta"].enforced)
self.assertTrue(policies["stable"].enforced)
self.assertTrue(policies["devui"].exempt)
self.assertFalse(policies["devui"].enforced)
self.assertTrue(policies["lab"].exempt)
self.assertFalse(policies["lab"].enforced)
def test_load_package_policies_rejects_missing_lifecycle(self) -> None:
package_dir = self.packages_dir / "missing"
package_dir.mkdir()
(package_dir / "pyproject.toml").write_text('[project]\nname = "agent-framework-missing"\n')
with self.assertRaisesRegex(ValueError, "exactly one Development Status"):
coverage_checker.load_package_policies(self.packages_dir)
def test_parse_coverage_aggregates_nested_modules_by_distribution(self) -> None:
xml_path = self.write_coverage({
"packages/core/agent_framework/_agents.py": [1, 0],
"packages/core/agent_framework/_workflows/_workflow.py": [1, 1],
})
package_stats, _, _ = coverage_checker.parse_coverage_xml(xml_path)
self.assertEqual(package_stats["core"].lines_valid, 4)
self.assertEqual(package_stats["core"].lines_covered, 3)
def test_beta_package_below_threshold_fails(self) -> None:
self.write_package("beta", "agent-framework-beta", "4 - Beta")
xml_path = self.write_coverage({"packages/beta/agent_framework_beta/client.py": [1, 0]})
self.assertFalse(coverage_checker.check_coverage(xml_path, 85, self.packages_dir))
def test_missing_beta_package_fails(self) -> None:
self.write_package("beta", "agent-framework-beta", "4 - Beta")
xml_path = self.write_coverage({})
self.assertFalse(coverage_checker.check_coverage(xml_path, 85, self.packages_dir))
def test_alpha_and_exempt_packages_do_not_fail(self) -> None:
self.write_package("alpha", "agent-framework-alpha", "3 - Alpha")
self.write_package("devui", "agent-framework-devui", "4 - Beta")
self.write_package("lab", "agent-framework-lab", "4 - Beta")
xml_path = self.write_coverage({})
self.assertTrue(coverage_checker.check_coverage(xml_path, 85, self.packages_dir))
def test_beta_package_at_threshold_passes(self) -> None:
self.write_package("beta", "agent-framework-beta", "4 - Beta")
xml_path = self.write_coverage({"packages/beta/agent_framework_beta/client.py": [1] * 17 + [0] * 3})
self.assertTrue(coverage_checker.check_coverage(xml_path, 85, self.packages_dir))
if __name__ == "__main__":
unittest.main()
@@ -1,212 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
/**
* Tests for resolve_integration_test_target.js.
*
* Run with: node --test .github/tests/test_resolve_integration_test_target.js
*/
const { describe, it } = require('node:test');
const assert = require('node:assert/strict');
const resolveIntegrationTestTarget = require('../scripts/resolve_integration_test_target.js');
const HEAD_SHA = 'a'.repeat(40);
const BASE_SHA = 'b'.repeat(40);
function review({
id,
login,
state = 'APPROVED',
commitId = HEAD_SHA,
submittedAt = `2026-07-13T00:00:${String(id).padStart(2, '0')}Z`,
}) {
return {
id,
state,
commit_id: commitId,
submitted_at: submittedAt,
user: { login },
};
}
function createMocks({
pullState = 'open',
pullAuthor = 'contributor',
reviews = [],
permissions = {},
} = {}) {
const core = {
infoMessages: [],
info(message) {
this.infoMessages.push(message);
},
};
const context = {
repo: { owner: 'microsoft', repo: 'agent-framework' },
};
const github = {
paginate: async () => reviews,
rest: {
pulls: {
get: async () => ({
data: {
state: pullState,
user: { login: pullAuthor },
head: { sha: HEAD_SHA },
base: { sha: BASE_SHA },
},
}),
listReviews: async () => {},
},
repos: {
get: async () => ({ data: { default_branch: 'main' } }),
getBranch: async ({ branch }) => ({
data: { commit: { sha: branch === 'main' ? BASE_SHA : HEAD_SHA } },
}),
getCollaboratorPermissionLevel: async ({ username }) => ({
data: permissions[username] || {
permission: 'read',
user: { permissions: { push: false } },
},
}),
},
},
};
return { core, context, github };
}
const WRITE_PERMISSION = {
permission: 'write',
user: { permissions: { push: true } },
};
describe('input validation', () => {
it('rejects missing and conflicting targets', async () => {
const mocks = createMocks();
await assert.rejects(
() => resolveIntegrationTestTarget(mocks),
/provide either a PR number or a branch name/,
);
await assert.rejects(
() => resolveIntegrationTestTarget({ ...mocks, prNumber: '1', branch: 'feature' }),
/not both/,
);
});
it('rejects invalid PR numbers and branch names', async () => {
const mocks = createMocks();
await assert.rejects(
() => resolveIntegrationTestTarget({ ...mocks, prNumber: '1;echo' }),
/Invalid PR number/,
);
await assert.rejects(
() => resolveIntegrationTestTarget({ ...mocks, branch: 'feature branch' }),
/Invalid branch name/,
);
});
});
describe('pull request resolution', () => {
it('pins an open PR with two fresh write-capable approvals', async () => {
const mocks = createMocks({
reviews: [
review({ id: 1, login: 'maintainer-one' }),
review({ id: 2, login: 'maintainer-two' }),
],
permissions: {
'maintainer-one': WRITE_PERMISSION,
'maintainer-two': WRITE_PERMISSION,
},
});
const result = await resolveIntegrationTestTarget({ ...mocks, prNumber: '123' });
assert.deepEqual(result, {
baseRef: BASE_SHA,
checkoutRef: HEAD_SHA,
description: 'PR #123',
});
});
it('rejects closed PRs', async () => {
const mocks = createMocks({ pullState: 'closed' });
await assert.rejects(
() => resolveIntegrationTestTarget({ ...mocks, prNumber: '123' }),
/is not open/,
);
});
it('ignores stale, self, and read-only approvals', async () => {
const mocks = createMocks({
reviews: [
review({ id: 1, login: 'stale', commitId: 'c'.repeat(40) }),
review({ id: 2, login: 'contributor' }),
review({ id: 3, login: 'reader' }),
review({ id: 4, login: 'maintainer' }),
],
permissions: {
contributor: WRITE_PERMISSION,
reader: { permission: 'read', user: { permissions: { push: false } } },
maintainer: WRITE_PERMISSION,
},
});
await assert.rejects(
() => resolveIntegrationTestTarget({ ...mocks, prNumber: '123' }),
/found 1/,
);
});
it('uses each reviewer latest decisive review and ignores later comments', async () => {
const mocks = createMocks({
reviews: [
review({ id: 1, login: 'changes-requested' }),
review({ id: 2, login: 'changes-requested', state: 'CHANGES_REQUESTED' }),
review({ id: 3, login: 'maintainer-one' }),
review({ id: 4, login: 'maintainer-one', state: 'COMMENTED' }),
review({ id: 5, login: 'maintainer-two' }),
],
permissions: {
'changes-requested': WRITE_PERMISSION,
'maintainer-one': WRITE_PERMISSION,
'maintainer-two': WRITE_PERMISSION,
},
});
const result = await resolveIntegrationTestTarget({ ...mocks, prNumber: '123' });
assert.equal(result.checkoutRef, HEAD_SHA);
});
it('does not count a dismissed approval', async () => {
const mocks = createMocks({
reviews: [
review({ id: 1, login: 'dismissed', state: 'DISMISSED' }),
review({ id: 2, login: 'maintainer' }),
],
permissions: {
dismissed: WRITE_PERMISSION,
maintainer: WRITE_PERMISSION,
},
});
await assert.rejects(
() => resolveIntegrationTestTarget({ ...mocks, prNumber: '123' }),
/found 1/,
);
});
});
describe('branch resolution', () => {
it('pins base-repository branches and their comparison base to SHAs', async () => {
const mocks = createMocks();
const result = await resolveIntegrationTestTarget({ ...mocks, branch: 'feature/test' });
assert.deepEqual(result, {
baseRef: BASE_SHA,
checkoutRef: HEAD_SHA,
description: 'branch feature/test',
});
});
});
-37
View File
@@ -1,37 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
/**
* Tests for review_command.js.
*
* Run with: node --test .github/tests/test_review_command.js
*/
const { describe, it } = require('node:test');
const assert = require('node:assert/strict');
const isReviewCommand = require('../scripts/review_command.js');
describe('review command validation', () => {
it('accepts the exact review command', () => {
assert.equal(isReviewCommand('/review'), true);
});
it('accepts surrounding whitespace', () => {
assert.equal(isReviewCommand('/review\r\n'), true);
assert.equal(isReviewCommand(' \n/review\t'), true);
});
it('rejects commands with additional content', () => {
assert.equal(isReviewCommand('/reviewer'), false);
assert.equal(isReviewCommand('/review please'), false);
assert.equal(isReviewCommand('/review\nadditional text'), false);
assert.equal(isReviewCommand('/Review'), false);
});
it('rejects missing or non-string comment bodies', () => {
assert.equal(isReviewCommand(''), false);
assert.equal(isReviewCommand(null), false);
assert.equal(isReviewCommand(undefined), false);
});
});
@@ -23,16 +23,16 @@ For each project that needs to be migrated, you need to do the following:
- Identify the specific Semantic Kernel agent types being used:
- `ChatCompletionAgent``ChatClientAgent`
- `OpenAIAssistantAgent``assistantsClient.CreateAIAgent()` (via OpenAI Assistants client extension)
- `AzureAIAgent``persistentAgentsClient.CreateAIAgent()` (via Microsoft Foundry 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 Microsoft Foundry
- **Existing hosted agents**: Use `GetAIAgent(agentId)` methods for OpenAI Assistants and Azure AI Foundry
</agent_type_identification>
- Determine the AI provider being used (OpenAI, Azure OpenAI, Microsoft Foundry, etc.)
- Determine the AI provider being used (OpenAI, Azure OpenAI, Azure AI Foundry, etc.)
- Analyze tool/function registration patterns
- Review thread management and invocation patterns
@@ -90,7 +90,7 @@ below in wrong order or skip any of them):
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, Microsoft Foundry, A2A, ONNX, etc.)
- 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
@@ -223,7 +223,7 @@ 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 Microsoft Foundry provider
using Azure.AI.Agents.Persistent; // For Azure AI Foundry provider
using Azure.Identity; // For Azure authentication
```
</configuration_changes>
@@ -499,7 +499,7 @@ For every thread created if there's intent to cleanup, the caller should track a
var assistantClient = new OpenAIClient(apiKey).GetAssistantClient();
await assistantClient.DeleteThreadAsync(thread.ConversationId);
// For Microsoft Foundry (when cleanup is needed):
// For Azure AI Foundry (when cleanup is needed):
var persistentClient = new PersistentAgentsClient(endpoint, credential);
await persistentClient.Threads.DeleteThreadAsync(thread.ConversationId);
@@ -514,7 +514,7 @@ await persistentClient.Threads.DeleteThreadAsync(thread.ConversationId);
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, Microsoft Foundry)
4. Only implement cleanup for providers that require it (Assistants, Azure AI Foundry)
</api_changes>
### Provider-Specific Creation Patterns
@@ -550,13 +550,13 @@ AIAgent agent = new AzureOpenAIClient(endpoint, credential)
.CreateAIAgent(instructions: instructions);
```
**Microsoft Foundry (New):**
**Azure AI Foundry (New):**
```csharp
AIAgent agent = new PersistentAgentsClient(endpoint, credential)
.CreateAIAgent(model: deploymentName, instructions: instructions);
```
**Microsoft Foundry (Existing):**
**Azure AI Foundry (Existing):**
```csharp
AIAgent agent = await new PersistentAgentsClient(endpoint, credential)
.GetAIAgentAsync(agentId);
@@ -1079,7 +1079,7 @@ AgentThread thread = agent.GetNewThread();
```
</api_changes>
### 4. Microsoft Foundry (AzureAIAgent) Migration
### 4. Azure AI Foundry (AzureAIAgent) Migration
<configuration_changes>
**Remove Semantic Kernel Packages:**
+4 -4
View File
@@ -32,13 +32,13 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3
uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -51,7 +51,7 @@ jobs:
# 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@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
uses: github/codeql-action/autobuild@9e0d7b8d25671d64c341c19c0152d693099fb5ba # 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
@@ -64,6 +64,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3
uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4
with:
category: "/language:${{matrix.language}}"
+37 -111
View File
@@ -6,9 +6,6 @@ on:
- opened
- reopened
- ready_for_review
issue_comment:
types:
- created
workflow_dispatch:
inputs:
pr_number:
@@ -18,12 +15,11 @@ on:
permissions:
contents: read
id-token: write
issues: write
pull-requests: write
concurrency:
group: devflow-pr-review-${{ github.repository }}-${{ github.event.pull_request.number || github.event.issue.number || inputs.pr_number || github.run_id }}
group: devflow-pr-review-${{ github.repository }}-${{ github.event.pull_request.number || inputs.pr_number || github.run_id }}
cancel-in-progress: true
env:
@@ -31,46 +27,10 @@ env:
DEVFLOW_REF: main
TARGET_REPO_PATH: ${{ github.workspace }}/target-repo
DEVFLOW_PATH: ${{ github.workspace }}/devflow
MODEL_CONFIG_PATH: ${{ github.workspace }}/devflow/config.ci.yaml
jobs:
command_check:
if: >-
github.event_name != 'issue_comment' ||
(
github.event.issue.pull_request &&
(
github.event.comment.author_association == 'MEMBER' ||
github.event.comment.author_association == 'OWNER'
)
)
runs-on: ubuntu-latest
outputs:
should_review: ${{ steps.check.outputs.should_review }}
steps:
- name: Checkout review command validation
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.sha }}
sparse-checkout: .github/scripts/review_command.js
fetch-depth: 1
persist-credentials: false
- name: Check review command
id: check
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
const isReviewCommand = require('./.github/scripts/review_command.js');
const shouldReview = context.eventName !== 'issue_comment' ||
isReviewCommand(context.payload.comment?.body);
core.setOutput('should_review', shouldReview ? 'true' : 'false');
team_check:
needs: command_check
if: ${{ needs.command_check.outputs.should_review == 'true' }}
runs-on: ubuntu-latest
environment: github-app-auth
outputs:
is_team_member: ${{ steps.check.outputs.is_team_member }}
pr_number: ${{ steps.pr.outputs.pr_number }}
@@ -82,7 +42,6 @@ jobs:
shell: bash
env:
PR_HTML_URL: ${{ github.event.pull_request.html_url }}
PR_NUMBER_COMMENT: ${{ github.event.issue.number }}
PR_NUMBER_EVENT: ${{ github.event.pull_request.number }}
PR_NUMBER_INPUT: ${{ inputs.pr_number }}
run: |
@@ -91,9 +50,6 @@ jobs:
if [[ "${GITHUB_EVENT_NAME}" == "pull_request_target" ]]; then
pr_number="${PR_NUMBER_EVENT}"
pr_url="${PR_HTML_URL}"
elif [[ "${GITHUB_EVENT_NAME}" == "issue_comment" ]]; then
pr_number="${PR_NUMBER_COMMENT}"
pr_url="https://github.com/${GITHUB_REPOSITORY}/pull/${pr_number}"
else
pr_number="${PR_NUMBER_INPUT}"
pr_url="https://github.com/${GITHUB_REPOSITORY}/pull/${pr_number}"
@@ -108,78 +64,49 @@ jobs:
echo "pr_number=${pr_number}" >> "$GITHUB_OUTPUT"
echo "repo=${GITHUB_REPOSITORY}" >> "$GITHUB_OUTPUT"
- name: Checkout GitHub automation
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.sha }}
sparse-checkout: |
.github/actions/github-app-token
.github/scripts/check_team_membership.js
fetch-depth: 1
persist-credentials: false
- name: Get GitHub automation token
id: github-auth
uses: ./.github/actions/github-app-token
with:
mode: ${{ vars.GH_APP_AUTH_MODE }}
azure-client-id: ${{ secrets.GH_APP_AZURE_CLIENT_ID }}
azure-tenant-id: ${{ secrets.GH_APP_AZURE_TENANT_ID }}
azure-subscription-id: ${{ secrets.GH_APP_AZURE_SUBSCRIPTION_ID }}
key-vault-name: ${{ secrets.GH_APP_KEY_VAULT_NAME }}
key-name: ${{ secrets.GH_APP_KEY_NAME }}
github-app-client-id: ${{ secrets.GH_APP_CLIENT_ID }}
github-app-installation-id: ${{ secrets.GH_APP_INSTALLATION_ID }}
repository: ${{ github.repository }}
fallback-token: ${{ secrets.GH_ACTIONS_PR_WRITE }}
- name: Check review requester team membership
- name: Check PR author team membership
id: check
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
MEMBERSHIP_USER: ${{ github.event_name == 'issue_comment' && github.event.comment.user.login || '' }}
TEAM_NAME: ${{ secrets.DEVELOPER_TEAM }}
PR_NUMBER: ${{ steps.pr.outputs.pr_number }}
with:
github-token: ${{ steps.github-auth.outputs.token }}
github-token: ${{ secrets.GH_ACTIONS_PR_WRITE }}
script: |
const checkTeamMembership = require('./.github/scripts/check_team_membership.js');
const { author, isTeamMember } = await checkTeamMembership({
github,
context,
core,
teamSlug: process.env.TEAM_NAME,
issueNumber: process.env.PR_NUMBER,
username: process.env.MEMBERSHIP_USER,
});
core.setOutput('is_team_member', isTeamMember ? 'true' : 'false');
if (isTeamMember) {
core.info(`User ${author} is a team member; proceeding with review.`);
} else {
core.info(`User ${author} is not a member of ${process.env.TEAM_NAME}; skipping review.`);
let author = context.payload.pull_request?.user?.login;
if (!author) {
const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: Number(process.env.PR_NUMBER),
});
author = pr.user.login;
}
- name: React to authorized review command
if: ${{ github.event_name == 'issue_comment' && steps.check.outputs.is_team_member == 'true' }}
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ steps.github-auth.outputs.token }}
script: |
await github.rest.reactions.createForIssueComment({
...context.repo,
comment_id: context.payload.comment.id,
content: 'eyes',
});
let isTeamMember = false;
try {
const teamMembership = await github.rest.teams.getMembershipForUserInOrg({
org: context.repo.owner,
team_slug: process.env.TEAM_NAME,
username: author,
});
isTeamMember = teamMembership.data.state === 'active';
} catch (error) {
console.log(`Team membership lookup failed for ${author}: ${error.message}`);
isTeamMember = false;
}
core.setOutput('is_team_member', isTeamMember ? 'true' : 'false');
if (isTeamMember) {
core.info(`Author ${author} is a team member; proceeding with review.`);
} else {
core.info(`Author ${author} is not a member of ${process.env.TEAM_NAME}; skipping review.`);
}
review:
runs-on: ubuntu-latest
needs: team_check
if: ${{ needs.team_check.outputs.is_team_member == 'true' }}
permissions:
copilot-requests: write
contents: read
issues: write
pull-requests: write
timeout-minutes: 60
# Advisory check: failures here should not block the PR. The reviewer
# posts comments as a best-effort signal; if the pipeline breaks, the
@@ -189,7 +116,7 @@ jobs:
steps:
# Safe checkout: base repo only, not the untrusted PR head.
- name: Checkout target repo base
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.sha }}
fetch-depth: 0
@@ -198,7 +125,7 @@ jobs:
# Private DevFlow checkout: the PAT/token grants access to this repo's code.
- name: Checkout DevFlow
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
repository: ${{ env.DEVFLOW_REPOSITORY }}
ref: ${{ env.DEVFLOW_REF }}
@@ -208,12 +135,12 @@ jobs:
path: devflow
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.13"
- name: Set up uv
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
with:
version: "0.11.x"
enable-cache: true
@@ -226,8 +153,8 @@ jobs:
id: review
working-directory: ${{ env.DEVFLOW_PATH }}
env:
DEVFLOW_TOKEN: ${{ secrets.DEVFLOW_TOKEN }}
GITHUB_TOKEN: ${{ github.token }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_COPILOT_TOKEN: ${{ secrets.GH_COPILOT_TOKEN }}
SK_REPO_PATH: ${{ env.TARGET_REPO_PATH }}
AGENT_REPO_PATH: ${{ env.TARGET_REPO_PATH }}
PR_URL: ${{ needs.team_check.outputs.pr_url }}
@@ -235,5 +162,4 @@ jobs:
uv run python scripts/trigger_pr_review.py \
--pr-url "$PR_URL" \
--github-username "$GITHUB_ACTOR" \
--review-compare \
--no-require-comment-selection
+133 -54
View File
@@ -37,12 +37,12 @@ jobs:
outputs:
dotnetChanges: ${{ steps.filter.outputs.dotnet }}
cosmosDbChanges: ${{ steps.filter.outputs.cosmosdb }}
azureStorageChanges: ${{ steps.filter.outputs.azurestorage }}
foundryHostingChanges: ${{ steps.filter.outputs.foundryHosting }}
functionsChanged: ${{ steps.filter.outputs.functions }}
coreChanged: ${{ steps.filter.outputs.core }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: dorny/paths-filter@d1c1ffe0248fe513906c8e24db8ea791d46f8590 # v3
id: filter
with:
filters: |
@@ -50,12 +50,6 @@ jobs:
- 'dotnet/**'
cosmosdb:
- 'dotnet/src/Microsoft.Agents.AI.CosmosNoSql/**'
azurestorage:
- 'dotnet/src/Microsoft.Agents.AI.Hosting.AzureStorage/**'
- 'dotnet/tests/Microsoft.Agents.AI.Hosting.AzureStorage.IntegrationTests/**'
- 'dotnet/tests/Microsoft.Agents.AI.Hosting.AzureStorage.UnitTests/**'
- 'dotnet/Directory.Packages.props'
- '.github/workflows/dotnet-build-and-test.yml'
# The Foundry hosted-agent IT is costly (builds a container, pushes to ACR,
# provisions live agents). Only run it when the project under test, its
# dependency chain, the test container, the test fixture, or their tooling
@@ -72,6 +66,13 @@ jobs:
- 'dotnet/Directory.Packages.props'
- 'dotnet/tests/Foundry.Hosting.IntegrationTests/scripts/it-build-image.ps1'
- '.github/workflows/dotnet-build-and-test.yml'
functions:
- 'dotnet/src/Microsoft.Agents.AI.DurableTask/**'
- 'dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/**'
- 'dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/**'
- 'dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/**'
- '.github/actions/azure-functions-integration-setup/**'
- '.github/workflows/dotnet-build-and-test.yml'
core:
- 'dotnet/src/Microsoft.Agents.AI/**'
- 'dotnet/src/Microsoft.Agents.AI.Abstractions/**'
@@ -110,7 +111,7 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
sparse-checkout: |
@@ -127,7 +128,6 @@ jobs:
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
with:
global-json-file: ${{ github.workspace }}/dotnet/global.json
- name: Build dotnet solutions
shell: bash
run: |
@@ -163,7 +163,6 @@ jobs:
# Change to project directory to ensure local nuget.config is used
pushd consoleapp
dotnet add packcheck.csproj package Microsoft.Agents.AI --prerelease
dotnet add packcheck.csproj package Microsoft.Agents.AI.LocalCodeAct --prerelease
dotnet build -f ${{ matrix.targetFramework }} -c ${{ matrix.configuration }} packcheck.csproj
# Clean up
@@ -185,14 +184,13 @@ jobs:
runs-on: ${{ matrix.os }}
environment: ${{ matrix.environment }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
sparse-checkout: |
.
.github
dotnet
docs/specs
python
declarative-agents
@@ -209,27 +207,6 @@ jobs:
Start-CosmosDbEmulator -NoUI -Key "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="
echo "COSMOSDB_EMULATOR_AVAILABLE=true" >> $env:GITHUB_ENV
- name: Start Azurite Blob service
if: ${{ runner.os == 'Linux' && (needs.paths-filter.outputs.azureStorageChanges == 'true' || (github.event_name != 'pull_request' && matrix.integration-tests)) }}
shell: bash
run: |
docker run --detach --rm \
--name azurite \
--publish 10000:10000 \
mcr.microsoft.com/azure-storage/azurite:3.35.0@sha256:647c63a91102a9d8e8000aab803436e1fc85fbb285e7ce830a82ee5d6661cf37 \
azurite-blob --blobHost 0.0.0.0 --blobPort 10000 --skipApiVersionCheck
for attempt in {1..30}; do
if (echo > /dev/tcp/127.0.0.1/10000) > /dev/null 2>&1; then
echo "AZURITE_AVAILABLE=true" >> "$GITHUB_ENV"
exit 0
fi
sleep 1
done
docker logs azurite
exit 1
- name: Setup dotnet
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
with:
@@ -264,6 +241,7 @@ jobs:
-OutputPath dotnet/filtered-unit.slnx
./dotnet/eng/scripts/New-FilteredSolution.ps1 @commonArgs `
-TestProjectNameIncludeFilter "*IntegrationTests*" `
-TestProjectNameExcludeFilter "*DurableTask.IntegrationTests*","*AzureFunctions.IntegrationTests*" `
-OutputPath dotnet/filtered-integration.slnx
- name: Run Unit Tests
@@ -299,7 +277,7 @@ jobs:
- name: Azure CLI Login
if: github.event_name != 'pull_request' && matrix.integration-tests
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
@@ -334,15 +312,14 @@ jobs:
AZURE_OPENAI_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME }}
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME }}
AZURE_OPENAI_ENDPOINT: ${{ vars.AZURE_OPENAI_ENDPOINT }}
# Microsoft Foundry
# Azure AI Foundry
AZURE_AI_PROJECT_ENDPOINT: ${{ vars.AZURE_AI_PROJECT_ENDPOINT }}
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.AZURE_AI_MODEL_DEPLOYMENT_NAME }}
AZURE_AI_BING_CONNECTION_ID: ${{ vars.AZURE_AI_BING_CONNECTION_ID }}
# Anthropic Models
# Disable Anthropic tests by not providing environment vars until 404 failure is resolved
# ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
# ANTHROPIC_CHAT_MODEL_NAME: ${{ vars.ANTHROPIC_CHAT_MODEL_NAME }}
# ANTHROPIC_REASONING_MODEL_NAME: ${{ vars.ANTHROPIC_REASONING_MODEL_NAME }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
ANTHROPIC_CHAT_MODEL_NAME: ${{ vars.ANTHROPIC_CHAT_MODEL_NAME }}
ANTHROPIC_REASONING_MODEL_NAME: ${{ vars.ANTHROPIC_REASONING_MODEL_NAME }}
# Generate test reports and check coverage
- name: Generate test reports
@@ -355,7 +332,7 @@ jobs:
- name: Upload coverage report artifact
if: matrix.targetFramework == env.COVERAGE_FRAMEWORK
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: CoverageReport-${{ matrix.os }}-${{ matrix.targetFramework }}-${{ matrix.configuration }} # Artifact name
path: ./TestResults/Reports # Directory containing files to upload
@@ -367,7 +344,7 @@ jobs:
- name: Upload integration test results
if: always() && github.event_name != 'pull_request' && matrix.integration-tests
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: dotnet-test-results-${{ matrix.targetFramework }}-${{ matrix.os }}
path: IntegrationTestResults/**/*.junit
@@ -385,7 +362,7 @@ jobs:
env:
configuration: Release
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
sparse-checkout: |
@@ -413,7 +390,7 @@ jobs:
run: dotnet build dotnet/tests/Foundry.Hosting.IntegrationTests/Foundry.Hosting.IntegrationTests.csproj -c "$configuration" --warnaserror
- name: Azure CLI Login
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
@@ -461,11 +438,113 @@ jobs:
AZURE_SEARCH_INDEX_NAME: ${{ secrets.AZURE_SEARCH_INDEX_NAME }}
# IT_HOSTED_AGENT_IMAGE was exported into $GITHUB_ENV by the previous step.
# DurableTask and AzureFunctions integration tests (ubuntu/net10.0 only).
# Split from main dotnet-test job for path-based filtering and parallelism.
dotnet-test-functions:
needs: [paths-filter]
if: >
github.event_name != 'pull_request' &&
(needs.paths-filter.outputs.functionsChanged == 'true' ||
needs.paths-filter.outputs.coreChanged == 'true' ||
github.event_name == 'schedule' ||
github.event_name == 'workflow_dispatch')
runs-on: ubuntu-latest
environment: integration
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
sparse-checkout: |
.
.github
dotnet
python
declarative-agents
- name: Free runner disk space
uses: ./.github/actions/free-runner-disk-space
- name: Setup dotnet
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
with:
global-json-file: ${{ github.workspace }}/dotnet/global.json
- name: Build functions integration test projects
shell: bash
working-directory: dotnet
run: |
dotnet build ./tests/Microsoft.Agents.AI.DurableTask.IntegrationTests -c Release -f net10.0 --warnaserror
dotnet build ./tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests -c Release -f net10.0 --warnaserror
- name: Azure CLI Login
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Set up Durable Task and Azure Functions Integration Test Emulators
uses: ./.github/actions/azure-functions-integration-setup
id: azure-functions-setup
- name: Run Functions Integration Tests
shell: pwsh
working-directory: dotnet
run: |
# Run DurableTask integration tests
dotnet test `
--project ./tests/Microsoft.Agents.AI.DurableTask.IntegrationTests `
-f net10.0 `
-c Release `
--no-build -v Normal `
--report-xunit-trx `
--report-junit `
--results-directory ../IntegrationTestResults/ `
--ignore-exit-code 8 `
--filter-not-trait "Category=IntegrationDisabled" `
--parallel-algorithm aggressive `
--max-threads 2.0x
# Run AzureFunctions integration tests
dotnet test `
--project ./tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests `
-f net10.0 `
-c Release `
--no-build -v Normal `
--report-xunit-trx `
--report-junit `
--results-directory ../IntegrationTestResults/ `
--ignore-exit-code 8 `
--filter-not-trait "Category=IntegrationDisabled" `
--parallel-algorithm aggressive `
--max-threads 2.0x
env:
# OpenAI Models
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_CHAT_MODEL_NAME: ${{ vars.OPENAI_CHAT_MODEL_NAME }}
OPENAI_REASONING_MODEL_NAME: ${{ vars.OPENAI_REASONING_MODEL_NAME }}
# Azure OpenAI Models
AZURE_OPENAI_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME }}
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME }}
AZURE_OPENAI_ENDPOINT: ${{ vars.AZURE_OPENAI_ENDPOINT }}
# Azure AI Foundry
AZURE_AI_PROJECT_ENDPOINT: ${{ vars.AZURE_AI_PROJECT_ENDPOINT }}
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.AZURE_AI_MODEL_DEPLOYMENT_NAME }}
AZURE_AI_BING_CONNECTION_ID: ${{ vars.AZURE_AI_BING_CONNECTION_ID }}
- name: Upload functions test results
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: dotnet-test-results-functions-net10.0-ubuntu-latest
path: IntegrationTestResults/**/*.junit
if-no-files-found: ignore
# 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, dotnet-test, dotnet-foundry-hosted-it]
needs: [dotnet-build, dotnet-test, dotnet-foundry-hosted-it, dotnet-test-functions]
steps:
- name: Get Date
shell: bash
@@ -493,14 +572,14 @@ jobs:
- name: Fail workflow if tests failed
id: check_tests_failed
if: contains(join(needs.*.result, ','), 'failure')
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # 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@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: core.setFailed('Integration Tests Cancelled!')
@@ -512,13 +591,13 @@ jobs:
github.event_name != 'pull_request' &&
(contains(join(needs.*.result, ','), 'success') ||
contains(join(needs.*.result, ','), 'failure'))
needs: [dotnet-test]
needs: [dotnet-test, dotnet-test-functions]
runs-on: ubuntu-latest
defaults:
run:
working-directory: python
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
sparse-checkout: |
@@ -530,12 +609,12 @@ jobs:
python-version: "3.13"
os: ${{ runner.os }}
- name: Download all test results from current run
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
pattern: dotnet-test-results-*
path: dotnet-test-results/
- name: Restore report history cache
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: python/dotnet-integration-report-history.json
key: dotnet-integration-report-history-${{ github.run_id }}
@@ -552,13 +631,13 @@ jobs:
run: cat dotnet-integration-test-report.md >> $GITHUB_STEP_SUMMARY
- name: Save report history cache
if: always()
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: python/dotnet-integration-report-history.json
key: dotnet-integration-report-history-${{ github.run_id }}
- name: Upload trend report
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: dotnet-integration-test-report
path: |
+4 -11
View File
@@ -30,7 +30,7 @@ jobs:
steps:
- name: Check out code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
persist-credentials: false
@@ -54,14 +54,11 @@ jobs:
- name: Find csproj files
id: find-csproj
if: github.event_name != 'pull_request' || steps.changed-files.outputs.added_modified != '' || steps.changed-files.outcome == 'failure'
env:
ADDED_MODIFIED: ${{ steps.changed-files.outputs.added_modified }}
run: |
csproj_files=()
exclude_files=("Experimental.Orchestration.Flow.csproj" "Experimental.Orchestration.Flow.UnitTests.csproj" "Experimental.Orchestration.Flow.IntegrationTests.csproj")
set -f
if [[ ${{ steps.changed-files.outcome }} == 'success' ]]; then
for file in $ADDED_MODIFIED; do
for file in ${{ steps.changed-files.outputs.added_modified }}; do
echo "$file was changed"
dir="./$file"
while [[ $dir != "." && $dir != "/" && $dir != $GITHUB_WORKSPACE ]]; do
@@ -83,7 +80,6 @@ jobs:
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
set +f
- name: Pull container dotnet/sdk:${{ matrix.dotnet }}
if: steps.find-csproj.outputs.csproj_files != ''
@@ -92,11 +88,8 @@ jobs:
# This step will run dotnet format on each of the unique csproj files and fail if any changes are made
- name: Run dotnet format
if: steps.find-csproj.outputs.csproj_files != ''
env:
CSPROJ_FILES: ${{ steps.find-csproj.outputs.csproj_files }}
run: |
set -f
for csproj in $CSPROJ_FILES; do
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 }} dotnet format "$csproj" --verify-no-changes --verbosity diagnostic
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"
done
+8 -19
View File
@@ -9,30 +9,16 @@ on:
workflow_call:
inputs:
checkout-ref:
description: "Immutable commit SHA to check out"
description: "Git ref to checkout (e.g., refs/pull/123/head)"
required: true
type: string
secrets:
AZURE_CLIENT_ID:
required: true
AZURE_TENANT_ID:
required: true
AZURE_SUBSCRIPTION_ID:
required: true
AZUREAI__ENDPOINT:
required: true
OPENAI__APIKEY:
required: true
permissions:
contents: read
id-token: write
jobs:
dotnet-integration-tests:
permissions:
copilot-requests: write
contents: read
id-token: write
strategy:
fail-fast: false
matrix:
@@ -43,7 +29,7 @@ jobs:
environment: integration
timeout-minutes: 60
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ inputs.checkout-ref }}
persist-credentials: false
@@ -77,12 +63,16 @@ jobs:
done
- name: Azure CLI Login
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Set up Durable Task and Azure Functions Integration Test Emulators
if: matrix.os == 'ubuntu-latest'
uses: ./.github/actions/azure-functions-integration-setup
- name: Run Integration Tests
shell: bash
run: |
@@ -98,7 +88,6 @@ jobs:
env:
COSMOSDB_ENDPOINT: https://localhost:8081
COSMOSDB_KEY: C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==
GITHUB_TOKEN: ${{ github.token }}
OpenAI__ApiKey: ${{ secrets.OPENAI__APIKEY }}
OpenAI__ChatModelId: ${{ vars.OPENAI__CHATMODELID }}
OpenAI__ChatReasoningModelId: ${{ vars.OPENAI__CHATREASONINGMODELID }}
+4 -7
View File
@@ -41,7 +41,7 @@ jobs:
environment: 'integration'
timeout-minutes: 90
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
sparse-checkout: |
@@ -58,7 +58,7 @@ jobs:
- name: Azure CLI Login
if: github.event_name != 'pull_request'
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
@@ -105,13 +105,10 @@ jobs:
AZURE_OPENAI_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME }}
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME }}
AZURE_OPENAI_ENDPOINT: ${{ vars.AZURE_OPENAI_ENDPOINT }}
# Microsoft Foundry
# Azure AI Foundry
AZURE_AI_PROJECT_ENDPOINT: ${{ vars.AZURE_AI_PROJECT_ENDPOINT }}
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.AZURE_AI_MODEL_DEPLOYMENT_NAME }}
AZURE_AI_BING_CONNECTION_ID: ${{ vars.AZURE_AI_BING_CONNECTION_ID }}
# Foundry
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
- name: Write Job Summary
if: always()
@@ -126,7 +123,7 @@ jobs:
- name: Upload results
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: verify-samples-results
path: |
@@ -1,42 +0,0 @@
name: GitHub automation tests
on:
pull_request:
paths:
- ".github/actions/**"
- ".github/scripts/**"
- ".github/tests/**"
- ".github/workflows/python-test-coverage.yml"
- ".github/workflows/github-automation-tests.yml"
push:
branches:
- main
paths:
- ".github/actions/**"
- ".github/scripts/**"
- ".github/tests/**"
- ".github/workflows/python-test-coverage.yml"
- ".github/workflows/github-automation-tests.yml"
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "22"
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.11"
- name: Run JavaScript tests
run: node --test .github/tests/*.js
- name: Run Python tests
run: python .github/tests/test_python_check_coverage.py
+52 -53
View File
@@ -3,7 +3,7 @@
# Go to Actions → "Integration Tests (Manual)" → Run workflow → enter a PR number or branch name.
#
# It calls dedicated integration-only workflows (dotnet-integration-tests and python-integration-tests),
# passing an immutable commit SHA so they check out and test the approved code.
# passing a ref so they check out and test the correct code.
# Changed paths are detected here so only the relevant test suites run.
#
@@ -26,6 +26,7 @@ on:
permissions:
contents: read
pull-requests: read
id-token: write
concurrency:
group: integration-tests-manual-${{ github.event.inputs.pr-number || github.event.inputs.branch }}
@@ -37,50 +38,67 @@ jobs:
runs-on: ubuntu-latest
outputs:
checkout-ref: ${{ steps.resolve.outputs.checkout-ref }}
base-ref: ${{ steps.resolve.outputs.base-ref }}
dotnet-changes: ${{ steps.detect-changes.outputs.dotnet }}
python-changes: ${{ steps.detect-changes.outputs.python }}
steps:
- name: Check out trusted workflow helpers
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.sha }}
persist-credentials: false
sparse-checkout: .github/scripts
- name: Resolve and authorize checkout ref
- name: Resolve checkout ref
id: resolve
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const resolveIntegrationTestTarget = require(
'./.github/scripts/resolve_integration_test_target.js'
);
const target = await resolveIntegrationTestTarget({
github,
context,
core,
prNumber: process.env.PR_NUMBER,
branch: process.env.BRANCH,
});
core.setOutput('checkout-ref', target.checkoutRef);
core.setOutput('base-ref', target.baseRef);
core.info(`Running integration tests for ${target.description} at ${target.checkoutRef}.`);
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.inputs.pr-number }}
BRANCH: ${{ github.event.inputs.branch }}
REPO: ${{ github.repository }}
run: |
if [ -n "$PR_NUMBER" ] && [ -n "$BRANCH" ]; then
echo "::error::Please provide either a PR number or a branch name, not both."
exit 1
fi
if [ -z "$PR_NUMBER" ] && [ -z "$BRANCH" ]; then
echo "::error::Please provide either a PR number or a branch name."
exit 1
fi
if [ -n "$PR_NUMBER" ]; then
if ! echo "$PR_NUMBER" | grep -Eq '^[0-9]+$'; then
echo "::error::Invalid PR number. Only numeric values are allowed."
exit 1
fi
PR_DATA=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json state)
PR_STATE=$(echo "$PR_DATA" | jq -r '.state')
if [ "$PR_STATE" != "OPEN" ]; then
echo "::error::PR #$PR_NUMBER is not open (state: $PR_STATE)"
exit 1
fi
echo "checkout-ref=refs/pull/$PR_NUMBER/head" >> "$GITHUB_OUTPUT"
echo "Running integration tests for PR #$PR_NUMBER"
else
if ! echo "$BRANCH" | grep -Eq '^[a-zA-Z0-9_./-]+$'; then
echo "::error::Invalid branch name. Only alphanumeric characters, hyphens, underscores, dots, and slashes are allowed."
exit 1
fi
echo "checkout-ref=$BRANCH" >> "$GITHUB_OUTPUT"
echo "Running integration tests for branch $BRANCH"
fi
- name: Detect changed paths
id: detect-changes
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BASE_REF: ${{ steps.resolve.outputs.base-ref }}
CHECKOUT_REF: ${{ steps.resolve.outputs.checkout-ref }}
PR_NUMBER: ${{ github.event.inputs.pr-number }}
BRANCH: ${{ github.event.inputs.branch }}
REPO: ${{ github.repository }}
run: |
CHANGED_FILES=$(gh api "repos/$REPO/compare/$BASE_REF...$CHECKOUT_REF" \
--jq '.files[].filename')
if [ -n "$PR_NUMBER" ]; then
CHANGED_FILES=$(gh pr diff "$PR_NUMBER" --repo "$REPO" --name-only)
else
# For branches, compare against main using the GitHub API
CHANGED_FILES=$(gh api "repos/$REPO/compare/main...$BRANCH" --jq '.files[].filename')
fi
DOTNET_CHANGES=false
PYTHON_CHANGES=false
@@ -95,41 +113,22 @@ jobs:
echo "dotnet=$DOTNET_CHANGES" >> "$GITHUB_OUTPUT"
echo "python=$PYTHON_CHANGES" >> "$GITHUB_OUTPUT"
echo "Detected changes; dotnet: $DOTNET_CHANGES, python: $PYTHON_CHANGES"
echo "Detected changes dotnet: $DOTNET_CHANGES, python: $PYTHON_CHANGES"
dotnet-integration-tests:
name: .NET Integration Tests
needs: resolve-ref
if: needs.resolve-ref.outputs.dotnet-changes == 'true'
permissions:
copilot-requests: write
contents: read
id-token: write
uses: ./.github/workflows/dotnet-integration-tests.yml
with:
checkout-ref: ${{ needs.resolve-ref.outputs.checkout-ref }}
secrets:
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
AZUREAI__ENDPOINT: ${{ secrets.AZUREAI__ENDPOINT }}
OPENAI__APIKEY: ${{ secrets.OPENAI__APIKEY }}
secrets: inherit
python-integration-tests:
name: Python Integration Tests
needs: resolve-ref
if: needs.resolve-ref.outputs.python-changes == 'true'
permissions:
copilot-requests: write
contents: read
id-token: write
uses: ./.github/workflows/python-integration-tests.yml
with:
checkout-ref: ${{ needs.resolve-ref.outputs.checkout-ref }}
secrets:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
FOUNDRY_MODELS_API_KEY: ${{ secrets.FOUNDRY_MODELS_API_KEY }}
OPENAI__APIKEY: ${{ secrets.OPENAI__APIKEY }}
secrets: inherit
+21 -62
View File
@@ -2,13 +2,7 @@ name: Issue Triage
on:
issues:
types: [opened, typed]
workflow_dispatch:
inputs:
issue_number:
description: Issue number to triage
required: true
type: string
types: [opened, labeled]
permissions:
contents: read
@@ -18,8 +12,9 @@ permissions:
concurrency:
group: >-
issue-triage-${{ github.repository }}-${{
github.event_name == 'workflow_dispatch' && inputs.issue_number
|| github.event.issue.type.name == 'Bug' && github.event.issue.number
((github.event.action == 'opened' && contains(github.event.issue.labels.*.name, 'bug'))
|| (github.event.action == 'labeled' && github.event.label.name == 'bug'))
&& github.event.issue.number
|| github.run_id
}}
cancel-in-progress: true
@@ -29,17 +24,11 @@ env:
DEVFLOW_REF: main
TARGET_REPO_PATH: ${{ github.workspace }}/target-repo
DEVFLOW_PATH: ${{ github.workspace }}/devflow
MODEL_CONFIG_PATH: ${{ github.workspace }}/devflow/config.ci.yaml
jobs:
team_check:
runs-on: ubuntu-latest
environment: github-app-auth
if: >-
${{
github.event_name == 'workflow_dispatch'
|| github.event.issue.type.name == 'Bug'
}}
if: ${{ (github.event.action == 'opened' && contains(github.event.issue.labels.*.name, 'bug')) || (github.event.action == 'labeled' && github.event.label.name == 'bug') }}
outputs:
is_team_member: ${{ steps.check.outputs.is_team_member }}
issue_number: ${{ steps.issue.outputs.issue_number }}
@@ -49,18 +38,14 @@ jobs:
id: issue
shell: bash
env:
ISSUE_NUMBER: >-
${{
github.event_name == 'workflow_dispatch' && inputs.issue_number
|| github.event.issue.number
}}
ISSUE_NUMBER_EVENT: ${{ github.event.issue.number }}
run: |
set -euo pipefail
issue_number="${ISSUE_NUMBER}"
issue_number="${ISSUE_NUMBER_EVENT}"
if [[ ! "$issue_number" =~ ^[1-9][0-9]*$ ]]; then
echo "Could not determine issue number from event payload or manual input." >&2
echo "Could not determine issue number from event payload." >&2
exit 1
fi
@@ -68,38 +53,20 @@ jobs:
echo "repo=${GITHUB_REPOSITORY}" >> "$GITHUB_OUTPUT"
- name: Checkout scripts
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
sparse-checkout: |
.github/actions/github-app-token
.github/scripts
sparse-checkout: .github/scripts
fetch-depth: 1
persist-credentials: false
- name: Get GitHub automation token
id: github-auth
uses: ./.github/actions/github-app-token
with:
mode: ${{ vars.GH_APP_AUTH_MODE }}
azure-client-id: ${{ secrets.GH_APP_AZURE_CLIENT_ID }}
azure-tenant-id: ${{ secrets.GH_APP_AZURE_TENANT_ID }}
azure-subscription-id: ${{ secrets.GH_APP_AZURE_SUBSCRIPTION_ID }}
key-vault-name: ${{ secrets.GH_APP_KEY_VAULT_NAME }}
key-name: ${{ secrets.GH_APP_KEY_NAME }}
github-app-client-id: ${{ secrets.GH_APP_CLIENT_ID }}
github-app-installation-id: ${{ secrets.GH_APP_INSTALLATION_ID }}
repository: ${{ github.repository }}
fallback-token: ${{ secrets.GH_ACTIONS_PR_WRITE }}
- name: Check issue author team membership
if: ${{ github.event_name != 'workflow_dispatch' }}
id: check
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
TEAM_NAME: ${{ secrets.DEVELOPER_TEAM }}
ISSUE_NUMBER: ${{ steps.issue.outputs.issue_number }}
with:
github-token: ${{ steps.github-auth.outputs.token }}
github-token: ${{ secrets.GH_ACTIONS_PR_WRITE }}
script: |
const checkTeamMembership = require('./.github/scripts/check_team_membership.js');
const { author, isTeamMember } = await checkTeamMembership({
@@ -119,23 +86,14 @@ jobs:
triage:
runs-on: ubuntu-latest
needs: team_check
if: >-
${{
github.event_name == 'workflow_dispatch'
|| needs.team_check.outputs.is_team_member == 'false'
}}
if: ${{ needs.team_check.outputs.is_team_member == 'false' }}
environment: integration
permissions:
copilot-requests: write
contents: read
id-token: write
issues: write
timeout-minutes: 60
steps:
# Safe checkout: base repo only.
- name: Checkout target repo base
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
persist-credentials: false
@@ -143,7 +101,7 @@ jobs:
# Private DevFlow (maf-dashboard) checkout.
- name: Checkout DevFlow
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
repository: ${{ env.DEVFLOW_REPOSITORY }}
ref: ${{ env.DEVFLOW_REF }}
@@ -153,12 +111,12 @@ jobs:
path: devflow
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.13"
- name: Set up uv
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
with:
version: "0.11.x"
enable-cache: true
@@ -168,7 +126,7 @@ jobs:
run: uv sync --frozen
- name: Azure CLI Login
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
@@ -178,7 +136,7 @@ jobs:
id: spam
working-directory: ${{ env.DEVFLOW_PATH }}
env:
GITHUB_TOKEN: ${{ github.token }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DEVFLOW_TOKEN: ${{ secrets.DEVFLOW_TOKEN }}
SK_REPO_PATH: ${{ env.TARGET_REPO_PATH }}
AGENT_REPO_PATH: ${{ env.TARGET_REPO_PATH }}
@@ -203,7 +161,8 @@ jobs:
id: repro
working-directory: ${{ env.DEVFLOW_PATH }}
env:
GITHUB_TOKEN: ${{ github.token }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_COPILOT_TOKEN: ${{ secrets.GH_COPILOT_TOKEN }}
# Not seen by the agent prompt; used only to push a paper-trail
# branch back to maf-dashboard at run end.
DEVFLOW_TOKEN: ${{ secrets.DEVFLOW_TOKEN }}
+20 -38
View File
@@ -10,39 +10,12 @@ jobs:
name: "Issue: add labels"
if: ${{ github.event.action == 'opened' || github.event.action == 'reopened' }}
runs-on: ubuntu-latest
environment: github-app-auth
permissions:
contents: read
id-token: write
issues: write
steps:
- name: Checkout GitHub automation
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
sparse-checkout: |
.github/actions/github-app-token
.github/scripts/check_team_membership.js
fetch-depth: 1
persist-credentials: false
- name: Get GitHub automation token
id: github-auth
uses: ./.github/actions/github-app-token
with:
mode: ${{ vars.GH_APP_AUTH_MODE }}
azure-client-id: ${{ secrets.GH_APP_AZURE_CLIENT_ID }}
azure-tenant-id: ${{ secrets.GH_APP_AZURE_TENANT_ID }}
azure-subscription-id: ${{ secrets.GH_APP_AZURE_SUBSCRIPTION_ID }}
key-vault-name: ${{ secrets.GH_APP_KEY_VAULT_NAME }}
key-name: ${{ secrets.GH_APP_KEY_NAME }}
github-app-client-id: ${{ secrets.GH_APP_CLIENT_ID }}
github-app-installation-id: ${{ secrets.GH_APP_INSTALLATION_ID }}
repository: ${{ github.repository }}
fallback-token: ${{ secrets.GH_ACTIONS_PR_WRITE }}
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ steps.github-auth.outputs.token }}
github-token: ${{ secrets.GH_ACTIONS_PR_WRITE }}
script: |
// Get the issue body and title
const body = context.payload.issue.body
@@ -51,14 +24,21 @@ jobs:
// Define the labels array
let labels = []
const checkTeamMembership = require('./.github/scripts/check_team_membership.js')
const { isTeamMember } = await checkTeamMembership({
github,
context,
core,
teamSlug: process.env.TEAM_NAME,
issueNumber: context.issue.number,
})
// 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) {
@@ -110,7 +90,9 @@ jobs:
// Check for issue type from issue form dropdown
const issueTypeField = getFormFieldValue(body, 'Type of Issue')
if (issueTypeField) {
if (issueTypeField === 'Feature Request') {
if (issueTypeField === 'Bug') {
labels.push("bug")
} else if (issueTypeField === 'Feature Request') {
labels.push("enhancement")
} else if (issueTypeField === 'Question') {
labels.push("question")
+3 -41
View File
@@ -6,54 +6,16 @@
# https://github.com/actions/labeler
name: Label pull request
on:
pull_request_target:
types: [opened, synchronize, reopened, edited]
on: [pull_request_target]
jobs:
add_label:
runs-on: ubuntu-latest
environment: github-app-auth
permissions:
contents: read
id-token: write
issues: write
pull-requests: write
steps:
- name: Checkout scripts
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213 # v6
with:
ref: ${{ github.event.pull_request.base.sha }}
sparse-checkout: |
.github/actions/github-app-token
.github/scripts
fetch-depth: 1
persist-credentials: false
- name: Get GitHub automation token
id: github-auth
uses: ./.github/actions/github-app-token
with:
mode: ${{ vars.GH_APP_AUTH_MODE }}
azure-client-id: ${{ secrets.GH_APP_AZURE_CLIENT_ID }}
azure-tenant-id: ${{ secrets.GH_APP_AZURE_TENANT_ID }}
azure-subscription-id: ${{ secrets.GH_APP_AZURE_SUBSCRIPTION_ID }}
key-vault-name: ${{ secrets.GH_APP_KEY_VAULT_NAME }}
key-name: ${{ secrets.GH_APP_KEY_NAME }}
github-app-client-id: ${{ secrets.GH_APP_CLIENT_ID }}
github-app-installation-id: ${{ secrets.GH_APP_INSTALLATION_ID }}
repository: ${{ github.repository }}
fallback-token: ${{ secrets.GH_ACTIONS_PR_WRITE }}
- uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213 # v6.1.0
with:
repo-token: ${{ steps.github-auth.outputs.token }}
- name: "PR: add breaking change label from title"
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ steps.github-auth.outputs.token }}
script: |
const { syncBreakingChangeLabelFromTitle } = require('./.github/scripts/title_prefix.js');
await syncBreakingChangeLabelFromTitle({ github, context, core });
repo-token: "${{ secrets.GH_ACTIONS_PR_WRITE }}"
+51 -10
View File
@@ -15,17 +15,58 @@ jobs:
pull-requests: write
steps:
- name: Checkout scripts
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
sparse-checkout: .github/scripts
fetch-depth: 1
persist-credentials: false
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
name: "Issue/PR: update title"
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { updateTitleForAddedLabel } = require('./.github/scripts/title_prefix.js');
await updateTitleForAddedLabel({ github, context, core });
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);
}
}
+8 -47
View File
@@ -6,7 +6,6 @@ on:
permissions:
contents: read
id-token: write
issues: write
pull-requests: write
@@ -22,43 +21,24 @@ env:
jobs:
team_check:
runs-on: ubuntu-latest
environment: github-app-auth
outputs:
is_team_member: ${{ steps.check.outputs.is_team_member }}
steps:
- name: Checkout scripts
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ github.event.pull_request.base.sha }}
sparse-checkout: |
.github/actions/github-app-token
.github/scripts
sparse-checkout: .github/scripts
fetch-depth: 1
persist-credentials: false
- name: Get GitHub automation token
id: github-auth
uses: ./.github/actions/github-app-token
with:
mode: ${{ vars.GH_APP_AUTH_MODE }}
azure-client-id: ${{ secrets.GH_APP_AZURE_CLIENT_ID }}
azure-tenant-id: ${{ secrets.GH_APP_AZURE_TENANT_ID }}
azure-subscription-id: ${{ secrets.GH_APP_AZURE_SUBSCRIPTION_ID }}
key-vault-name: ${{ secrets.GH_APP_KEY_VAULT_NAME }}
key-name: ${{ secrets.GH_APP_KEY_NAME }}
github-app-client-id: ${{ secrets.GH_APP_CLIENT_ID }}
github-app-installation-id: ${{ secrets.GH_APP_INSTALLATION_ID }}
repository: ${{ github.repository }}
fallback-token: ${{ secrets.GH_ACTIONS_PR_WRITE }}
- name: Check PR author team membership
id: check
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
TEAM_NAME: ${{ secrets.DEVELOPER_TEAM }}
PR_NUMBER: ${{ github.event.pull_request.number }}
with:
github-token: ${{ steps.github-auth.outputs.token }}
github-token: ${{ secrets.GH_ACTIONS_PR_WRITE }}
script: |
const checkTeamMembership = require('./.github/scripts/check_team_membership.js');
const { author, isTeamMember } = await checkTeamMembership({
@@ -77,39 +57,20 @@ jobs:
limit_open_prs:
runs-on: ubuntu-latest
environment: github-app-auth
needs: team_check
if: ${{ needs.team_check.outputs.is_team_member == 'false' }}
steps:
- name: Checkout scripts
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ github.event.pull_request.base.sha }}
sparse-checkout: |
.github/actions/github-app-token
.github/scripts
sparse-checkout: .github/scripts
fetch-depth: 1
persist-credentials: false
- name: Get GitHub automation token
id: github-auth
uses: ./.github/actions/github-app-token
with:
mode: ${{ vars.GH_APP_AUTH_MODE }}
azure-client-id: ${{ secrets.GH_APP_AZURE_CLIENT_ID }}
azure-tenant-id: ${{ secrets.GH_APP_AZURE_TENANT_ID }}
azure-subscription-id: ${{ secrets.GH_APP_AZURE_SUBSCRIPTION_ID }}
key-vault-name: ${{ secrets.GH_APP_KEY_VAULT_NAME }}
key-name: ${{ secrets.GH_APP_KEY_NAME }}
github-app-client-id: ${{ secrets.GH_APP_CLIENT_ID }}
github-app-installation-id: ${{ secrets.GH_APP_INSTALLATION_ID }}
repository: ${{ github.repository }}
fallback-token: ${{ secrets.GH_ACTIONS_PR_WRITE }}
- name: Enforce open PR limit
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
github-token: ${{ steps.github-auth.outputs.token }}
github-token: ${{ secrets.GH_ACTIONS_PR_WRITE }}
script: |
const { enforcePrLimit } = require('./.github/scripts/pr_limit_moderation.js');
await enforcePrLimit({
+3 -3
View File
@@ -19,12 +19,12 @@ jobs:
runs-on: ubuntu-22.04
# check out the latest version of the code
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Set up Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
uses: actions/setup-node@v4
with:
node-version: 20
@@ -33,7 +33,7 @@ jobs:
# Checks the status of hyperlinks in all files
- name: Run linkspector
uses: umbrelladocs/action-linkspector@963b6264d7de32c904942a70b488d3407453049e # v1.5.1
uses: umbrelladocs/action-linkspector@963b6264d7de32c904942a70b488d3407453049e # v1
with:
reporter: local
filter_mode: nofilter
+2 -2
View File
@@ -19,7 +19,7 @@ jobs:
steps:
- name: Wait for required checks
if: github.event_name == 'pull_request'
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
TIMEOUT_SECONDS: "3600"
INTERVAL_SECONDS: "30"
@@ -27,7 +27,7 @@ jobs:
# "Cleanup artifacts", "Agent", "Prepare", and "Upload results" are check runs
# created by an org-level GitHub App (MSDO), not by any workflow in this repo.
# They are outside our control and their transient failures should not block merges.
IGNORED_NAMES: "CodeQL,CodeQL analysis (csharp),Cleanup artifacts,Agent,Prepare,Upload results,review"
IGNORED_NAMES: "CodeQL,CodeQL analysis (csharp),Cleanup artifacts,Agent,Prepare,Upload results"
with:
script: |
const timeoutSeconds = Number(process.env.TIMEOUT_SECONDS);
+382
View File
@@ -0,0 +1,382 @@
#!/usr/bin/env python3
# Copyright (c) Microsoft. All rights reserved.
"""Check Python test coverage against threshold for enforced targets.
This script parses a Cobertura XML coverage report and enforces a minimum
coverage threshold on specific targets. Targets can be package names
(e.g., "packages.core.agent_framework") or individual Python file paths
(e.g., "packages/core/agent_framework/observability.py").
Non-enforced targets are reported for visibility but don't block the build.
Usage:
python python-check-coverage.py <coverage-xml-path> <threshold>
Example:
python python-check-coverage.py python-coverage.xml 85
"""
import sys
import xml.etree.ElementTree as ET
from dataclasses import dataclass
# =============================================================================
# ENFORCED TARGETS CONFIGURATION
# =============================================================================
# Add or remove entries from this set to control which targets must meet
# the coverage threshold. Only these targets will fail the build if below
# threshold. Other targets are reported for visibility only.
#
# Target values can be:
# - Package paths as they appear in the coverage report
# (e.g., "packages.azure-ai.agent_framework_azure_ai")
# - Python source file paths as they appear in the coverage report
# (e.g., "packages/core/agent_framework/observability.py")
# =============================================================================
ENFORCED_TARGETS: set[str] = {
# Packages (sorted alphabetically)
"packages.anthropic.agent_framework_anthropic",
"packages.azure-ai-search.agent_framework_azure_ai_search",
"packages.core.agent_framework",
"packages.core.agent_framework._workflows",
"packages.foundry.agent_framework_foundry",
"packages.openai.agent_framework_openai",
"packages.purview.agent_framework_purview",
# Individual files (if you want to enforce specific files instead of whole packages)
"packages/core/agent_framework/observability.py",
# Add more targets here as coverage improves
}
@dataclass
class PackageCoverage:
"""Coverage data for a single package."""
name: str
line_rate: float
branch_rate: float
lines_valid: int
lines_covered: int
branches_valid: int
branches_covered: int
@property
def line_coverage_percent(self) -> float:
"""Return line coverage as a percentage."""
return self.line_rate * 100
@property
def branch_coverage_percent(self) -> float:
"""Return branch coverage as a percentage."""
return self.branch_rate * 100
def normalize_coverage_path(path: str) -> str:
"""Normalize coverage paths for reliable matching."""
return path.replace("\\", "/").lstrip("./")
def parse_coverage_xml(
xml_path: str,
) -> tuple[dict[str, PackageCoverage], dict[str, PackageCoverage], float, float]:
"""Parse Cobertura XML and extract per-package coverage data.
Args:
xml_path: Path to the Cobertura XML coverage report.
Returns:
A tuple of (packages_dict, files_dict, overall_line_rate, overall_branch_rate).
"""
tree = ET.parse(xml_path)
root = tree.getroot()
# Get overall coverage from root element
overall_line_rate = float(root.get("line-rate", 0))
overall_branch_rate = float(root.get("branch-rate", 0))
packages: dict[str, PackageCoverage] = {}
file_stats: dict[str, dict[str, int]] = {}
for package in root.findall(".//package"):
package_path = package.get("name", "unknown")
line_rate = float(package.get("line-rate", 0))
branch_rate = float(package.get("branch-rate", 0))
# Count lines and branches from classes within this package
lines_valid = 0
lines_covered = 0
branches_valid = 0
branches_covered = 0
for class_elem in package.findall(".//class"):
file_path = normalize_coverage_path(class_elem.get("filename", ""))
if file_path and file_path not in file_stats:
file_stats[file_path] = {
"lines_valid": 0,
"lines_covered": 0,
"branches_valid": 0,
"branches_covered": 0,
}
for line in class_elem.findall(".//line"):
lines_valid += 1
if int(line.get("hits", 0)) > 0:
lines_covered += 1
if file_path:
file_stats[file_path]["lines_valid"] += 1
if int(line.get("hits", 0)) > 0:
file_stats[file_path]["lines_covered"] += 1
# Branch coverage from line elements
if line.get("branch") == "true":
condition_coverage = line.get("condition-coverage", "")
if condition_coverage:
# Parse "X% (covered/total)" format
try:
coverage_parts = (
condition_coverage.split("(")[1].rstrip(")").split("/")
)
branches_covered += int(coverage_parts[0])
branches_valid += int(coverage_parts[1])
if file_path:
file_stats[file_path]["branches_covered"] += int(
coverage_parts[0]
)
file_stats[file_path]["branches_valid"] += int(
coverage_parts[1]
)
except (IndexError, ValueError):
# Ignore malformed condition-coverage strings; treat this line as having no branch data.
pass
# Use full package path as the key (no aggregation)
packages[package_path] = PackageCoverage(
name=package_path,
line_rate=line_rate if lines_valid == 0 else lines_covered / lines_valid,
branch_rate=branch_rate
if branches_valid == 0
else branches_covered / branches_valid,
lines_valid=lines_valid,
lines_covered=lines_covered,
branches_valid=branches_valid,
branches_covered=branches_covered,
)
files: dict[str, PackageCoverage] = {}
for file_path, stats in file_stats.items():
lines_valid = stats["lines_valid"]
lines_covered = stats["lines_covered"]
branches_valid = stats["branches_valid"]
branches_covered = stats["branches_covered"]
files[file_path] = PackageCoverage(
name=file_path,
line_rate=0 if lines_valid == 0 else lines_covered / lines_valid,
branch_rate=0 if branches_valid == 0 else branches_covered / branches_valid,
lines_valid=lines_valid,
lines_covered=lines_covered,
branches_valid=branches_valid,
branches_covered=branches_covered,
)
return packages, files, overall_line_rate, overall_branch_rate
def format_coverage_value(coverage: float, threshold: float, is_enforced: bool) -> str:
"""Format a coverage value with optional pass/fail indicator.
Args:
coverage: Coverage percentage (0-100).
threshold: Minimum required coverage percentage.
is_enforced: Whether this target is enforced.
Returns:
Formatted string like "85.5%" or "85.5%" or "75.0%".
"""
formatted = f"{coverage:.1f}%"
if is_enforced:
icon = "" if coverage >= threshold else ""
formatted = f"{formatted} {icon}"
return formatted
def print_coverage_table(
packages: dict[str, PackageCoverage],
files: dict[str, PackageCoverage],
threshold: float,
overall_line_rate: float,
overall_branch_rate: float,
) -> None:
"""Print a formatted coverage summary table.
Args:
packages: Dictionary of package name to coverage data.
files: Dictionary of file path to coverage data, used for per-file enforcement.
threshold: Minimum required coverage percentage.
overall_line_rate: Overall line coverage rate (0-1).
overall_branch_rate: Overall branch coverage rate (0-1).
"""
print("\n" + "=" * 80)
print("PYTHON TEST COVERAGE REPORT")
print("=" * 80)
# Overall coverage
print(f"\nOverall Line Coverage: {overall_line_rate * 100:.1f}%")
print(f"Overall Branch Coverage: {overall_branch_rate * 100:.1f}%")
print(f"Threshold: {threshold}%")
enforced_targets = {normalize_coverage_path(t) for t in ENFORCED_TARGETS}
# Package table
print("\n" + "-" * 110)
print(f"{'Package':<80} {'Lines':<15} {'Line Cov':<15}")
print("-" * 110)
# Sort: enforced package targets first, then alphabetically
sorted_packages = sorted(
packages.values(),
key=lambda p: (p.name not in ENFORCED_TARGETS, p.name),
)
for pkg in sorted_packages:
is_enforced = normalize_coverage_path(pkg.name) in enforced_targets
enforced_marker = "[ENFORCED] " if is_enforced else ""
line_cov = format_coverage_value(
pkg.line_coverage_percent, threshold, is_enforced
)
lines_info = f"{pkg.lines_covered}/{pkg.lines_valid}"
package_label = f"{enforced_marker}{pkg.name}"
print(f"{package_label:<80} {lines_info:<15} {line_cov:<15}")
print("-" * 110)
# Enforced file/model entries (if configured)
enforced_files = [
files[target]
for target in sorted(enforced_targets)
if target in files and target.endswith(".py")
]
if enforced_files:
print("\nEnforced Files/Models")
print("-" * 110)
print(f"{'File':<80} {'Lines':<15} {'Line Cov':<15}")
print("-" * 110)
for file_cov in enforced_files:
line_cov = format_coverage_value(
file_cov.line_coverage_percent, threshold, True
)
lines_info = f"{file_cov.lines_covered}/{file_cov.lines_valid}"
print(f"[ENFORCED] {file_cov.name:<69} {lines_info:<15} {line_cov:<15}")
print("-" * 110)
def check_coverage(xml_path: str, threshold: float) -> bool:
"""Check if all enforced targets meet the coverage threshold.
Args:
xml_path: Path to the Cobertura XML coverage report.
threshold: Minimum required coverage percentage.
Returns:
True if all enforced targets pass, False otherwise.
"""
packages, files, overall_line_rate, overall_branch_rate = parse_coverage_xml(
xml_path
)
print_coverage_table(
packages, files, threshold, overall_line_rate, overall_branch_rate
)
# Check enforced targets
failed_targets: list[str] = []
missing_targets: list[str] = []
for target_name in ENFORCED_TARGETS:
normalized_target = normalize_coverage_path(target_name)
package_alias = normalized_target.replace("/", ".")
target_coverage = None
if target_name in packages:
target_coverage = packages[target_name]
elif normalized_target in files:
target_coverage = files[normalized_target]
elif package_alias in packages:
target_coverage = packages[package_alias]
if target_coverage is None:
missing_targets.append(target_name)
continue
if target_coverage.line_coverage_percent < threshold:
failed_targets.append(
f"{target_name} ({target_coverage.line_coverage_percent:.1f}%)"
)
# Report results
if missing_targets:
print(
f"\n❌ FAILED: Enforced targets not found in coverage report: {', '.join(missing_targets)}"
)
return False
if failed_targets:
print(
f"\n❌ FAILED: The following enforced targets are below {threshold}% coverage threshold:"
)
for target in failed_targets:
print(f" - {target}")
print("\nTo fix: Add more tests to improve coverage for the failing targets.")
return False
if ENFORCED_TARGETS:
found_enforced = [
target
for target in ENFORCED_TARGETS
if target in packages or normalize_coverage_path(target) in files
]
if found_enforced:
print(
f"\n✅ PASSED: All enforced targets meet the {threshold}% coverage threshold."
)
return True
def main() -> int:
"""Main entry point.
Returns:
Exit code: 0 for success, 1 for failure.
"""
if len(sys.argv) != 3:
print(f"Usage: {sys.argv[0]} <coverage-xml-path> <threshold>")
print(f"Example: {sys.argv[0]} python-coverage.xml 85")
return 1
xml_path = sys.argv[1]
try:
threshold = float(sys.argv[2])
except ValueError:
print(f"Error: Invalid threshold value: {sys.argv[2]}")
return 1
try:
success = check_coverage(xml_path, threshold)
return 0 if success else 1
except FileNotFoundError:
print(f"Error: Coverage file not found: {xml_path}")
return 1
except ET.ParseError as e:
print(f"Error: Failed to parse coverage XML: {e}")
return 1
if __name__ == "__main__":
sys.exit(main())
+12 -14
View File
@@ -6,10 +6,6 @@ on:
branches: ["main"]
paths:
- "python/**"
- "!python/AGENTS.md"
- "!python/**/AGENTS.md"
- "!python/.github/skills/*"
- "!python/.github/skills/**"
env:
# Configure a constant location for the uv cache
@@ -31,7 +27,7 @@ jobs:
env:
UV_PYTHON: ${{ matrix.python-version }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
- name: Set up python and install the project
@@ -42,11 +38,11 @@ jobs:
os: ${{ runner.os }}
env:
UV_CACHE_DIR: /tmp/.uv-cache
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: ~/.cache/prek
key: prek|${{ matrix.python-version }}|${{ hashFiles('python/.pre-commit-config.yaml') }}
- uses: j178/prek-action@bdca6f102f98e2b4c7029491a53dfd366469e33d # v2.0.4
- uses: j178/prek-action@0bb87d7f00b0c99306c8bcb8b8beba1eb581c037 # v1
name: Run Pre-commit Hooks (excluding poe-check)
env:
SKIP: poe-check
@@ -68,7 +64,7 @@ jobs:
env:
UV_PYTHON: ${{ matrix.python-version }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
- name: Set up python and install the project
@@ -97,7 +93,7 @@ jobs:
env:
UV_PYTHON: ${{ matrix.python-version }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
- name: Set up python and install the project
@@ -113,8 +109,8 @@ jobs:
- name: Run markdown code lint
run: uv run poe markdown-code-lint
test-typing:
name: Test Typing Checks
mypy:
name: Mypy Checks
if: "!cancelled()"
strategy:
fail-fast: false
@@ -128,7 +124,7 @@ jobs:
env:
UV_PYTHON: ${{ matrix.python-version }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
- name: Set up python and install the project
@@ -139,5 +135,7 @@ jobs:
os: ${{ runner.os }}
env:
UV_CACHE_DIR: /tmp/.uv-cache
- name: Run tests/samples type checkers (mypy, pyrefly, ty)
run: uv run python scripts/workspace_poe_tasks.py ci-test-typing
- name: Run Mypy
env:
GITHUB_BASE_REF: ${{ github.event.pull_request.base.ref || github.base_ref || 'main' }}
run: uv run python scripts/workspace_poe_tasks.py ci-mypy
@@ -1,431 +0,0 @@
name: Python - Dependency Maintenance
on:
workflow_dispatch:
schedule:
- cron: "0 4 * * 1"
permissions:
contents: write
issues: write
concurrency:
group: python-dependency-maintenance
cancel-in-progress: false
env:
UV_CACHE_DIR: /tmp/.uv-cache
jobs:
dependency-maintenance:
name: Dependency Maintenance
runs-on: ubuntu-latest
env:
# Match the existing Python dependency maintenance workflows. Reevaluate if package
# installability starts differing across supported Python versions.
UV_PYTHON: "3.13"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Set up python and install the project
uses: ./.github/actions/python-setup
with:
python-version: ${{ env.UV_PYTHON }}
os: ${{ runner.os }}
env:
UV_CACHE_DIR: /tmp/.uv-cache
- name: Set dependency release cutoff
run: |
cutoff="$(date -u -d '7 days ago' '+%Y-%m-%dT%H:%M:%SZ')"
echo "DEPENDENCY_RELEASE_CUTOFF=${cutoff}" >> "$GITHUB_ENV"
echo "Using dependency release cutoff: ${cutoff}"
- name: Repin dev dependency declarations
run: uv run poe upgrade-dev-dependency-pins
working-directory: ./python
- name: Refresh lockfile after dev pin updates
run: uv lock
working-directory: ./python
- name: Save dev dependency changes
run: |
DEV_PATCH="${RUNNER_TEMP}/python-dev-dependency-updates.patch"
git diff -- python/pyproject.toml "python/packages/*/pyproject.toml" python/uv.lock > "${DEV_PATCH}"
if [ -s "${DEV_PATCH}" ]; then
echo "has_dev_changes=true" >> "$GITHUB_OUTPUT"
else
echo "has_dev_changes=false" >> "$GITHUB_OUTPUT"
fi
echo "patch=${DEV_PATCH}" >> "$GITHUB_OUTPUT"
id: dev_changes
- name: Run dependency bounds test scenarios
id: validate_bounds_test
continue-on-error: true
run: uv run poe validate-dependency-bounds-test --package "*"
working-directory: ./python
- name: Run dependency upper-bound validation
id: validate_ranges
if: steps.validate_bounds_test.outcome == 'success'
continue-on-error: true
run: uv run poe validate-dependency-bounds-project --mode upper --package "*"
working-directory: ./python
- name: Upload dependency validation reports
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: dependency-maintenance-results
path: |
python/scripts/dependencies/dependency-bounds-test-results.json
python/scripts/dependencies/dependency-range-results.json
if-no-files-found: warn
- name: Create issue for failed dependency bounds test
if: steps.validate_bounds_test.outcome != 'success'
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const fs = require("fs")
const reportPath = "python/scripts/dependencies/dependency-bounds-test-results.json"
const owner = context.repo.owner
const repo = context.repo.repo
const openIssues = await github.paginate(github.rest.issues.listForRepo, {
owner,
repo,
state: "open",
per_page: 100,
})
const openIssueTitles = new Set(
openIssues.filter((issue) => !issue.pull_request).map((issue) => issue.title)
)
const formatError = (message) => String(message || "No error output captured.").replace(/```/g, "'''")
const title = "Dependency bounds test failed"
if (openIssueTitles.has(title)) {
core.info(`Issue already exists: ${title}`)
return
}
const bodyLines = [
"Automated dependency bounds test mode failed before dependency upper-bound validation could run.",
"",
"The weekly dependency maintenance workflow kept only dev dependency updates for the generated PR, if any, and skipped dependency range updates for this run.",
"",
]
if (fs.existsSync(reportPath)) {
const report = JSON.parse(fs.readFileSync(reportPath, "utf8"))
const failedScenarios = (report.scenarios ?? []).filter((scenario) => scenario.status === "failed")
for (const scenario of failedScenarios) {
bodyLines.push(`### ${scenario.name} scenario (${scenario.resolution})`)
const failedPackages = (scenario.packages ?? []).filter((pkg) => pkg.status === "failed")
for (const pkg of failedPackages.slice(0, 10)) {
bodyLines.push(
"",
`- Package: \`${pkg.package_name}\``,
`- Project path: \`${pkg.project_path}\``,
"",
"```",
formatError(pkg.error).slice(0, 3500),
"```"
)
}
if (failedPackages.length > 10) {
bodyLines.push("", `_Additional failed packages omitted: ${failedPackages.length - 10}_`)
}
}
} else {
bodyLines.push(`No dependency bounds test report was found at \`${reportPath}\`.`)
}
bodyLines.push("", `Workflow run: ${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`)
await github.rest.issues.create({
owner,
repo,
title,
body: bodyLines.join("\n"),
})
core.info(`Created issue: ${title}`)
- name: Create issues for failed dependency candidates
if: always()
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const fs = require("fs")
const reportPath = "python/scripts/dependencies/dependency-range-results.json"
if (!fs.existsSync(reportPath)) {
core.info(`No dependency range report found at ${reportPath}`)
return
}
const report = JSON.parse(fs.readFileSync(reportPath, "utf8"))
const dependencyFailures = []
for (const packageResult of report.packages ?? []) {
for (const dependency of packageResult.dependencies ?? []) {
const candidateVersions = new Set(dependency.candidate_versions ?? [])
const failedAttempts = (dependency.attempts ?? []).filter(
(attempt) => attempt.status === "failed" && candidateVersions.has(attempt.trial_upper)
)
if (!failedAttempts.length) {
continue
}
const failuresByVersion = new Map()
for (const attempt of failedAttempts) {
const version = attempt.trial_upper || "unknown"
if (!failuresByVersion.has(version)) {
failuresByVersion.set(version, attempt.error || "No error output captured.")
}
}
dependencyFailures.push({
packageName: packageResult.package_name,
projectPath: packageResult.project_path,
dependencyName: dependency.name,
originalRequirements: dependency.original_requirements ?? [],
finalRequirements: dependency.final_requirements ?? [],
failedVersions: [...failuresByVersion.entries()].map(([version, error]) => ({ version, error })),
})
}
}
if (!dependencyFailures.length) {
core.info("No failing dependency candidates found.")
return
}
const owner = context.repo.owner
const repo = context.repo.repo
const openIssues = await github.paginate(github.rest.issues.listForRepo, {
owner,
repo,
state: "open",
per_page: 100,
})
const openIssueTitles = new Set(
openIssues.filter((issue) => !issue.pull_request).map((issue) => issue.title)
)
const formatError = (message) => String(message || "No error output captured.").replace(/```/g, "'''")
for (const failure of dependencyFailures) {
const title = `Dependency validation failed: ${failure.dependencyName} (${failure.packageName})`
if (openIssueTitles.has(title)) {
core.info(`Issue already exists: ${title}`)
continue
}
const visibleFailures = failure.failedVersions.slice(0, 5)
const omittedCount = failure.failedVersions.length - visibleFailures.length
const failureDetails = visibleFailures
.map(
(entry) =>
`- \`${entry.version}\`\n\n\`\`\`\n${formatError(entry.error).slice(0, 3500)}\n\`\`\``
)
.join("\n\n")
const body = [
"Automated dependency range validation found candidate versions that failed checks.",
"",
`- Package: \`${failure.packageName}\``,
`- Project path: \`${failure.projectPath}\``,
`- Dependency: \`${failure.dependencyName}\``,
`- Original requirements: ${
failure.originalRequirements.length
? failure.originalRequirements.map((value) => `\`${value}\``).join(", ")
: "_none_"
}`,
`- Final requirements after run: ${
failure.finalRequirements.length
? failure.finalRequirements.map((value) => `\`${value}\``).join(", ")
: "_none_"
}`,
"",
"### Failed versions and errors",
failureDetails,
omittedCount > 0 ? `\n_Additional failed versions omitted: ${omittedCount}_` : "",
"",
`Workflow run: ${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`,
].join("\n")
await github.rest.issues.create({
owner,
repo,
title,
body,
})
openIssueTitles.add(title)
core.info(`Created issue: ${title}`)
}
- name: Keep only dev updates when range validation fails
if: steps.validate_bounds_test.outcome != 'success' || steps.validate_ranges.outcome != 'success'
env:
DEV_PATCH: ${{ steps.dev_changes.outputs.patch }}
HAS_DEV_CHANGES: ${{ steps.dev_changes.outputs.has_dev_changes }}
run: |
git restore python/pyproject.toml python/packages/*/pyproject.toml python/uv.lock
if [ "${HAS_DEV_CHANGES}" = "true" ]; then
git apply "${DEV_PATCH}"
fi
- name: Refresh lockfile after dependency range updates
if: steps.validate_bounds_test.outcome == 'success' && steps.validate_ranges.outcome == 'success'
run: uv lock
working-directory: ./python
- name: Install final dependency set
run: uv run poe install
working-directory: ./python
- name: Run final checks
run: uv run poe check
working-directory: ./python
- name: Run final typing
run: uv run poe typing
working-directory: ./python
- name: Commit and push dependency updates
id: commit_updates
run: |
BRANCH="automation/python-dependency-maintenance"
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git checkout -B "${BRANCH}"
git add python/pyproject.toml python/packages/*/pyproject.toml python/uv.lock
if git diff --cached --quiet; then
echo "has_changes=false" >> "$GITHUB_OUTPUT"
echo "No dependency updates to commit."
exit 0
fi
git commit -m "Python: chore: update dependencies"
git push --force-with-lease --set-upstream origin "${BRANCH}"
echo "has_changes=true" >> "$GITHUB_OUTPUT"
- name: Create or update dependency maintenance tracking issue
if: steps.commit_updates.outputs.has_changes == 'true'
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const branch = "automation/python-dependency-maintenance"
const prTitle = "Python: chore: update dependencies"
const issueTitle = "Python dependency maintenance PR needed"
const owner = context.repo.owner
const repo = context.repo.repo
const branchRef = await github.rest.git.getRef({
owner,
repo,
ref: `heads/${branch}`,
})
const branchSha = branchRef.data.object.sha
const runUrl = `${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`
const compareUrl = `${context.serverUrl}/${owner}/${repo}/compare/main...${branch}`
const prBody = [
"### Motivation & Context",
"",
"This automated update keeps Python dependency metadata coherent across the uv workspace. Python dependencies can be declared in multiple `pyproject.toml` files, but the workspace has one shared `python/uv.lock`, so dependency maintenance should update and validate them together instead of through per-manifest Dependabot PRs.",
"",
"### Description & Review Guide",
"",
"- **What are the major changes?** Refresh Python dev dependency pins, update package dependency ranges when the bounds tooling succeeds, and refresh `python/uv.lock`.",
"- **What is the impact of these changes?** Keeps the Python workspace dependency set current while producing at most one dependency PR for the week. If dependency range validation fails, this PR contains only the dev dependency updates that still pass final validation, and separate issues track failed range candidates.",
"- **What do you want reviewers to focus on?** Review the generated dependency metadata changes and any dependency-range updates for package-specific compatibility concerns.",
'<!-- NOTE FOR AUTOMATED/AI REVIEWERS: the "What do you want reviewers to focus on?"',
" item above is intended for human reviewers only. Automated/AI reviewers should",
" ignore it and review the entire change rather than narrowing scope to it. -->",
"",
"",
"### Related Issue",
"",
"No linked issue; this PR is generated by scheduled Python dependency maintenance.",
"",
"### Contribution Checklist",
"",
"- [x] The code builds clean without any errors or warnings",
"- [x] All unit tests pass, and I have added new tests where possible",
"- [x] The PR follows the [Contribution Guidelines](https://github.com/microsoft/agent-framework/blob/main/CONTRIBUTING.md)",
"- [ ] This PR is linked to an issue and there is no other open PR for this issue (see Related Issue above).",
'- [x] **This is not a breaking change.** If it _is_ a breaking change, add the `breaking change` label (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.',
].join("\n")
const prBodyFence = "```"
const command = [
"PR_BODY_FILE=\"$(mktemp)\"",
`cat > "$PR_BODY_FILE" <<'EOF'`,
prBody,
"EOF",
"gh pr create --repo microsoft/agent-framework --base main \\",
` --head ${owner}:${branch} \\`,
` --title "${prTitle}" \\`,
" --body-file \"$PR_BODY_FILE\"",
].join("\n")
const issueBody = [
"The Python dependency maintenance workflow generated and validated dependency updates, then pushed them to the automation branch.",
"",
`- Branch: \`${branch}\``,
`- Commit: \`${branchSha}\``,
`- Compare: ${compareUrl}`,
`- Workflow run: ${runUrl}`,
"",
"GitHub Actions is not permitted to create pull requests in this repository, so a maintainer needs to create the PR manually.",
"",
"### Create the PR",
"",
"```bash",
command,
"```",
"",
"### Generated PR body",
"",
prBodyFence,
prBody,
prBodyFence,
].join("\n")
const openIssues = await github.paginate(github.rest.issues.listForRepo, {
owner,
repo,
state: "open",
per_page: 100,
})
const existingIssue = openIssues.find((issue) => !issue.pull_request && issue.title === issueTitle)
if (existingIssue) {
await github.rest.issues.update({
owner,
repo,
issue_number: existingIssue.number,
title: issueTitle,
body: issueBody,
})
core.info(`Updated issue #${existingIssue.number}: ${issueTitle}`)
} else {
const createdIssue = await github.rest.issues.create({
owner,
repo,
title: issueTitle,
body: issueBody,
})
core.info(`Created issue #${createdIssue.data.number}: ${issueTitle}`)
}
@@ -0,0 +1,216 @@
# Probe the highest allowed dependency versions, then open issues/PRs from the passing updates.
name: Python - Dependency Range Validation
on:
workflow_dispatch:
permissions:
contents: write
issues: write
pull-requests: write
env:
UV_CACHE_DIR: /tmp/.uv-cache
jobs:
dependency-range-validation:
name: Dependency Range Validation
runs-on: ubuntu-latest
env:
# For now only run 3.13, if we do encounter situations where there are mismatches between packages and python versions (other then 3.10 and 3.14 which are known to not be able to install everything)
# then we will have to reevaluate.
UV_PYTHON: "3.13"
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
- name: Set up python and install the project
uses: ./.github/actions/python-setup
with:
python-version: ${{ env.UV_PYTHON }}
os: ${{ runner.os }}
env:
UV_CACHE_DIR: /tmp/.uv-cache
- name: Run dependency range validation
id: validate_ranges
# Keep workflow running so we can still publish diagnostics from this run.
continue-on-error: true
run: uv run poe validate-dependency-bounds-project --mode upper --package "*"
working-directory: ./python
- name: Upload dependency range report
# Always publish the report so failures are inspectable even when validation fails.
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: dependency-range-results
path: python/scripts/dependencies/dependency-range-results.json
if-no-files-found: warn
- name: Create issues for failed dependency candidates
# Always process the report so failed candidates create actionable tracking issues.
if: always()
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
const fs = require("fs")
const reportPath = "python/scripts/dependencies/dependency-range-results.json"
if (!fs.existsSync(reportPath)) {
core.warning(`No dependency range report found at ${reportPath}`)
return
}
const report = JSON.parse(fs.readFileSync(reportPath, "utf8"))
const dependencyFailures = []
for (const packageResult of report.packages ?? []) {
for (const dependency of packageResult.dependencies ?? []) {
const candidateVersions = new Set(dependency.candidate_versions ?? [])
const failedAttempts = (dependency.attempts ?? []).filter(
(attempt) => attempt.status === "failed" && candidateVersions.has(attempt.trial_upper)
)
if (!failedAttempts.length) {
continue
}
const failuresByVersion = new Map()
for (const attempt of failedAttempts) {
const version = attempt.trial_upper || "unknown"
if (!failuresByVersion.has(version)) {
failuresByVersion.set(version, attempt.error || "No error output captured.")
}
}
dependencyFailures.push({
packageName: packageResult.package_name,
projectPath: packageResult.project_path,
dependencyName: dependency.name,
originalRequirements: dependency.original_requirements ?? [],
finalRequirements: dependency.final_requirements ?? [],
failedVersions: [...failuresByVersion.entries()].map(([version, error]) => ({ version, error })),
})
}
}
if (!dependencyFailures.length) {
core.info("No failing dependency candidates found.")
return
}
const owner = context.repo.owner
const repo = context.repo.repo
const openIssues = await github.paginate(github.rest.issues.listForRepo, {
owner,
repo,
state: "open",
per_page: 100,
})
const openIssueTitles = new Set(
openIssues.filter((issue) => !issue.pull_request).map((issue) => issue.title)
)
const formatError = (message) => String(message || "No error output captured.").replace(/```/g, "'''")
for (const failure of dependencyFailures) {
const title = `Dependency validation failed: ${failure.dependencyName} (${failure.packageName})`
if (openIssueTitles.has(title)) {
core.info(`Issue already exists: ${title}`)
continue
}
const visibleFailures = failure.failedVersions.slice(0, 5)
const omittedCount = failure.failedVersions.length - visibleFailures.length
const failureDetails = visibleFailures
.map(
(entry) =>
`- \`${entry.version}\`\n\n\`\`\`\n${formatError(entry.error).slice(0, 3500)}\n\`\`\``
)
.join("\n\n")
const body = [
"Automated dependency range validation found candidate versions that failed checks.",
"",
`- Package: \`${failure.packageName}\``,
`- Project path: \`${failure.projectPath}\``,
`- Dependency: \`${failure.dependencyName}\``,
`- Original requirements: ${
failure.originalRequirements.length
? failure.originalRequirements.map((value) => `\`${value}\``).join(", ")
: "_none_"
}`,
`- Final requirements after run: ${
failure.finalRequirements.length
? failure.finalRequirements.map((value) => `\`${value}\``).join(", ")
: "_none_"
}`,
"",
"### Failed versions and errors",
failureDetails,
omittedCount > 0 ? `\n_Additional failed versions omitted: ${omittedCount}_` : "",
"",
`Workflow run: ${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`,
].join("\n")
await github.rest.issues.create({
owner,
repo,
title,
body,
})
openIssueTitles.add(title)
core.info(`Created issue: ${title}`)
}
- name: Refresh lockfile
# Only refresh lockfile after a clean validation to avoid committing known-bad ranges.
if: steps.validate_ranges.outcome == 'success'
run: uv lock --upgrade
working-directory: ./python
- name: Commit and push dependency updates
id: commit_updates
if: steps.validate_ranges.outcome == 'success'
run: |
BRANCH="automation/python-dependency-range-updates"
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git checkout -B "${BRANCH}"
git add python/packages/*/pyproject.toml python/uv.lock
if git diff --cached --quiet; then
echo "has_changes=false" >> "$GITHUB_OUTPUT"
echo "No dependency updates to commit."
exit 0
fi
git commit -m "chore: update dependency ranges"
git push --force-with-lease --set-upstream origin "${BRANCH}"
echo "has_changes=true" >> "$GITHUB_OUTPUT"
- name: Create or update pull request with GitHub CLI
# Only open/update PRs for validated updates to keep automation branches trustworthy.
if: steps.validate_ranges.outcome == 'success' && steps.commit_updates.outputs.has_changes == 'true'
run: |
BRANCH="automation/python-dependency-range-updates"
PR_TITLE="Python: chore: update dependency ranges"
PR_BODY_FILE="$(mktemp)"
cat > "${PR_BODY_FILE}" <<'EOF'
This PR was generated by the dependency range validation workflow.
- Ran `uv run poe validate-dependency-bounds-project --mode upper --package "*"`
- Updated package dependency bounds
- Refreshed `python/uv.lock` with `uv lock --upgrade`
EOF
PR_NUMBER="$(gh pr list --head "${BRANCH}" --base main --state open --json number --jq '.[0].number')"
if [ -n "${PR_NUMBER}" ]; then
gh pr edit "${PR_NUMBER}" --title "${PR_TITLE}" --body-file "${PR_BODY_FILE}"
else
gh pr create --base main --head "${BRANCH}" --title "${PR_TITLE}" --body-file "${PR_BODY_FILE}"
fi
@@ -0,0 +1,91 @@
name: Python - Dev Dependency Upgrade
on:
workflow_dispatch:
permissions:
contents: write
pull-requests: write
env:
UV_CACHE_DIR: /tmp/.uv-cache
jobs:
upgrade-dev-dependencies:
name: Upgrade Dev Dependencies
runs-on: ubuntu-latest
env:
UV_PYTHON: "3.13"
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
- name: Set up python and install the project
uses: ./.github/actions/python-setup
with:
python-version: ${{ env.UV_PYTHON }}
os: ${{ runner.os }}
env:
UV_CACHE_DIR: /tmp/.uv-cache
- name: Upgrade dev dependencies and validate workspace
run: uv run poe upgrade-dev-dependencies
working-directory: ./python
- name: Commit and push dev dependency updates
id: commit_updates
run: |
BRANCH="automation/python-dev-dependency-updates"
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git checkout -B "${BRANCH}"
git add python/pyproject.toml python/packages/*/pyproject.toml python/uv.lock
if git diff --cached --quiet; then
echo "has_changes=false" >> "$GITHUB_OUTPUT"
echo "No dev dependency updates to commit."
exit 0
fi
git commit -F- <<'EOF'
Python: chore: upgrade dev dependencies
EOF
git push --force-with-lease --set-upstream origin "${BRANCH}"
echo "has_changes=true" >> "$GITHUB_OUTPUT"
- name: Create or update pull request with GitHub CLI
if: steps.commit_updates.outputs.has_changes == 'true'
run: |
BRANCH="automation/python-dev-dependency-updates"
PR_TITLE="Python: chore: upgrade dev dependencies"
PR_BODY_FILE="$(mktemp)"
cat > "${PR_BODY_FILE}" <<'EOF'
### Motivation and Context
This automated update refreshes Python dev dependency pins across the workspace and reruns the repo validation gates before opening a pull request.
### Description
- Ran `uv run poe upgrade-dev-dependencies`
- Refreshed dev dependency pins in workspace `pyproject.toml` files
- Refreshed `python/uv.lock` with `uv lock --upgrade`
- Reinstalled from the frozen lockfile and reran `check`, `typing`, and `test`
### Contribution Checklist
- [x] The code builds clean without any errors or warnings
- [x] The PR follows the [Contribution Guidelines](https://github.com/microsoft/agent-framework/blob/main/CONTRIBUTING.md)
- [x] 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.
EOF
PR_NUMBER="$(gh pr list --head "${BRANCH}" --base main --state open --json number --jq '.[0].number')"
if [ -n "${PR_NUMBER}" ]; then
gh pr edit "${PR_NUMBER}" --title "${PR_TITLE}" --body-file "${PR_BODY_FILE}"
else
gh pr create --base main --head "${BRANCH}" --title "${PR_TITLE}" --body-file "${PR_BODY_FILE}"
fi
+2 -2
View File
@@ -24,9 +24,9 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Set up uv
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
with:
version-file: "python/pyproject.toml"
enable-cache: true
+96 -56
View File
@@ -13,25 +13,13 @@ on:
workflow_call:
inputs:
checkout-ref:
description: "Immutable commit SHA to check out"
description: "Git ref to checkout (e.g., refs/pull/123/head)"
required: true
type: string
secrets:
ANTHROPIC_API_KEY:
required: true
AZURE_CLIENT_ID:
required: true
AZURE_TENANT_ID:
required: true
AZURE_SUBSCRIPTION_ID:
required: true
FOUNDRY_MODELS_API_KEY:
required: false
OPENAI__APIKEY:
required: true
permissions:
contents: read
id-token: write
env:
UV_CACHE_DIR: /tmp/.uv-cache
@@ -48,7 +36,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ inputs.checkout-ref }}
persist-credentials: false
@@ -81,7 +69,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ inputs.checkout-ref }}
persist-credentials: false
@@ -102,7 +90,7 @@ jobs:
--junitxml=pytest.xml
- name: Upload test results
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: test-results-openai
path: ./python/pytest.xml
@@ -111,9 +99,6 @@ jobs:
# Azure OpenAI integration tests
python-tests-azure-openai:
name: Python Integration Tests - Azure OpenAI
permissions:
contents: read
id-token: write
runs-on: ubuntu-latest
environment: integration
timeout-minutes: 60
@@ -127,7 +112,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ inputs.checkout-ref }}
persist-credentials: false
@@ -138,7 +123,7 @@ jobs:
python-version: ${{ env.UV_PYTHON }}
os: ${{ runner.os }}
- name: Azure CLI Login
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
@@ -156,7 +141,7 @@ jobs:
--junitxml=pytest.xml
- name: Upload test results
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: test-results-azure-openai
path: ./python/pytest.xml
@@ -178,7 +163,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ inputs.checkout-ref }}
persist-credentials: false
@@ -192,7 +177,7 @@ jobs:
run: curl -fsSL https://ollama.com/install.sh | sh
working-directory: .
- name: Cache Ollama models
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: ~/.ollama/models
key: ollama-models-qwen2.5-1.5b-nomic-embed-text-v1
@@ -232,15 +217,13 @@ jobs:
fallback_url: ${{ env.LOCAL_MCP_URL }}
- name: Prefer local MCP URL when available
run: echo "LOCAL_MCP_URL=${{ steps.local-mcp.outputs.effective_url }}" >> "$GITHUB_ENV"
- name: Test with pytest (Anthropic, Hyperlight, Mistral, Ollama, MCP integration)
- name: Test with pytest (Anthropic, Hyperlight, Ollama, MCP integration)
run: >
uv run pytest --import-mode=importlib
packages/anthropic/tests
packages/hyperlight/tests
packages/mistral/tests
packages/ollama/tests
packages/core/tests/core/test_mcp.py
packages/hosting-mcp/tests
-m integration
-n logical --dist worksteal
--timeout=120 --session-timeout=900 --timeout_method thread
@@ -248,7 +231,7 @@ jobs:
--junitxml=pytest.xml
- name: Upload test results
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: test-results-misc
path: ./python/pytest.xml
@@ -274,12 +257,73 @@ jobs:
done
kill -KILL -- "-$server_pid" 2>/dev/null || kill -KILL "$server_pid" 2>/dev/null || true
# Azure Functions + Durable Task integration tests
python-tests-functions:
name: Python Integration Tests - Functions
runs-on: ubuntu-latest
environment: integration
timeout-minutes: 60
env:
UV_PYTHON: "3.11"
OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_CHAT_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_EMBEDDING_MODEL: ${{ vars.OPENAI_EMBEDDING_MODEL_ID }}
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
AZURE_OPENAI_MODEL: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
AZURE_OPENAI_CHAT_MODEL: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
AZURE_OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ inputs.checkout-ref }}
persist-credentials: false
- name: Set up python and install the project
id: python-setup
uses: ./.github/actions/python-setup
with:
python-version: ${{ env.UV_PYTHON }}
os: ${{ runner.os }}
- name: Azure CLI Login
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # 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 (Functions + Durable Task integration)
run: >
uv run pytest --import-mode=importlib
packages/azurefunctions/tests/integration_tests
packages/durabletask/tests/integration_tests
-m integration
-n logical --dist worksteal
-x
--timeout=480 --session-timeout=900 --timeout_method thread
--retries 2 --retry-delay 5
--junitxml=pytest.xml
- name: Upload test results
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: test-results-functions
path: ./python/pytest.xml
if-no-files-found: ignore
# Foundry integration tests
python-tests-foundry:
name: Python Integration Tests - Foundry
permissions:
contents: read
id-token: write
runs-on: ubuntu-latest
environment: integration
timeout-minutes: 60
@@ -297,7 +341,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ inputs.checkout-ref }}
persist-credentials: false
@@ -308,7 +352,7 @@ jobs:
python-version: ${{ env.UV_PYTHON }}
os: ${{ runner.os }}
- name: Azure CLI Login
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
@@ -325,7 +369,7 @@ jobs:
--junitxml=pytest.xml
- name: Upload test results
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: test-results-foundry
path: ./python/pytest.xml
@@ -334,9 +378,6 @@ jobs:
# Foundry Hosting integration tests
python-tests-foundry-hosting:
name: Python Integration Tests - Foundry Hosting
permissions:
contents: read
id-token: write
runs-on: ubuntu-latest
environment: integration
timeout-minutes: 60
@@ -347,7 +388,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ inputs.checkout-ref }}
persist-credentials: false
@@ -358,7 +399,7 @@ jobs:
python-version: ${{ env.UV_PYTHON }}
os: ${{ runner.os }}
- name: Azure CLI Login
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
@@ -375,7 +416,7 @@ jobs:
--junitxml=pytest.xml
- name: Upload test results
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: test-results-foundry-hosting
path: ./python/pytest.xml
@@ -402,7 +443,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ inputs.checkout-ref }}
persist-credentials: false
@@ -427,7 +468,7 @@ jobs:
run: uv run --directory packages/azure-cosmos poe integration-tests -n logical --dist worksteal --timeout=120 --session-timeout=900 --timeout_method thread --retries 2 --retry-delay 5 --junitxml=${{ github.workspace }}/python/pytest.xml
- name: Upload test results
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: test-results-cosmos
path: ./python/pytest.xml
@@ -438,18 +479,15 @@ jobs:
name: Python Integration Tests - GitHub Copilot
runs-on: ubuntu-latest
environment: integration
permissions:
copilot-requests: write
contents: read
timeout-minutes: 60
env:
GITHUB_TOKEN: ${{ github.token }}
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
GITHUB_COPILOT_TIMEOUT: "120"
defaults:
run:
working-directory: python
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ inputs.checkout-ref }}
persist-credentials: false
@@ -469,7 +507,7 @@ jobs:
--junitxml=pytest.xml
- name: Upload test results
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: test-results-github-copilot
path: ./python/pytest.xml
@@ -487,6 +525,7 @@ jobs:
python-tests-openai,
python-tests-azure-openai,
python-tests-misc-integration,
python-tests-functions,
python-tests-foundry,
python-tests-foundry-hosting,
python-tests-cosmos,
@@ -497,7 +536,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ inputs.checkout-ref }}
persist-credentials: false
@@ -507,12 +546,12 @@ jobs:
python-version: ${{ env.UV_PYTHON }}
os: ${{ runner.os }}
- name: Download all test results from current run
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
pattern: test-results-*
path: test-results/
- name: Restore report history cache
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: python/integration-report-history.json
key: integration-report-history-integration-${{ github.run_id }}
@@ -529,13 +568,13 @@ jobs:
run: cat integration-test-report.md >> $GITHUB_STEP_SUMMARY
- name: Save report history cache
if: always()
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: python/integration-report-history.json
key: integration-report-history-integration-${{ github.run_id }}
- name: Upload unified trend report
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: integration-test-report
path: |
@@ -551,6 +590,7 @@ jobs:
python-tests-openai,
python-tests-azure-openai,
python-tests-misc-integration,
python-tests-functions,
python-tests-foundry,
python-tests-foundry-hosting,
python-tests-cosmos,
@@ -559,12 +599,12 @@ jobs:
steps:
- name: Fail workflow if tests failed
if: contains(join(needs.*.result, ','), 'failure')
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: core.setFailed('Integration Tests Failed!')
- name: Fail workflow if tests cancelled
if: contains(join(needs.*.result, ','), 'cancelled')
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: core.setFailed('Integration Tests Cancelled!')
+7 -8
View File
@@ -24,17 +24,13 @@ jobs:
outputs:
pythonChanges: ${{ steps.filter.outputs.python}}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: dorny/paths-filter@d1c1ffe0248fe513906c8e24db8ea791d46f8590 # v3
id: filter
with:
filters: |
python:
- 'python/**'
- '!python/AGENTS.md'
- '!python/**/AGENTS.md'
- '!python/.github/skills/*'
- '!python/.github/skills/**'
# run only if 'python' files were changed
- name: python tests
if: steps.filter.outputs.python == 'true'
@@ -63,7 +59,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Set up python and install the project
id: python-setup
@@ -71,7 +67,7 @@ jobs:
with:
python-version: ${{ matrix.python-version }}
os: ${{ runner.os }}
exclude-packages: ${{ matrix.python-version == '3.10' && 'agent-framework-github-copilot agent-framework-azure-cosmos-memory' || '' }}
exclude-packages: ${{ matrix.python-version == '3.10' && 'agent-framework-github-copilot' || '' }}
env:
# Configure a constant location for the uv cache
UV_CACHE_DIR: /tmp/.uv-cache
@@ -92,6 +88,9 @@ jobs:
- 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()
+114 -37
View File
@@ -36,13 +36,14 @@ jobs:
openaiChanged: ${{ steps.filter.outputs.openai }}
azureChanged: ${{ steps.filter.outputs.azure }}
miscChanged: ${{ steps.filter.outputs.misc }}
functionsChanged: ${{ steps.filter.outputs.functions }}
foundryChanged: ${{ steps.filter.outputs.foundry }}
foundryHostingChanged: ${{ steps.filter.outputs.foundry_hosting }}
cosmosChanged: ${{ steps.filter.outputs.cosmos }}
githubCopilotChanged: ${{ steps.filter.outputs.github_copilot }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: dorny/paths-filter@d1c1ffe0248fe513906c8e24db8ea791d46f8590 # v3
id: filter
with:
filters: |
@@ -67,15 +68,16 @@ jobs:
misc:
- 'python/packages/anthropic/**'
- 'python/packages/hyperlight/**'
- 'python/packages/mistral/**'
- 'python/packages/ollama/**'
- 'python/packages/core/agent_framework/_mcp.py'
- 'python/packages/core/tests/core/test_mcp.py'
- 'python/packages/hosting-mcp/**'
- 'python/scripts/local_mcp_streamable_http_server.py'
- '.github/actions/setup-local-mcp-server/**'
- '.github/workflows/python-merge-tests.yml'
- '.github/workflows/python-integration-tests.yml'
functions:
- 'python/packages/azurefunctions/**'
- 'python/packages/durabletask/**'
foundry:
- 'python/packages/foundry/**'
- 'python/samples/**/providers/foundry/**'
@@ -107,7 +109,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Set up python and install the project
id: python-setup
uses: ./.github/actions/python-setup
@@ -154,7 +156,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Set up python and install the project
id: python-setup
uses: ./.github/actions/python-setup
@@ -187,7 +189,7 @@ jobs:
title: OpenAI integration test results
- name: Upload test results
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: test-results-openai
path: ./python/pytest.xml
@@ -215,7 +217,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Set up python and install the project
id: python-setup
uses: ./.github/actions/python-setup
@@ -224,7 +226,7 @@ jobs:
os: ${{ runner.os }}
- name: Azure CLI Login
if: github.event_name != 'pull_request'
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
@@ -257,7 +259,7 @@ jobs:
title: Azure OpenAI integration test results
- name: Upload test results
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: test-results-azure-openai
path: ./python/pytest.xml
@@ -285,7 +287,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Set up python and install the project
id: python-setup
uses: ./.github/actions/python-setup
@@ -296,7 +298,7 @@ jobs:
run: curl -fsSL https://ollama.com/install.sh | sh
working-directory: .
- name: Cache Ollama models
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: ~/.ollama/models
key: ollama-models-qwen2.5-1.5b-nomic-embed-text-v1
@@ -336,15 +338,13 @@ jobs:
fallback_url: ${{ env.LOCAL_MCP_URL }}
- name: Prefer local MCP URL when available
run: echo "LOCAL_MCP_URL=${{ steps.local-mcp.outputs.effective_url }}" >> "$GITHUB_ENV"
- name: Test with pytest (Anthropic, Hyperlight, Mistral, Ollama, MCP integration)
- name: Test with pytest (Anthropic, Hyperlight, Ollama, MCP integration)
run: >
uv run pytest --import-mode=importlib
packages/anthropic/tests
packages/hyperlight/tests
packages/mistral/tests
packages/ollama/tests
packages/core/tests/core/test_mcp.py
packages/hosting-mcp/tests
-m integration
-n logical --dist worksteal
--timeout=120 --session-timeout=900 --timeout_method thread
@@ -382,12 +382,90 @@ jobs:
title: Misc integration test results
- name: Upload test results
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: test-results-misc
path: ./python/pytest.xml
if-no-files-found: ignore
# Azure Functions + Durable Task integration tests
python-tests-functions:
name: Python Tests - Functions Integration
needs: paths-filter
if: >
github.event_name != 'pull_request' &&
needs.paths-filter.outputs.pythonChanges == 'true' &&
(github.event_name != 'merge_group' ||
needs.paths-filter.outputs.functionsChanged == 'true' ||
needs.paths-filter.outputs.coreChanged == 'true')
runs-on: ubuntu-latest
environment: integration
env:
UV_PYTHON: "3.11"
OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_CHAT_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_EMBEDDING_MODEL: ${{ vars.OPENAI_EMBEDDING_MODEL_ID }}
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
AZURE_OPENAI_MODEL: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
AZURE_OPENAI_CHAT_MODEL: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
AZURE_OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Set up python and install the project
id: python-setup
uses: ./.github/actions/python-setup
with:
python-version: ${{ env.UV_PYTHON }}
os: ${{ runner.os }}
- name: Azure CLI Login
if: github.event_name != 'pull_request'
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # 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 (Functions + Durable Task integration)
run: >
uv run pytest --import-mode=importlib
packages/azurefunctions/tests/integration_tests
packages/durabletask/tests/integration_tests
-m integration
-n logical --dist worksteal
-x
--timeout=480 --session-timeout=900 --timeout_method thread
--retries 2 --retry-delay 5
--junitxml=pytest.xml
working-directory: ./python
- name: Surface failing tests
if: always()
uses: pmeier/pytest-results-action@20b595761ba9bf89e115e875f8bc863f913bc8ad # v0.7.2
with:
path: ./python/pytest.xml
summary: true
display-options: fEX
fail-on-empty: false
title: Functions integration test results
- name: Upload test results
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: test-results-functions
path: ./python/pytest.xml
if-no-files-found: ignore
python-tests-foundry:
name: Python Integration Tests - Foundry
needs: paths-filter
@@ -413,7 +491,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Set up python and install the project
id: python-setup
uses: ./.github/actions/python-setup
@@ -422,7 +500,7 @@ jobs:
os: ${{ runner.os }}
- name: Azure CLI Login
if: github.event_name != 'pull_request'
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
@@ -449,7 +527,7 @@ jobs:
title: Test results
- name: Upload test results
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: test-results-foundry
path: ./python/pytest.xml
@@ -474,7 +552,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Set up python and install the project
id: python-setup
uses: ./.github/actions/python-setup
@@ -483,7 +561,7 @@ jobs:
os: ${{ runner.os }}
- name: Azure CLI Login
if: github.event_name != 'pull_request'
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
@@ -510,7 +588,7 @@ jobs:
title: Foundry Hosting integration test results
- name: Upload test results
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: test-results-foundry-hosting
path: ./python/pytest.xml
@@ -545,7 +623,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Set up python and install the project
id: python-setup
uses: ./.github/actions/python-setup
@@ -577,7 +655,7 @@ jobs:
title: Cosmos integration test results
- name: Upload test results
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: test-results-cosmos
path: ./python/pytest.xml
@@ -595,18 +673,15 @@ jobs:
needs.paths-filter.outputs.coreChanged == 'true')
runs-on: ubuntu-latest
environment: integration
permissions:
copilot-requests: write
contents: read
timeout-minutes: 60
env:
GITHUB_TOKEN: ${{ github.token }}
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
GITHUB_COPILOT_TIMEOUT: "120"
defaults:
run:
working-directory: python
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Set up python and install the project
id: python-setup
uses: ./.github/actions/python-setup
@@ -632,7 +707,7 @@ jobs:
title: GitHub Copilot integration test results
- name: Upload test results
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: test-results-github-copilot
path: ./python/pytest.xml
@@ -650,6 +725,7 @@ jobs:
python-tests-openai,
python-tests-azure-openai,
python-tests-misc-integration,
python-tests-functions,
python-tests-foundry,
python-tests-foundry-hosting,
python-tests-cosmos,
@@ -660,19 +736,19 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Set up python and install the project
uses: ./.github/actions/python-setup
with:
python-version: ${{ env.UV_PYTHON }}
os: ${{ runner.os }}
- name: Download all test results from current run
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
pattern: test-results-*
path: test-results/
- name: Restore report history cache
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: python/integration-report-history.json
key: integration-report-history-merge-${{ github.run_id }}
@@ -689,13 +765,13 @@ jobs:
run: cat integration-test-report.md >> $GITHUB_STEP_SUMMARY
- name: Save report history cache
if: always()
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: python/integration-report-history.json
key: integration-report-history-merge-${{ github.run_id }}
- name: Upload unified trend report
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: integration-test-report
path: |
@@ -711,6 +787,7 @@ jobs:
python-tests-openai,
python-tests-azure-openai,
python-tests-misc-integration,
python-tests-functions,
python-tests-foundry,
python-tests-foundry-hosting,
python-tests-cosmos,
@@ -720,13 +797,13 @@ jobs:
- name: Fail workflow if tests failed
id: check_tests_failed
if: contains(join(needs.*.result, ','), 'failure')
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # 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@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: core.setFailed('Integration Tests Cancelled!')
+15 -84
View File
@@ -23,7 +23,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Set up python and install the project
id: python-setup
uses: ./.github/actions/python-setup
@@ -33,99 +33,30 @@ jobs:
env:
# Configure a constant location for the uv cache
UV_CACHE_DIR: /tmp/.uv-cache
- name: Resolve the package to build
env:
TAG_NAME: ${{ github.event.release.tag_name }}
- name: Set environment variables
run: |
set -euo pipefail
# Extract package name from tag (format: python-<package>-<version>)
TAG="${{ github.event.release.tag_name }}"
PACKAGE=$(echo "$TAG" | sed 's/^python-\([^-]*\)-.*$/\1/')
TAG="$TAG_NAME"
# Release tags are either python-<version> for the whole workspace, or
# python-<package>-<version> for a single package. Package names may
# themselves contain hyphens (hosting-a2a, azure-ai-search), so the
# package part cannot be found by splitting on the first hyphen.
#
# Versions follow the lifecycle patterns in the python-package-management
# skill: X.Y.Z, X.Y.ZaYYMMDD, X.Y.ZbYYMMDD, X.Y.ZrcN, each optionally
# carrying a .N or .postN re-cut suffix.
VERSION_PATTERN='^[0-9]+\.[0-9]+\.[0-9]+([ab][0-9]+|rc[0-9]+)?(\.[0-9]+|\.post[0-9]+)?$'
REST="${TAG#python-}"
if [[ -z "$REST" ]]; then
echo "Error: tag '$TAG' has no version or package component"
# 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
if [[ "$REST" =~ $VERSION_PATTERN ]]; then
# python-<version>: build every workspace package plus the root meta package.
PACKAGE="all"
echo "Resolved tag '$TAG' to the full workspace build"
else
# python-<package>-<version>: split off the trailing version component and
# require it to be a real version, so a malformed tag fails here rather
# than being mistaken for another kind of release.
CANDIDATE="${REST%-*}"
VERSION="${REST##*-}"
if [[ "$CANDIDATE" == "$REST" || -z "$CANDIDATE" ]]; then
echo "Error: tag '$TAG' is neither python-<version> nor python-<package>-<version>"
exit 1
fi
if [[ ! "$VERSION" =~ $VERSION_PATTERN ]]; then
echo "Error: tag '$TAG' does not end in a supported version"
echo "Derived version: '$VERSION'"
exit 1
fi
# Resolve the package part against the real package directories. Tags use
# hyphens even where the directory uses underscores
# (python-github-copilot -> github_copilot).
PACKAGE=""
for dir in packages/*/; do
name="${dir#packages/}"
name="${name%/}"
if [[ "$name" == "$CANDIDATE" || "${name//_/-}" == "$CANDIDATE" ]]; then
PACKAGE="$name"
break
fi
done
if [[ -z "$PACKAGE" ]]; then
echo "Error: tag '$TAG' does not map to a directory in packages/"
echo "Derived package name: '$CANDIDATE'"
echo "Available packages: $(ls packages/)"
exit 1
fi
echo "Resolved tag '$TAG' to package '$PACKAGE'"
fi
echo "PACKAGE=$PACKAGE" >> "$GITHUB_ENV"
echo "PACKAGE=$PACKAGE" >> $GITHUB_ENV
echo "Building package: $PACKAGE"
- name: Check version
env:
TAG_NAME: ${{ github.event.release.tag_name }}
run: |
echo "Building and uploading Python release: $TAG_NAME"
if [[ "$PACKAGE" == "all" ]]; then
echo "Build scope: all workspace packages and the root meta package"
else
echo "Build scope: packages/$PACKAGE"
fi
echo "Building and uploading Python package version: ${{ github.event.release.tag_name }}"
echo "Package directory: packages/${{ env.PACKAGE }}"
- name: Build the package
run: |
set -euo pipefail
if [[ "$PACKAGE" == "all" ]]; then
uv run poe build
else
uv run poe --directory "packages/$PACKAGE" build
fi
run: uv run poe --directory packages/${{ env.PACKAGE }} build
- name: Release
uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2
with:
files: |
python/dist/*
+82 -296
View File
@@ -8,11 +8,11 @@ on:
env:
# Configure a constant location for the uv cache
UV_CACHE_DIR: /tmp/.uv-cache
GITHUB_TOKEN: ${{ github.token }}
GITHUB_COPILOT_MODEL: auto
# GitHub Copilot configuration
GITHUB_COPILOT_MODEL: claude-opus-4.6
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
permissions:
copilot-requests: write
contents: read
id-token: write
@@ -23,13 +23,13 @@ jobs:
environment: integration
env:
# Required configuration for get-started samples
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT || vars.AZURE_AI_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
defaults:
run:
working-directory: python
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup environment
uses: ./.github/actions/sample-validation-setup
@@ -48,12 +48,8 @@ jobs:
run: |
cd scripts && uv run python -m sample_validation --subdir 01-get-started --save-report --report-name 01-get-started
- name: Save sample playbooks
if: ${{ !cancelled() }}
uses: ./.github/actions/sample-validation-save-playbooks
- name: Upload validation report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
if: always()
with:
name: validation-report-01-get-started
@@ -61,13 +57,12 @@ jobs:
validate-02-agents:
name: Validate 02-agents
if: false # Temporarily disabled - to free up Copilot quota for other jobs
runs-on: ubuntu-latest
environment: integration
env:
# Foundry configuration
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT || vars.AZURE_AI_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
# Azure OpenAI configuration
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
AZURE_OPENAI_MODEL: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
@@ -75,19 +70,19 @@ jobs:
AZURE_OPENAI_CHAT_MODEL: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
AZURE_OPENAI_EMBEDDING_MODEL: ${{ vars.AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME || vars.AZUREOPENAI__EMBEDDINGDEPLOYMENTNAME }}
# OpenAI configuration
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.OPENAI_CHAT_MODEL_NAME }}
OPENAI_CHAT_MODEL: ${{ vars.OPENAI_REASONING_MODEL_NAME }}
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_CHAT_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
# GitHub MCP
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
OPENAI_MODEL: ${{ vars.OPENAI_REASONING_MODEL_NAME }}
OPENAI_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
# Observability
ENABLE_INSTRUMENTATION: "true"
defaults:
run:
working-directory: python
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup environment
uses: ./.github/actions/sample-validation-setup
@@ -113,123 +108,29 @@ jobs:
- name: Run sample validation
run: |
cd scripts && uv run python -m sample_validation --subdir 02-agents --exclude providers harness tools --save-report --report-name 02-agents
- name: Save sample playbooks
if: ${{ !cancelled() }}
uses: ./.github/actions/sample-validation-save-playbooks
cd scripts && uv run python -m sample_validation --subdir 02-agents --exclude providers --save-report --report-name 02-agents
- name: Upload validation report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
if: always()
with:
name: validation-report-02-agents
path: python/samples/sample_validation/reports/
validate-02-agents-harness:
name: Validate 02-agents/harness
if: false # Temporarily disabled - to free up Copilot quota for other jobs
runs-on: ubuntu-latest
environment: integration
env:
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
# Optional: enables the Foundry memory path in harness samples
FOUNDRY_EMBEDDING_MODEL: ${{ vars.FOUNDRY_EMBEDDING_MODEL || '' }}
FOUNDRY_MEMORY_STORE: ${{ vars.FOUNDRY_MEMORY_STORE || '' }}
defaults:
run:
working-directory: python
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup environment
uses: ./.github/actions/sample-validation-setup
with:
azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}
azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
azure-subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
os: ${{ runner.os }}
- name: Create .env for samples
run: |
echo "FOUNDRY_PROJECT_ENDPOINT=$FOUNDRY_PROJECT_ENDPOINT" >> .env
echo "FOUNDRY_MODEL=$FOUNDRY_MODEL" >> .env
echo "FOUNDRY_EMBEDDING_MODEL=$FOUNDRY_EMBEDDING_MODEL" >> .env
echo "FOUNDRY_MEMORY_STORE=$FOUNDRY_MEMORY_STORE" >> .env
- name: Run sample validation
run: |
cd scripts && uv run python -m sample_validation --subdir 02-agents/harness --save-report --report-name 02-agents-harness
- name: Save sample playbooks
if: ${{ !cancelled() }}
uses: ./.github/actions/sample-validation-save-playbooks
- name: Upload validation report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: validation-report-02-agents-harness
path: python/samples/sample_validation/reports/
validate-02-agents-tools:
name: Validate 02-agents/tools
if: false # Temporarily disabled - to free up Copilot quota for other jobs
runs-on: ubuntu-latest
environment: integration
env:
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
defaults:
run:
working-directory: python
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup environment
uses: ./.github/actions/sample-validation-setup
with:
azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}
azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
azure-subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
os: ${{ runner.os }}
- name: Create .env for samples
run: |
echo "FOUNDRY_PROJECT_ENDPOINT=$FOUNDRY_PROJECT_ENDPOINT" >> .env
echo "FOUNDRY_MODEL=$FOUNDRY_MODEL" >> .env
- name: Run sample validation
run: |
cd scripts && uv run python -m sample_validation --subdir 02-agents/tools --save-report --report-name 02-agents-tools
- name: Save sample playbooks
if: ${{ !cancelled() }}
uses: ./.github/actions/sample-validation-save-playbooks
- name: Upload validation report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: validation-report-02-agents-tools
path: python/samples/sample_validation/reports/
validate-02-agents-openai:
name: Validate 02-agents/providers/openai
if: false # Temporarily disabled - to free up Copilot quota for other jobs
runs-on: ubuntu-latest
environment: integration
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_MODEL: ${{ vars.OPENAI_CHAT_MODEL_NAME }}
OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.OPENAI_CHAT_MODEL_NAME }}
OPENAI_CHAT_MODEL: ${{ vars.OPENAI_REASONING_MODEL_NAME }}
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
OPENAI_MODEL: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_CHAT_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
defaults:
run:
working-directory: python
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup environment
uses: ./.github/actions/sample-validation-setup
@@ -250,12 +151,8 @@ jobs:
run: |
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/openai --save-report --report-name 02-agents-openai
- name: Save sample playbooks
if: ${{ !cancelled() }}
uses: ./.github/actions/sample-validation-save-playbooks
- name: Upload validation report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
if: always()
with:
name: validation-report-02-agents-openai
@@ -263,7 +160,6 @@ jobs:
validate-02-agents-azure:
name: Validate 02-agents/providers/azure
if: false # Temporarily disabled - to free up Copilot quota for other jobs
runs-on: ubuntu-latest
environment: integration
env:
@@ -274,7 +170,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup environment
uses: ./.github/actions/sample-validation-setup
@@ -294,12 +190,8 @@ jobs:
run: |
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/azure --save-report --report-name 02-agents-azure
- name: Save sample playbooks
if: ${{ !cancelled() }}
uses: ./.github/actions/sample-validation-save-playbooks
- name: Upload validation report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
if: always()
with:
name: validation-report-02-agents-azure
@@ -307,7 +199,6 @@ jobs:
validate-02-agents-anthropic:
name: Validate 02-agents/providers/anthropic
if: false # Temporarily disabled - to free up Copilot quota for other jobs
runs-on: ubuntu-latest
environment: integration
env:
@@ -317,7 +208,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup environment
uses: ./.github/actions/sample-validation-setup
@@ -336,12 +227,8 @@ jobs:
run: |
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/anthropic --save-report --report-name 02-agents-anthropic
- name: Save sample playbooks
if: ${{ !cancelled() }}
uses: ./.github/actions/sample-validation-save-playbooks
- name: Upload validation report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
if: always()
with:
name: validation-report-02-agents-anthropic
@@ -349,14 +236,13 @@ jobs:
validate-02-agents-github-copilot:
name: Validate 02-agents/providers/github_copilot
if: false # Temporarily disabled - to free up Copilot quota for other jobs
runs-on: ubuntu-latest
environment: integration
defaults:
run:
working-directory: python
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup environment
uses: ./.github/actions/sample-validation-setup
@@ -370,12 +256,8 @@ jobs:
run: |
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/github_copilot --save-report --report-name 02-agents-github-copilot
- name: Save sample playbooks
if: ${{ !cancelled() }}
uses: ./.github/actions/sample-validation-save-playbooks
- name: Upload validation report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
if: always()
with:
name: validation-report-02-agents-github-copilot
@@ -392,7 +274,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup environment
uses: ./.github/actions/sample-validation-setup
@@ -406,12 +288,8 @@ jobs:
run: |
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/amazon --save-report --report-name 02-agents-amazon
- name: Save sample playbooks
if: ${{ !cancelled() }}
uses: ./.github/actions/sample-validation-save-playbooks
- name: Upload validation report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
if: always()
with:
name: validation-report-02-agents-amazon
@@ -428,7 +306,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup environment
uses: ./.github/actions/sample-validation-setup
@@ -442,12 +320,8 @@ jobs:
run: |
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/ollama --save-report --report-name 02-agents-ollama
- name: Save sample playbooks
if: ${{ !cancelled() }}
uses: ./.github/actions/sample-validation-save-playbooks
- name: Upload validation report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
if: always()
with:
name: validation-report-02-agents-ollama
@@ -455,19 +329,19 @@ jobs:
validate-02-agents-foundry:
name: Validate 02-agents/providers/foundry
if: false # Temporarily disabled - to free up Copilot quota for other jobs
if: false # Temporarily disabled - provider folder also contains the local Foundry sample
runs-on: ubuntu-latest
environment: integration
env:
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT || vars.AZURE_AI_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
FOUNDRY_AGENT_NAME: ${{ vars.FOUNDRY_AGENT_NAME || '' }}
FOUNDRY_AGENT_VERSION: ${{ vars.FOUNDRY_AGENT_VERSION || '' }}
defaults:
run:
working-directory: python
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup environment
uses: ./.github/actions/sample-validation-setup
@@ -488,12 +362,8 @@ jobs:
run: |
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/foundry --save-report --report-name 02-agents-foundry
- name: Save sample playbooks
if: ${{ !cancelled() }}
uses: ./.github/actions/sample-validation-save-playbooks
- name: Upload validation report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
if: always()
with:
name: validation-report-02-agents-foundry
@@ -513,7 +383,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup environment
uses: ./.github/actions/sample-validation-setup
@@ -534,12 +404,8 @@ jobs:
run: |
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/copilotstudio --save-report --report-name 02-agents-copilotstudio
- name: Save sample playbooks
if: ${{ !cancelled() }}
uses: ./.github/actions/sample-validation-save-playbooks
- name: Upload validation report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
if: always()
with:
name: validation-report-02-agents-copilotstudio
@@ -553,7 +419,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup environment
uses: ./.github/actions/sample-validation-setup
@@ -567,12 +433,8 @@ jobs:
run: |
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/custom --save-report --report-name 02-agents-custom
- name: Save sample playbooks
if: ${{ !cancelled() }}
uses: ./.github/actions/sample-validation-save-playbooks
- name: Upload validation report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
if: always()
with:
name: validation-report-02-agents-custom
@@ -580,17 +442,16 @@ jobs:
validate-03-workflows:
name: Validate 03-workflows
if: false # Temporarily disabled - to free up Copilot quota for other jobs
runs-on: ubuntu-latest
environment: integration
env:
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT || vars.AZURE_AI_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
defaults:
run:
working-directory: python
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup environment
uses: ./.github/actions/sample-validation-setup
@@ -609,79 +470,28 @@ jobs:
run: |
cd scripts && uv run python -m sample_validation --subdir 03-workflows --save-report --report-name 03-workflows
- name: Save sample playbooks
if: ${{ !cancelled() }}
uses: ./.github/actions/sample-validation-save-playbooks
- name: Upload validation report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
if: always()
with:
name: validation-report-03-workflows
path: python/samples/sample_validation/reports/
validate-04-hosting-foundry-hosted-agents:
name: Validate 04-hosting (foundry-hosted-agents)
validate-04-hosting:
name: Validate 04-hosting
if: false # Temporarily disabled because of sample complexity
runs-on: ubuntu-latest
environment: integration
env:
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
# Foundry hosted agent configuration
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.FOUNDRY_MODEL }}
FOUNDRY_PROJECT_ID: ${{ vars.FOUNDRY_PROJECT_ID }}
AZURE_CONTAINER_REGISTRY_ENDPOINT: ${{ vars.AZURE_CONTAINER_REGISTRY_ENDPOINT }}
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
TOOLBOX_ENDPOINT: ${{ vars.TOOLBOX_ENDPOINT }}
FOUNDRY_AGENT_NAME: ${{ vars.FOUNDRY_HOSTED_AGENT_NAME }}
MEMORY_STORE_NAME: ${{ vars.FOUNDRY_HOSTED_AGENT_MEMORY_STORE }}
AZURE_SEARCH_ENDPOINT: ${{ vars.AZURE_SEARCH_ENDPOINT }}
AZURE_SEARCH_INDEX_NAME: ${{ vars.FOUNDRY_HOSTED_AGENT_SEARCH_INDEX_NAME }}
defaults:
run:
working-directory: python
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup environment
uses: ./.github/actions/sample-validation-setup
with:
azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}
azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
azure-subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
os: ${{ runner.os }}
- name: Run sample validation
# Maximum parallel workers is set to 1 because all samples use the same port
run: |
cd scripts && uv run python -m sample_validation --subdir 04-hosting/foundry-hosted-agents --save-report --report-name 04-hosting-foundry-hosted-agents --max-parallel-workers 1
- name: Save sample playbooks
if: ${{ !cancelled() }}
uses: ./.github/actions/sample-validation-save-playbooks
- name: Upload validation report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: validation-report-04-hosting-foundry-hosted-agents
path: python/samples/sample_validation/reports/
validate-04-hosting-other:
name: Validate 04-hosting (other)
if: false # Temporarily disabled - to free up Copilot quota for other jobs
runs-on: ubuntu-latest
environment: integration
env:
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT || vars.AZURE_AI_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
# A2A configuration
A2A_AGENT_HOST: http://localhost:5001/
defaults:
run:
working-directory: python
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup environment
uses: ./.github/actions/sample-validation-setup
@@ -693,17 +503,13 @@ jobs:
- name: Run sample validation
run: |
cd scripts && uv run python -m sample_validation --subdir 04-hosting --exclude foundry-hosted-agents --save-report --report-name 04-hosting-other
- name: Save sample playbooks
if: ${{ !cancelled() }}
uses: ./.github/actions/sample-validation-save-playbooks
cd scripts && uv run python -m sample_validation --subdir 04-hosting --save-report --report-name 04-hosting
- name: Upload validation report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
if: always()
with:
name: validation-report-04-hosting-other
name: validation-report-04-hosting
path: python/samples/sample_validation/reports/
validate-05-end-to-end:
@@ -712,8 +518,8 @@ jobs:
runs-on: ubuntu-latest
environment: integration
env:
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT || vars.AZURE_AI_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
# Azure OpenAI configuration
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
AZURE_OPENAI_MODEL: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
@@ -728,7 +534,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup environment
uses: ./.github/actions/sample-validation-setup
@@ -742,12 +548,8 @@ jobs:
run: |
cd scripts && uv run python -m sample_validation --subdir 05-end-to-end --save-report --report-name 05-end-to-end
- name: Save sample playbooks
if: ${{ !cancelled() }}
uses: ./.github/actions/sample-validation-save-playbooks
- name: Upload validation report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
if: always()
with:
name: validation-report-05-end-to-end
@@ -758,21 +560,21 @@ jobs:
runs-on: ubuntu-latest
environment: integration
env:
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT || vars.AZURE_AI_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
# Azure OpenAI configuration
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
AZURE_OPENAI_MODEL: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
# OpenAI configuration
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.OPENAI_CHAT_MODEL_NAME }}
OPENAI_CHAT_MODEL: ${{ vars.OPENAI_REASONING_MODEL_NAME }}
OPENAI_MODEL: ${{ vars.OPENAI_REASONING_MODEL_NAME }}
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_CHAT_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
defaults:
run:
working-directory: python
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup environment
uses: ./.github/actions/sample-validation-setup
@@ -792,19 +594,12 @@ jobs:
echo "OPENAI_CHAT_COMPLETION_MODEL=$OPENAI_CHAT_COMPLETION_MODEL" >> .env
echo "OPENAI_CHAT_MODEL=$OPENAI_CHAT_MODEL" >> .env
- name: Pre-install AutoGen dependencies for migration samples
run: uv pip install "autogen-agentchat" "autogen-ext[openai]"
- name: Run sample validation
run: |
cd scripts && uv run python -m sample_validation --subdir autogen-migration --save-report --report-name autogen-migration --agent-timeout 600
- name: Save sample playbooks
if: ${{ !cancelled() }}
uses: ./.github/actions/sample-validation-save-playbooks
cd scripts && uv run python -m sample_validation --subdir autogen-migration --save-report --report-name autogen-migration
- name: Upload validation report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
if: always()
with:
name: validation-report-autogen-migration
@@ -812,25 +607,23 @@ jobs:
validate-semantic-kernel-migration:
name: Validate semantic-kernel-migration
if: false # Temporarily disabled - to free up Copilot quota for other jobs
runs-on: ubuntu-latest
environment: integration
env:
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT || vars.AZURE_AI_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
# Azure OpenAI configuration for AF
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
AZURE_OPENAI_MODEL: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
# Azure OpenAI configuration for SK
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME }}
# OpenAI key
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.OPENAI_CHAT_MODEL_NAME }}
OPENAI_CHAT_MODEL: ${{ vars.OPENAI_REASONING_MODEL_NAME }}
OPENAI_MODEL: ${{ vars.OPENAI_REASONING_MODEL_NAME }}
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_CHAT_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
# OpenAI configuration for SK
OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI_CHAT_MODEL_NAME }}
OPENAI_RESPONSES_MODEL_ID: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }}
# Copilot Studio
COPILOTSTUDIOAGENT__ENVIRONMENTID: ${{ secrets.COPILOTSTUDIOAGENT__ENVIRONMENTID }}
COPILOTSTUDIOAGENT__SCHEMANAME: ${{ secrets.COPILOTSTUDIOAGENT__SCHEMANAME }}
@@ -840,7 +633,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup environment
uses: ./.github/actions/sample-validation-setup
@@ -868,12 +661,8 @@ jobs:
run: |
cd scripts && uv run python -m sample_validation --subdir semantic-kernel-migration --save-report --report-name semantic-kernel-migration
- name: Save sample playbooks
if: ${{ !cancelled() }}
uses: ./.github/actions/sample-validation-save-playbooks
- name: Upload validation report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
if: always()
with:
name: validation-report-semantic-kernel-migration
@@ -886,8 +675,6 @@ jobs:
needs:
- validate-01-get-started
- validate-02-agents
- validate-02-agents-harness
- validate-02-agents-tools
- validate-02-agents-openai
- validate-02-agents-azure
- validate-02-agents-anthropic
@@ -898,16 +685,15 @@ jobs:
- validate-02-agents-copilotstudio
- validate-02-agents-custom
- validate-03-workflows
- validate-04-hosting-foundry-hosted-agents
- validate-04-hosting-other
- validate-04-hosting
- validate-05-end-to-end
- validate-autogen-migration
- validate-semantic-kernel-migration
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Download all validation reports
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
pattern: validation-report-*
path: reports/
@@ -915,7 +701,7 @@ jobs:
- name: Restore validation history
id: cache-restore
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: validation-history/
key: validation-history-${{ github.run_id }}
@@ -933,13 +719,13 @@ jobs:
run: cat trend-report.md >> "$GITHUB_STEP_SUMMARY"
- name: Save validation history
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: validation-history/
key: validation-history-${{ github.run_id }}
- name: Upload trend report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
if: always()
with:
name: validation-trend-report
@@ -20,9 +20,9 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Download coverage report
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
github-token: ${{ github.token }}
run-id: ${{ github.event.workflow_run.id }}
@@ -30,34 +30,24 @@ jobs:
merge-multiple: true
- name: Display structure of downloaded files
run: ls
- name: Read and validate PR number
# Keep the artifact handoff aligned with the workflow run that produced it.
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
RUN_HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
- 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
ARTIFACT_PR_NUMBER=$(cat pr_number)
if ! [[ "$ARTIFACT_PR_NUMBER" =~ ^[0-9]+$ ]]; then
PR_NUMBER=$(cat pr_number)
if ! [[ "$PR_NUMBER" =~ ^[0-9]+$ ]]; then
echo "::error::PR number file contains invalid content"
exit 1
fi
PR_HEAD_SHA=$(gh pr view "$ARTIFACT_PR_NUMBER" --repo "$REPO" --json headRefOid --jq '.headRefOid')
if [ "$PR_HEAD_SHA" != "$RUN_HEAD_SHA" ]; then
echo "::error::PR head SHA does not match the triggering workflow run"
exit 1
fi
echo "PR_NUMBER=$ARTIFACT_PR_NUMBER" >> "$GITHUB_ENV"
echo "PR_NUMBER=$PR_NUMBER" >> "$GITHUB_ENV"
- name: Pytest coverage comment
id: coverageComment
uses: MishaKav/pytest-coverage-comment@dd5b80bde6d16941f336518e92929e89069d8451 # v1.7.2
uses: MishaKav/pytest-coverage-comment@26f986d2599c288bb62f623d29c2da98609e9cd4 # v1.6.0
with:
github-token: ${{ github.token }}
issue-number: ${{ env.PR_NUMBER }}
+4 -7
View File
@@ -6,9 +6,6 @@ on:
paths:
- "python/packages/**"
- "python/tests/unit/**"
- "python/scripts/workspace_poe_tasks.py"
- ".github/scripts/python_check_coverage.py"
- ".github/workflows/python-test-coverage.yml"
env:
# Configure a constant location for the uv cache
UV_CACHE_DIR: /tmp/.uv-cache
@@ -25,7 +22,7 @@ jobs:
env:
UV_PYTHON: "3.11"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 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
@@ -40,12 +37,12 @@ jobs:
env:
# Configure a constant location for the uv cache
UV_CACHE_DIR: /tmp/.uv-cache
- name: Run aggregate tests with coverage report
- name: Run all tests with coverage report
run: uv run poe test -A -C --cov-report=xml:python-coverage.xml -q --junitxml=pytest.xml
- name: Check coverage threshold
run: python ${{ github.workspace }}/.github/scripts/python_check_coverage.py python-coverage.xml ${{ env.COVERAGE_THRESHOLD }}
run: python ${{ github.workspace }}/.github/workflows/python-check-coverage.py python-coverage.xml ${{ env.COVERAGE_THRESHOLD }}
- name: Upload coverage report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
path: |
python/python-coverage.xml
+2 -6
View File
@@ -5,10 +5,6 @@ on:
branches: ["main", "feature*"]
paths:
- "python/**"
- "!python/AGENTS.md"
- "!python/**/AGENTS.md"
- "!python/.github/skills/*"
- "!python/.github/skills/**"
env:
# Configure a constant location for the uv cache
UV_CACHE_DIR: /tmp/.uv-cache
@@ -31,14 +27,14 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 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 }}
exclude-packages: ${{ matrix.python-version == '3.10' && 'agent-framework-github-copilot agent-framework-azure-cosmos-memory' || '' }}
exclude-packages: ${{ matrix.python-version == '3.10' && 'agent-framework-github-copilot' || '' }}
env:
# Configure a constant location for the uv cache
UV_CACHE_DIR: /tmp/.uv-cache
+3 -20
View File
@@ -26,31 +26,14 @@ jobs:
ping_stale:
name: "Ping stale issues and PRs"
runs-on: ubuntu-latest
environment: github-app-auth
permissions:
contents: read
id-token: write
issues: write
pull-requests: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Get GitHub automation token
id: github-auth
uses: ./.github/actions/github-app-token
with:
mode: ${{ vars.GH_APP_AUTH_MODE }}
azure-client-id: ${{ secrets.GH_APP_AZURE_CLIENT_ID }}
azure-tenant-id: ${{ secrets.GH_APP_AZURE_TENANT_ID }}
azure-subscription-id: ${{ secrets.GH_APP_AZURE_SUBSCRIPTION_ID }}
key-vault-name: ${{ secrets.GH_APP_KEY_VAULT_NAME }}
key-name: ${{ secrets.GH_APP_KEY_NAME }}
github-app-client-id: ${{ secrets.GH_APP_CLIENT_ID }}
github-app-installation-id: ${{ secrets.GH_APP_INSTALLATION_ID }}
repository: ${{ github.repository }}
fallback-token: ${{ secrets.GH_ACTIONS_PR_WRITE }}
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: '3.13'
@@ -60,7 +43,7 @@ jobs:
- name: Run stale issue/PR ping
run: python .github/scripts/stale_issue_pr_ping.py
env:
GITHUB_TOKEN: ${{ steps.github-auth.outputs.token }}
GITHUB_TOKEN: ${{ secrets.GH_ACTIONS_PR_WRITE }}
TEAM_SLUG: ${{ secrets.DEVELOPER_TEAM }}
DAYS_THRESHOLD: ${{ github.event.inputs.days_threshold || '4' }}
DRY_RUN: ${{ github.event.inputs.dry_run || 'false' }}
-6
View File
@@ -206,11 +206,6 @@ temp*/
.temp/
# AI
**/.checkpoints/
# Local AgentServer file store + crash-recovery HOME roots used by hosted samples
**/.agentserver-state/
**/.agentserver-state-*/
**/.home-*/
.claude/
.omc/
.omx/
@@ -218,7 +213,6 @@ WARP.md
**/memory-bank/
**/projectBrief.md
**/tmpclaude*
.kiro/
# Dependency-bound validation reports
python/scripts/dependency-*-results.json
python/scripts/dependencies/dependency-*-results.json
+1 -20
View File
@@ -127,28 +127,9 @@ We use and recommend the following workflow:
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. Address feedback from the code maintainers. Reply to every review comment with
the outcome and resolve each completed review conversation yourself before
requesting another review.
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.
### Resolving PR Review Comments
PR authors are responsible for closing out all review conversations on their pull
requests, including conversations opened by reviewers. Do not wait for the reviewer
or a maintainer to resolve completed conversations for you.
For every review comment:
- If the feedback was addressed, reply with a brief explanation and, preferably,
the commit containing the change.
- If the feedback was not addressed, reply with the reason why.
After replying and completing any necessary discussion, **resolve the conversation
yourself**. Leave a conversation open only while it has an unanswered question or
active discussion. Reviewers may reopen a conversation if further changes or
discussion are needed.
### Development Setup
Each language has its own dev setup guide, coding standards, and build scripts:
+6 -11
View File
@@ -6,15 +6,12 @@
[![MS Learn Documentation](https://img.shields.io/badge/MS%20Learn-Documentation-blue)](https://learn.microsoft.com/en-us/agent-framework/)
[![PyPI](https://img.shields.io/pypi/v/agent-framework)](https://pypi.org/project/agent-framework/)
[![NuGet](https://img.shields.io/nuget/v/Microsoft.Agents.AI)](https://www.nuget.org/profiles/MicrosoftAgentFramework/)
[![GitHub stars](https://img.shields.io/github/stars/microsoft/agent-framework?style=social)](https://github.com/microsoft/agent-framework)
[![GitHub stars](https://img.shields.io/github/stars/microsoft/agent-framework?style=social)](https://github.com/microsoft/agent-framework/stargazers)
Microsoft Agent Framework (MAF) is an open, multi-language framework for building **production-grade AI agents and multi-agent workflows** in **.NET and Python**.
Microsoft Agent Framework is built for teams taking agents from prototype to production. It provides a consistent foundation for building, orchestrating, and operating agent systems across Python, .NET and Go, while keeping architecture choices open as requirements evolve, and supports a broad ecosystem including Microsoft Foundry, Azure OpenAI, OpenAI, and the GitHub Copilot SDK, with samples and hosting patterns for both local development and cloud deployment.
> [!NOTE]
> For the Go SDK, including its documentation, samples, contribution guidance, and issue tracker, visit [microsoft/agent-framework-go](https://github.com/microsoft/agent-framework-go/).
Microsoft Agent Framework is built for teams taking agents from prototype to production. It provides a consistent foundation for building, orchestrating, and operating agent systems across Python and .NET, while keeping architecture choices open as requirements evolve, and supports a broad ecosystem including Microsoft Foundry, Azure OpenAI, OpenAI, and the GitHub Copilot SDK, with samples and hosting patterns for both local development and cloud deployment.
<p align="center">
<a href="https://www.youtube.com/watch?v=AAgdMhftj8w" title="Watch the full Agent Framework introduction (30 min)">
@@ -42,7 +39,6 @@ Explore new MAF capabilities and real implementation patterns on the [official b
- **Python and C#/.NET Support**: Full framework support for both Python and C#/.NET implementations with consistent APIs
- [Python packages](./python/packages/) | [.NET source](./dotnet/src/)
- **Go Support**: For the Go SDK, including its documentation, samples, contribution guidance, and issue tracker, visit [microsoft/agent-framework-go](https://github.com/microsoft/agent-framework-go/).
- **Multiple Agent Provider Support**: Support for various LLM providers with more being added continuously
- [Python examples](./python/samples/02-agents/providers/) | [.NET examples](./dotnet/samples/02-agents/AgentProviders/)
- **Middleware**: Flexible middleware system for request/response processing, exception handling, and custom pipelines
@@ -165,19 +161,19 @@ Console.WriteLine(await agent.RunAsync("Write a haiku about Microsoft Agent Fram
### Python
- [Getting Started](./python/samples/01-get-started): progressive tutorial from hello-world to workflows
- [Getting Started](./python/samples/01-get-started): progressive tutorial from hello-world to hosting
- [Agent Concepts](./python/samples/02-agents): deep-dive samples by topic (tools, middleware, providers, etc.)
- [Workflows](./python/samples/03-workflows): workflow creation and integration with agents
- [Hosting](./python/samples/04-hosting): A2A, self-hosted protocol helpers, and Foundry hosted agents. Durable Task and Azure Functions samples are in the [Durable Agent Framework extension](https://github.com/microsoft/agent-framework-durable-extension/tree/main/python/samples).
- [Hosting](./python/samples/04-hosting): A2A, Azure Functions, Durable Task hosting
- [End-to-End](./python/samples/05-end-to-end): full applications, evaluation, and demos
### .NET
- [Getting Started](./dotnet/samples/01-get-started): progressive tutorial from hello agent to workflows
- [Getting Started](./dotnet/samples/01-get-started): progressive tutorial from hello agent to hosting
- [Agent Concepts](./dotnet/samples/02-agents/Agents): basic agent creation and tool usage
- [Agent Providers](./dotnet/samples/02-agents/AgentProviders): samples showing different agent providers
- [Workflows](./dotnet/samples/03-workflows): advanced multi-agent patterns and workflow orchestration
- [Hosting](./dotnet/samples/04-hosting): A2A and Foundry hosted agents. Durable agent and workflow samples are in the [Durable Agent Framework extension](https://github.com/microsoft/agent-framework-durable-extension/tree/main/dotnet/samples).
- [Hosting](./dotnet/samples/04-hosting): A2A, Durable Agents, Durable Workflows
- [End-to-End](./dotnet/samples/05-end-to-end): full applications and demos
## Community & Feedback
@@ -203,7 +199,6 @@ For environment variable configuration specific to each sample, refer to the REA
## Contributor Resources
- [Contributing Guide](./CONTRIBUTING.md)
- [Code of Conduct](./CODE_OF_CONDUCT.md)
- [Python Development Guide](./python/DEV_SETUP.md)
- [Design Documents](./docs/design)
- [Architectural Decision Records](./docs/decisions)
+2 -2
View File
@@ -2,7 +2,7 @@
**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, Microsoft Foundry). It provides both simple chat agents and complex multi-agent workflows with graph-based orchestration.
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?**
@@ -12,7 +12,7 @@ The framework offers:
- **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, Microsoft Foundry, and other providers
- **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)?**
@@ -14,9 +14,6 @@
# - Knowledge Agent: Performs generic web searches.
# - Coder Agent: Able to write and execute code.
# - Weather Agent: Provides weather information.
#
# Example input:
# Find the current temperatures in Seattle and San Francisco, calculate the difference in Celsius and Fahrenheit, and recommend what clothing to pack for each city.
#
kind: Workflow
maxTurns: 500
@@ -267,14 +264,14 @@ trigger:
output:
messages: Local.Plan
input:
arguments:
team: =Local.TeamDescription
messages: |-
=UserMessage(
"Please briefly explain what went wrong on this last run (the root cause of the failure),
and then come up with a new plan that takes steps and/or includes hints to overcome prior challenges and especially avoids repeating the same mistakes.
As before, the new plan should be concise, be expressed in bullet-point form, and only involve the team members already described
(do not involve any other outside people since we cannot contact anyone else).")
As before, the new plan should be concise, be expressed in bullet-point form, and consider the following team composition
(do not involve any other outside people since we cannot contact anyone else):
{Local.TeamDescription}")
- kind: SetTextVariable
id: setVariable_jW7tmM
@@ -11,7 +11,7 @@ trigger:
kind: OnConversationStart
id: workflow_demo
actions:
- kind: InvokeAzureAgent
id: question_student
conversationId: =System.ConversationId
+2 -2
View File
@@ -2,7 +2,7 @@
# These are optional elements. Feel free to remove any of them.
status: accepted
contact: westey-m
date: 2025-07-10
date: 2025-07-10 {YYYY-MM-DD when the decision was last updated}
deciders: sergeymenshykh, markwallace, rbarreto, dmytrostruk, westey-m, eavanvalkenburg, stephentoub
consulted:
informed:
@@ -139,7 +139,7 @@ Therefore something like `AgentResponse.Text` which also aggregates all `TextCon
#### 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 response messages.
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
+8 -8
View File
@@ -113,7 +113,7 @@ Implement a hybrid strategy where common tools use generic `AITool`-derived abst
### AI Agent Tool Types Availability
Tool Type | Microsoft Foundry Agent Service | OpenAI Assistant API | OpenAI ChatCompletion API | OpenAI Responses API | Amazon Bedrock Agents | Google | Anthropic | Description
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.
@@ -132,7 +132,7 @@ Image Generation | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | Generates or edits
#### Function Calling
<details>
<summary>Microsoft Foundry Agent Service</summary>
<summary>Azure AI Foundry Agent Service</summary>
Source: <a href="https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/tools/function-calling?pivots=rest">https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/tools/function-calling?pivots=rest</a>
Message Request:
@@ -401,7 +401,7 @@ Image Generation | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | Generates or edits
#### Code Interpreter
<details>
<summary>Microsoft Foundry Agent Service</summary>
<summary>Azure AI Foundry Agent Service</summary>
<p>Source: <a href="https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/tools/code-interpreter-samples?pivots=rest-api">https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/tools/code-interpreter-samples?pivots=rest-api</a></p>
<p>.NET Support: ✅</p>
@@ -709,7 +709,7 @@ Image Generation | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | Generates or edits
#### Search and Retrieval
<details>
<summary>Microsoft Foundry Agent Service</summary>
<summary>Azure AI Foundry Agent Service</summary>
Source: <a href="https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/tools/file-search-upload-files?pivots=rest">https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/tools/file-search-upload-files?pivots=rest</a>
File Search Request:
@@ -1083,7 +1083,7 @@ Image Generation | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | Generates or edits
#### Web Search
<details>
<summary>Microsoft Foundry Agent Service</summary>
<summary>Azure AI Foundry Agent Service</summary>
Source: <a href="https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/tools/bing-code-samples?pivots=rest">https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/tools/bing-code-samples?pivots=rest</a>
Bing Search Message Request:
@@ -1630,7 +1630,7 @@ Image Generation | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | Generates or edits
#### OpenAPI Spec Tool
<details>
<summary>Microsoft Foundry Agent Service</summary>
<summary>Azure AI Foundry Agent Service</summary>
Source: <a href="https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/tools/openapi-spec-samples?pivots=rest-api">https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/tools/openapi-spec-samples?pivots=rest-api</a><br>
Source: <a href="https://learn.microsoft.com/en-us/rest/api/aifoundry/aiagents/run-steps/get-run-step?view=rest-aifoundry-aiagents-v1&tabs=HTTP#runstepopenapitoolcall">https://learn.microsoft.com/en-us/rest/api/aifoundry/aiagents/run-steps/get-run-step?view=rest-aifoundry-aiagents-v1&tabs=HTTP#runstepopenapitoolcall</a>
@@ -1712,7 +1712,7 @@ Image Generation | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | Generates or edits
#### Stateful Functions
<details>
<summary>Microsoft Foundry Agent Service</summary>
<summary>Azure AI Foundry Agent Service</summary>
Source: <a href="https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/tools/azure-functions-samples?pivots=rest">https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/tools/azure-functions-samples?pivots=rest</a>
Message Request:
@@ -1832,7 +1832,7 @@ Image Generation | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | Generates or edits
#### Microsoft Fabric
<details>
<summary>Microsoft Foundry Agent Service</summary>
<summary>Azure AI Foundry Agent Service</summary>
Source: <a href="https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/tools/fabric?pivots=rest">https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/tools/fabric?pivots=rest</a>
Message Request:
+2 -2
View File
@@ -2,7 +2,7 @@
# These are optional elements. Feel free to remove any of them.
status: accepted
contact: westey-m
date: 2025-09-12
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:
@@ -25,7 +25,7 @@ See various features that would need to be supported via this type of mechanism,
- 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 [Microsoft 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 [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
@@ -1125,7 +1125,7 @@ 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.
No specific code examples available for interception.
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)
@@ -57,7 +57,7 @@ This section describes different options for various aspects required to add lon
### 1. Methods for Working with Long-Running Operations
Based on the analysis of existing APIs that support long-running operations (such as OpenAI Responses, Microsoft Foundry Agents, and A2A),
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.
@@ -757,7 +757,7 @@ Some of them natively support resuming streaming from a specific point in the st
| API | Can Resume Streaming | Model |
|-------------------------|--------------------------------------|------------------------------------------------------------------------------------------------------------|
| OpenAI Responses | Yes | StreamingResponseUpdate.**SequenceNumber** + GetResponseStreamingAsync(responseId, **startingAfter**, ct) |
| Microsoft Foundry Agents | Emulated<sup>2</sup> | RunStep.**Id** + custom pseudo code: client.Runs.GetRunStepsAsync(...).AllStepsAfter(**stepId**) |
| Azure AI Foundry Agents | Emulated<sup>2</sup> | RunStep.**Id** + custom pseudo code: client.Runs.GetRunStepsAsync(...).AllStepsAfter(**stepId**) |
| A2A | Implementation dependent<sup>1</sup> | |
<sup>1</sup> The [A2A specification](https://github.com/a2aproject/A2A/blob/main/docs/topics/streaming-and-async.md#1-streaming-with-server-sent-events-sse)
@@ -765,7 +765,7 @@ allows an A2A agent implementation to decide how to handle streaming resumption:
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._
<sup>2</sup> The Microsoft 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.
<sup>2</sup> 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
@@ -828,7 +828,7 @@ Sequence of updates from OpenAI Responses API to answer the question "What time
| resp_2 | 10 | resp.output_item.done | - | InProgress | |
| resp_2 | 11 | resp.completed | Completed | Completed | |
Sequence of updates from Microsoft Foundry Agents API to answer the question "What time is it?" using a function call:
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 | |
@@ -852,7 +852,7 @@ Sequence of updates from Microsoft Foundry Agents API to answer the question "Wh
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 Microsoft Foundry Agents, use
- `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.
@@ -1089,7 +1089,7 @@ public class ChatOptions
##### 6.1.5 Continuation Token of a Custom Type
The option is similar to the "6.1.3 Continuation Token of System.ClientModel.ContinuationToken Type" option but suggests using a
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**
@@ -1203,7 +1203,7 @@ response = await agent.CancelRunAsync(response.ResponseId, new AgentCancelRunOpt
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 Microsoft Foundry Agents, require the thread identifier to cancel a run. To accommodate this requirement, the `CancelRunAsync` method
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
@@ -1574,7 +1574,7 @@ the thread is provided with background operations consistently for all runs.
</details>
<details>
<summary>Microsoft Foundry Agents</summary>
<summary>Azure AI Foundry Agents</summary>
- Create a thread and run the agent against it and wait for it to complete using polling:
```csharp
@@ -34,11 +34,11 @@ Key changes:
1. **New `agent-framework-openai` package** with dependencies on `agent-framework-core`, `openai`, and `packaging` only.
2. **Class renames**: `OpenAIResponsesClient``OpenAIChatClient` (Responses API), `OpenAIChatClient``OpenAIChatCompletionClient` (Chat Completions API). Old names remain as deprecated aliases.
3. **Deprecated classes**: `OpenAIAssistantsClient`, all `AzureOpenAI*Client` classes, `AzureAIClient`, `AzureAIAgentClient`, and `AzureAIProjectAgentProvider` are marked deprecated.
4. **New `FoundryChatClient`** in azure-ai for Microsoft Foundry Responses API access, built on `RawFoundryChatClient(RawOpenAIChatClient)`.
4. **New `FoundryChatClient`** in azure-ai for Azure AI Foundry Responses API access, built on `RawFoundryChatClient(RawOpenAIChatClient)`.
5. **All deprecated `AzureOpenAI*` classes** consolidated into a single file (`_deprecated_azure_openai.py`) in the azure-ai package for clean future deletion.
6. **Core's `agent_framework.openai` and `agent_framework.azure` namespaces** become lazy-loading gateways, preserving backward-compatible import paths while removing hard dependencies.
7. **Unified `model` parameter** replaces `model_id` (OpenAI), `deployment_name` (Azure OpenAI), and `model_deployment_name` (Azure AI) across all client constructors. The term `model` is intentionally generic: it naturally maps to an OpenAI model name *and* to an Azure OpenAI deployment name, making it straightforward to use `OpenAIChatClient` with either OpenAI or Azure OpenAI backends (via `AsyncAzureOpenAI`). Environment variables are similarly unified (e.g., `OPENAI_MODEL` instead of separate `OPENAI_CHAT_MODEL_ID` / `OPENAI_CHAT_COMPLETION_MODEL_ID`).
8. **`FoundryAgent`** replaces the pattern of `Agent(client=AzureAIClient(...))` for connecting to pre-configured agents in Microsoft Foundry (PromptAgents and HostedAgents). The underlying `RawFoundryAgentChatClient` is an implementation detail — most users interact only with `FoundryAgent`. `AzureAIAgentClient` is separately deprecated as it refers to the V1 Agents Service API. See below for design rationale.
8. **`FoundryAgent`** replaces the pattern of `Agent(client=AzureAIClient(...))` for connecting to pre-configured agents in Azure AI Foundry (PromptAgents and HostedAgents). The underlying `RawFoundryAgentChatClient` is an implementation detail — most users interact only with `FoundryAgent`. `AzureAIAgentClient` is separately deprecated as it refers to the V1 Agents Service API. See below for design rationale.
### Foundry Agent Design: `FoundryAgentClient` vs `FoundryAgent`
@@ -7,11 +7,11 @@ consulted: Pratyush Mishra, Shivam Shrivastava, Manni Arora (Centrica eval scena
informed: Agent Framework team, Foundry Evals team
---
# Agent Evaluation Architecture with Microsoft Foundry Integration
# Agent Evaluation Architecture with Azure AI Foundry Integration
## Context and Problem Statement
Microsoft Foundry provides a rich evaluation service for AI agents — built-in evaluators for agent behavior (task adherence, intent resolution), tool usage (tool call accuracy, tool selection), quality (coherence, fluency, relevance), and safety (violence, self-harm, prohibited actions). Results are viewable in the Foundry portal with dashboards and comparison views.
Azure AI Foundry provides a rich evaluation service for AI agents — built-in evaluators for agent behavior (task adherence, intent resolution), tool usage (tool call accuracy, tool selection), quality (coherence, fluency, relevance), and safety (violence, self-harm, prohibited actions). Results are viewable in the Foundry portal with dashboards and comparison views.
However, using Foundry Evals with an agent-framework agent today requires significant manual effort. Developers must:
@@ -445,7 +445,7 @@ These factorings produce different scores for the same conversation. The framewo
### Azure AI: FoundryEvals
`Evaluator` implementation backed by Microsoft Foundry:
`Evaluator` implementation backed by Azure AI Foundry:
```python
class FoundryEvals:
@@ -812,4 +812,4 @@ public sealed class EvalItem
## More Information
- [Foundry Evals documentation](https://learn.microsoft.com/azure/ai-foundry/concepts/evaluation-approach-gen-ai) — Microsoft Foundry evaluation overview
- [Foundry Evals documentation](https://learn.microsoft.com/azure/ai-foundry/concepts/evaluation-approach-gen-ai) — Azure AI Foundry evaluation overview
@@ -43,11 +43,6 @@ FIDES (Flow Integrity Deterministic Enforcement System) is a label-based securit
3. **Variable Indirection**`ContentVariableStore` and `VariableReferenceContent` for physical isolation of untrusted content from the LLM context.
4. **Quarantined Execution**`quarantined_llm` and `inspect_variable` tools for isolated processing of untrusted data with audit logging.
In addition, remote MCP integrations are secured through two mechanisms:
- **Hint-based tool auto-labeling**: MCP `ToolAnnotations` (`readOnlyHint`, `openWorldHint`, etc.) are mapped to FIDES tool properties (`source_integrity`, `accepts_untrusted`, `max_allowed_confidentiality`).
- **Server `_meta.ifc` result labels**: MCP result metadata is parsed into per-item `security_label` values, so provider-supplied IFC labels are enforced by middleware.
### Consequences
- Good, because it provides deterministic security guarantees about what untrusted content can influence.
@@ -122,13 +117,6 @@ Monitor agent behavior and block suspicious actions post-facto.
- Uses existing `FunctionMiddleware` base class.
- Attaches labels via `additional_properties` (no schema changes).
- Leverages `SerializationMixin` for label persistence.
- Integrates MCP hint/result metadata through `additional_properties` keys (`max_allowed_confidentiality`, `source_integrity`, `__mcp_result_meta__`) without transport-specific policy code in core middleware.
### MCP-Specific Security Notes
- `SecureMCPToolProxy` applies `apply_mcp_security_labels(...)` automatically when connecting an MCP tool or URL.
- For servers like the GitHub MCP server (with `X-MCP-Features: ifc_labels`), `_meta.ifc` labels are considered authoritative for per-result label assignment.
- Tools that are not explicitly `readOnlyHint=True` are treated as potential sinks and default to `max_allowed_confidentiality=PUBLIC` to prevent exfiltration.
### Backwards Compatibility
@@ -9,7 +9,7 @@ deciders: evmattso
## What is the goal of this feature?
Enable Agent Framework users to consume Foundry **toolboxes** — named, versioned bundles of tool definitions stored server-side in a Microsoft Foundry project — directly from `FoundryChatClient`, without dropping to the raw `azure-ai-projects` SDK.
Enable Agent Framework users to consume Foundry **toolboxes** — named, versioned bundles of tool definitions stored server-side in an Azure AI Foundry project — directly from `FoundryChatClient`, without dropping to the raw `azure-ai-projects` SDK.
A user who has configured a toolbox in the Foundry portal (or via the raw SDK) should be able to load it into an agent with a single call:
@@ -1,7 +1,7 @@
---
status: superseded by [ADR-0030](0030-hosted-platform-context-agentserver-2.0.md)
status: accepted
contact: rogerbarreto
date: 2026-06-29
date: 2026-05-07
deciders: rogerbarreto
consulted: []
informed: []
@@ -9,8 +9,6 @@ informed: []
# Hosted session identity context for Foundry Hosting
> **Superseded by [ADR-0030](0030-hosted-platform-context-agentserver-2.0.md).** `Azure.AI.AgentServer.*` 2.0.0 (responses protocol `2.0.0`) replaced `ResponseContext.Isolation` (`UserIsolationKey` / `ChatIsolationKey`, headers `x-agent-user-isolation-key` / `x-agent-chat-isolation-key`) with `ResponseContext.PlatformContext` (`UserIdKey` / `CallId`, headers `x-agent-user-id` / `x-agent-foundry-call-id`). The chat isolation key was removed and `HostedSessionContext` is now user-only. This ADR is retained as the historical record of the original design.
## Context and Problem Statement
Server-hosted Foundry agents need a way to scope per-user state (most notably `FoundryMemoryProvider` memories) by the end user that initiated the request. The Foundry platform already injects `x-agent-user-isolation-key` and `x-agent-chat-isolation-key` headers on every Responses request, but the agent-framework hosting layer did not surface those values to `AIContextProvider` instances. The provider's `stateInitializer` only received an `AgentSession?` with no identity attached, so per-user scoping was impossible without out-of-band plumbing.
-524
View File
@@ -1,524 +0,0 @@
---
status: accepted
contact: eavanvalkenburg
date: 2026-06-30
deciders: eavanvalkenburg
consulted: rogerbarreto, moonbox3
---
# Python protocol helpers and optional execution state
## Context and Problem Statement
Agent Framework needs to help applications expose agents and workflows over external protocols such as OpenAI
Responses, Telegram, Activity Protocol, and future transports.
FastAPI, Starlette, Azure Functions, Django, Telegram SDKs, Bot Framework SDKs, and other app frameworks already own
route registration, dependency injection, middleware, authentication, background tasks, lifecycle, and native client
calls. Agent Framework should not duplicate those surfaces unless a specific hosting environment requires it.
## Decision Drivers
- Keep the released surface small enough to explain without first teaching a channel framework.
- Provide reusable Agent Framework run translation that works with FastAPI, Django, and other web frameworks.
- Let app/framework code own route declaration, auth, middleware, native SDK clients, command handling, and background
work.
- Keep stateful execution support explicit: session lookup/storage and workflow checkpoint lookup/storage may still need
a small AF-owned home.
## Considered Options
1. Create protocol-specific hosts.
2. Ship a full host/channel framework with route contribution and channel hooks.
3. Ship protocol conversion helpers plus optional execution state.
### 1. Create protocol-specific hosts
- Good: no new shared abstraction.
- Neutral: each protocol host can evolve independently.
- Bad: every package reinvents AF input/result mapping, session-key conventions, and stateful execution helpers.
### 2. Ship a full host/channel framework
- Good: one object can assemble routes, channels, session handling, hooks, and lifecycle callbacks.
- Good: app code using the supported host shape can be short.
- Bad: the framework owns concerns already handled by web frameworks, protocol SDKs and/or other services.
- Bad: users must understand `Channel`, contribution, hook, and host-dispatch concepts before they can see how a request
becomes `agent.run(...)`.
- Bad: the abstraction is hard to reuse outside the chosen web framework.
### 3. Ship protocol helpers plus optional execution state
- Good: protocol packages provide the Agent Framework run value directly: `<protocol>_to_run(...)` and
`<protocol>_from_run(...)` style helpers.
- Good: apps keep native FastAPI, Starlette, Azure Functions, Django, Bot Framework, or Telegram SDK code.
- Good: helper functions can be tested without a web framework app or host pipeline.
- Good: small state objects can still own target-coupled state: `AgentState` pairs an agent target with a `SessionStore`,
and `WorkflowState` resolves a workflow target while reusing the existing `CheckpointStorage` abstraction.
- Good: provides maximum configurability in handling input and outputs (outside of the conversions)
- Bad: building a first iteration of a new Host is more verbose.
- Bad: samples show more explicit route/client code than a fully assembled channel host.
## Decision Outcome
Chosen option: **3. Ship protocol helpers plus optional execution state**.
Protocol packages own:
- parsing protocol-native input into Agent Framework run input and options;
- rendering `AgentResponse`, `AgentResponseUpdate`, workflow results, or workflow updates back into protocol-native
response/event payloads;
- protocol-specific isolation/session id helper functions when useful, such as `telegram_session_id(update)`;
- protocol-specific typing/update event helpers where the protocol has a native concept.
Application or web-framework code owns:
- HTTP route declaration and route grouping;
- dependency injection;
- authentication and authorization;
- middleware;
- background tasks and webhook acknowledgement policy;
- native protocol SDK clients and outbound calls;
- command registration and command dispatch;
- request/response status codes and framework-specific error handling;
- choosing the isolation/session id source for the current deployment and route.
The application builder can make the server exactly as they see fit, but this is outside the responsibilities of this proposed scheme.
This might include implementing other known API surfaces from vendors like OpenAI, such as creating conversations, vector stores, deleting things, etc.
If they want they can build the full OpenAI API, but it will include code that does not rely on agent-framework-hosting, which is fine.
They are responsible for what they expose.
The optional execution-state helpers, if provided, are limited to shared execution state:
- `AgentState`: one `SupportsAgentRun`-compatible target plus a `SessionStore`;
- `WorkflowState`: one `Workflow`, `WorkflowBuilder`-shaped builder, orchestration builder, or workflow factory;
- `SessionStore`: plain async storage (`get` / `set` / `delete`) by an app-selected id.
The store does not create sessions. `AgentState` provides the target-aware `get_or_create_session(...)` helper because
only the state object has both the store and the resolved agent target. Workflow checkpointing should use the existing
`CheckpointStorage` abstraction directly; app/state code may keep a small cursor (`session_id -> checkpoint_id`) when it
needs to resume a workflow for a session.
These objects are **not** app objects, channel registries, or route owners. They do not own FastAPI/Starlette setup,
route contribution, protocol dispatch, command projection, or native SDK calls.
### Helper naming and families
Helpers should be protocol-specific, not generic. Avoid a generic `protocol_to_run(...)` name in public samples because it
hides the protocol-specific contract behind a second abstraction.
Protocol packages should consider these helper families. This table is a set of examples, not a required protocol or
checklist. Not every protocol needs every helper, but when a protocol has the concept the naming should stay consistent:
| Helper family | Shape | Purpose |
| --- | --- | --- |
| Run conversion | `<protocol>_to_run(...)` | Convert one protocol-native call/update/request into `Agent.run` or `Workflow.run` values. |
| Final rendering | `<protocol>_from_run(...)` | Convert a final `AgentResponse` / workflow result into protocol-native response payloads or operations. |
| Stream rendering | `<protocol>_from_streaming_run(...)` | Convert `ResponseStream` / workflow updates into protocol-native events or operations. |
| Session id extraction | `<protocol>_session_id(...)` | Extract the protocol's natural continuation/partition key from the call, if present. |
| Command/action parsing | `<protocol>_command(...)` | Parse a protocol-native command/action/operation name without deciding app policy. |
Examples:
- `responses_to_run(...)`, `responses_from_run(...)`, `responses_from_streaming_run(...)`,
`responses_session_id(...)`;
- `telegram_to_run(...)`, `telegram_from_run(...)`, `telegram_from_streaming_run(...)`,
`telegram_session_id(...)`, `telegram_command(...)`;
- `activity_to_run(...)`, `activity_from_run(...)`, `activity_session_id(...)`, `activity_command(...)`;
- `discord_to_run(...)`, `discord_from_run(...)`, `discord_session_id(...)`, `discord_command(...)`.
The app still owns what a parsed command means. For example, a Telegram `/new`, Discord slash command, Bot Framework
command activity, or A2A cancellation/request action may parse through a command/action helper, but the route or SDK
handler decides whether that command clears a session, cancels a task, calls an agent, or is ignored.
Additional helper functions can be protocol-specific when the concept is not broadly shared. Examples include
`telegram_chat_id(...)`, `telegram_callback_query_id(...)`, `telegram_media_file_id(...)`,
`discord_interaction_id(...)`, `a2a_task_id(...)`, `a2a_context_id(...)`, and MCP tool/prompt/resource helpers. These
helpers should still stay side-effect-free: they extract, normalize, or describe protocol data, while app/native SDK code
performs acknowledgements, sends/edits messages, resolves protected file URLs, applies rate limits, and registers
handlers.
### Security responsibilities for application builders
The application builder owns the trust boundary. Protocol helper packages can parse native payloads and expose candidate
ids or operations, but they do not authenticate callers, authorize access to state, or decide which side effects are
allowed.
Application code that uses these helpers are responsible for (this means that we advice you to think through these topics,
but ultimately, the choice of which controls are needed for the intended use case is up to the application builder):
- authenticate the caller through the app's normal mechanism before using protocol-provided ids;
- authorize any caller-supplied session, checkpoint, task, context, conversation, thread, or response id before loading
state for it;
- bind externally supplied ids to the authenticated user, tenant, workspace, installation, or chat context before using
them as `SessionStore` keys or checkpoint cursor keys;
- treat `<protocol>_session_id(...)` results as untrusted candidate keys until that ownership check has passed;
- keep platform-provided isolation helpers fail-closed outside their trusted hosting environment;
- authorize command/action effects such as reset, cancel, approve, submit, or tool invocation after parsing them;
- opt in explicitly before resolving protected media/resource/file URLs and passing them to a remote model provider;
- persist post-run session or checkpoint state only after `agent.run(...)`, `workflow.run(...)`, or stream finalization has
updated that state.
For Foundry specifically, helpers may read values established by Foundry hosting middleware, but must not treat raw
request headers as trusted Foundry isolation when the app is running outside Foundry. Implementations must test that
non-Foundry requests do not accept spoofable isolation headers as platform-provided keys.
For workflow checkpointing, the checkpoint boundary must be at least as specific as the authorized session/tenant
boundary. A shared storage lookup such as "latest checkpoint for workflow name" is safe only when the storage is already
scoped to the authorized session. In a shared durable store, map the authorized `session_id` to a checkpoint id or other
cursor and load that specific checkpoint.
### Session continuity
Session continuity remains explicit. Run parsing and isolation/session id selection are separate operations because
isolation can come from more than one source:
- protocol input, such as OpenAI Responses `previous_response_id`, a Telegram chat id, or an Activity conversation id;
- running environment, such as Foundry Hosted Agents user/chat isolation context;
- app-specific trusted middleware or route state.
The app chooses which helper to call for that route and deployment. For example:
- `responses_session_id(body)` from `agent-framework-hosting-responses`, which can return either a `resp_*` previous
response id or a `conv_*` conversation id when present;
- `telegram_session_id(update, bot_id=...)` from `agent-framework-hosting-telegram`, which uses the bot and sender for
private chats and the bot and chat for shared group sessions;
- `activity_session_id(activity)`, `discord_session_id(interaction_or_message)`, or
`a2a_session_id(request_context)` from their respective protocol packages;
- `foundry_user_isolation_key()` or `foundry_chat_isolation_key()` from `agent-framework-foundry-hosting`.
Keep these helpers outside `responses_to_run(...)`, `telegram_to_run(...)`, and other run-input parsers. That makes the
trust boundary visible: using a request-derived key is a different decision than using a platform-provided isolation key.
The application builder is also responsible for deciding whether the hosting environment is **persistent** (for example,
a long-running container or web app) or **transient** (for example, Azure Functions, Foundry Hosted Agents, or any
environment where process memory is not a reliable continuity boundary). That decision controls which state mechanisms are
safe to use:
- persistent single-process apps may use in-memory state for local development or simple deployments, while still needing
durable state for multi-replica continuity;
- transient apps must not rely on in-memory `SessionStore` state between calls and need a durable session store or a
service-owned continuation id;
- workflow hosts must choose an explicit `CheckpointStorage` and, when they need per-session resume, a durable
`session_id -> checkpoint_id` cursor because in-process workflow state and in-memory checkpoint cursors do not survive
transient execution.
A `SessionStore` stores `session_id -> AgentSession`, but it does not create sessions. `AgentState` resolves the agent
target and creates the session on first use. Reads return independent working copies so running from one continuation
point does not mutate the stored snapshot or another simultaneous branch:
For agent targets:
```python
session = await state.get_or_create_session(session_id)
target = await state.get_target()
result = await target.run(messages, session=session, options=options)
```
If the protocol mints a new continuation id as part of the response being created (for example, OpenAI Responses
`resp_*` ids), store the **post-run** session explicitly under that new id:
```python
session = await state.get_or_create_session(previous_response_id)
target = await state.get_target()
result = await target.run(messages, session=session, options=options)
await state.set_session(response_id, session)
```
`agent.run(...)` may update the session object (for example, with service continuation state), so the explicit store call
belongs after the run, not before it.
Response ids are immutable continuation points, so simultaneous callers can branch from one `previous_response_id` and
store their completed sessions under different new response ids. A stable `conversation_id` is a mutable head: the app
must explicitly update it after the run and provide single-writer coordination. The hosting state helper does not lock
an entire run or resolve concurrent updates to that stable key.
The session id is a partition key, not proof of identity. App or platform code must authenticate and authorize any
externally supplied key before using it.
### Workflow checkpoints
Workflow checkpointing is execution state, not protocol state. `WorkflowState` pairs a workflow target with checkpoint
state, but it should not wrap or replace the existing `CheckpointStorage` abstraction. Apps should pass the actual
`CheckpointStorage` they want the workflow to use. If an app needs per-session resume, it can keep a small cursor from
authorized `session_id` to `checkpoint_id` (or an equivalent store-specific resume token).
Workflow runs do not currently emit a checkpoint id on `WorkflowRunResult` or normal workflow events by default. The
runner receives checkpoint ids internally from `CheckpointStorage.save(...)`. App/state code that owns the storage can
observe the latest id by querying the storage after a run, for example
`await storage.get_latest(workflow_name=target.name)`.
For workflow targets, app code adapts the protocol helper output into the workflow's expected input and invokes the
workflow through the state object's target:
```python
# session_id must already be authenticated and authorized for this caller
target = await state.get_target()
result = await target.run(message=workflow_input, checkpoint_storage=checkpoint_storage)
latest = await checkpoint_storage.get_latest(workflow_name=target.name)
if latest is not None:
await checkpoint_cursor_store.set(session_id, latest.checkpoint_id)
```
If a route wants to resume from a prior checkpoint, it explicitly chooses the checkpoint and passes it to
`workflow.run(...)`:
```python
# session_id must already be authenticated and authorized for this caller
target = await state.get_target()
checkpoint_id = await checkpoint_cursor_store.get(session_id)
if checkpoint_id is None:
result = await target.run(message=workflow_input, checkpoint_storage=checkpoint_storage)
else:
result = await target.run(checkpoint_id=checkpoint_id, checkpoint_storage=checkpoint_storage)
latest = await checkpoint_storage.get_latest(workflow_name=target.name)
if latest is not None:
await checkpoint_cursor_store.set(session_id, latest.checkpoint_id)
```
`workflow.run(...)` writes checkpoints to the provided storage, so storage selection must be explicit at the route layer.
Protocol helper packages should not own checkpoint layout, route lifecycle, or durable execution.
## Non-goals for v1
The following remain outside the v1 protocol-helper contract. Some are deliberately app-owned in v1; others are possible
future framework work only after a separate design.
### App-owned in v1
The app builder owns these concerns with normal web-framework, SDK, platform, or application code:
- authentication, authorization policy, and allowlists;
- deciding whether identities across protocols map to the same `session_id`;
- non-originating sends using native SDK clients;
- background work, durable execution, retry, and replay when app code owns the work;
- routing between multiple agents.
This is easier in the protocol-helper model than it was in the host/channel model: app code already owns the native SDK
clients, route handlers, authenticated caller context, session id selection, and outbound send calls. An app can link
channels by choosing the same authorized `session_id` for multiple protocols, and can do non-originating delivery by
calling the destination protocol's native client directly. That does not make a reusable framework feature safe by
default; it just means the app-specific version no longer has to fight a host abstraction.
### Future framework work
The following require a reviewed identity, storage, delivery, replay, and observability model before becoming reusable
framework features:
- reusable cross-channel identity linking;
- framework-owned proactive or non-originating delivery;
- fan-out, multicast, selected-channel, active-channel, or all-linked delivery;
- framework-owned delivery observability, dead-letter handling, and replay semantics;
- cross-channel confidentiality and link policy.
These possible framework enhancements are tracked by [ADR-0028](0028-hosting-linking-multicast-enhancements.md). They are
not prerequisites for shipping or using the v1 protocol-helper surface. ADR-0028 was written against the earlier
host/channel framing and must be revised to align with this protocol-helper and execution-state boundary before those
enhancements are implemented.
## Consequences
Positive:
- The released surface is smaller and easier to inspect: helpers plus state, not a channel framework.
- Protocol helpers can be used from FastAPI, Starlette, Azure Functions, Django, CLI tools, tests, or native SDK webhook
handlers.
- App authors can use the authentication, dependency injection, lifecycle, and background-task tools they already know.
- Session continuity stays explicit and debuggable.
- Workflow checkpointing can still be centralized if needed without making protocol packages own routing.
Negative:
- Multi-protocol samples include explicit route/client code.
- Apps that want a batteries-included ASGI app must write or depend on an app-specific wrapper.
- Existing unreleased code and docs that mention channels, contribution, or hooks must be revised before release.
## More Information
- Follow-up linking and multicast ADR: [ADR-0028](0028-hosting-linking-multicast-enhancements.md). That ADR still uses
some earlier host/channel terminology and must be aligned before implementation work starts.
## Appendix: Developer experience sketch
The examples below are sketches, not runtime-ready sample code. They show the minimum shape a developer would need to
build: where protocol helpers are called, where app-owned auth/authorization belongs, where state is loaded/stored, and
where native framework code remains in charge.
### Optional execution state
`AgentState` and `WorkflowState` stay small: they are target-specific state holders, not app hosts.
```python
from typing import Protocol
from agent_framework import AgentSession, SupportsAgentRun, Workflow
class SupportsBuild(Protocol):
def build(self) -> Workflow: ...
class SessionStore:
async def get(self, session_id: str) -> AgentSession | None: ...
async def set(self, session_id: str, session: AgentSession) -> None: ...
async def delete(self, session_id: str) -> None: ...
class CheckpointCursorStore:
async def get(self, session_id: str) -> str | None: ...
async def set(self, session_id: str, checkpoint_id: str) -> None: ...
async def delete(self, session_id: str) -> None: ...
class AgentState:
def __init__(self, target: SupportsAgentRun, *, session_store: SessionStore | None = None) -> None: ...
async def get_target(self) -> SupportsAgentRun: ...
async def get_or_create_session(self, session_id: str) -> AgentSession: ...
async def set_session(self, session_id: str, session: AgentSession) -> None: ...
class WorkflowState:
def __init__(self, target: Workflow | SupportsBuild) -> None: ...
async def get_target(self) -> Workflow: ...
```
`WorkflowState` accepts direct `Workflow` instances, workflow factories, and builder-shaped objects with
`build() -> Workflow`. That structurally covers `WorkflowBuilder` and the builders in `agent_framework_orchestrations`
without making `agent-framework-hosting` depend on the orchestration package.
### Responses-only route
This sketch shows the intended Responses-only shape. The protocol package owns the Agent Framework run conversion helpers and
response-id minting details; the application owns FastAPI routing, auth, policy adjustment, and response construction.
```python
import os
from collections.abc import AsyncIterator
from agent_framework import Agent, ResponseStream
from agent_framework.openai import OpenAIChatClient
from agent_framework_hosting import AgentState # pyright: ignore[reportAttributeAccessIssue]
from agent_framework_hosting_responses import create_response_id, responses_from_run, responses_from_streaming_run, responses_session_id, responses_to_run # pyright: ignore[reportAttributeAccessIssue]
from fastapi import Body, FastAPI, Header, HTTPException
from fastapi.responses import JSONResponse, StreamingResponse
app = FastAPI()
agent = Agent(
client=OpenAIChatClient(),
name="Assistant",
instructions="Be concise and helpful.",
)
state = AgentState(agent)
@app.post("/responses")
async def responses(body: dict = Body(...), x_api_key: str | None = Header(default=None)) -> JSONResponse | StreamingResponse:
if x_api_key != os.environ["RESPONSES_API_KEY"]:
raise HTTPException(status_code=401, detail="bad api key")
# parse the request body into a set of AF objects
run = responses_to_run(body)
# get the candidate session id from the body
# can be a resp_* for previous_response_id or a conv_* for a conversation
candidate_session_id = responses_session_id(body)
# create a new response_id for this run
response_id = create_response_id()
# the developer can make any adjustments to the request, i.e.:
run["options"]["store"] = False
run["options"].pop("model", None)
# the options here are of the shape defined by the ChatClient/Agent
# load the session (or create a new one) - this is optional
# verify this caller owns candidate_session_id before loading it; API-key auth
# alone does not prove ownership of a caller-supplied resp_* or conv_* id
session_id = candidate_session_id or response_id
session = await state.get_or_create_session(session_id)
target = await state.get_target()
if run["stream"]:
stream = target.run(
run["messages"],
stream=True,
session=session,
options=run["options"],
)
async def stream_events() -> AsyncIterator[str]:
async for event in responses_from_streaming_run(
stream,
response_id=response_id,
session_id=candidate_session_id,
):
yield event
# agent.run may update the session during stream finalization, so store the post-run session explicitly
await state.set_session(response_id, session)
return StreamingResponse(stream_events(), media_type="text/event-stream")
result = await target.run(
run["messages"],
session=session,
options=run["options"],
)
# agent.run may update the session, so store the post-run session explicitly under the response id
# this might also be skipped, if the app chooses to respect `store=False` policy
await state.set_session(response_id, session)
return JSONResponse(responses_from_run(result, response_id=response_id, session_id=candidate_session_id))
```
### Responses-only Django class-based view
The same helper surface can be used without FastAPI. A Django app owns URL routing, CSRF/auth policy, request parsing,
and `JsonResponse` construction. In a real Django project this would live in the app's normal view module (for example
`assistant/views.py`) and be routed from that app's `urls.py`; Django discovers it through its standard project/app
layout, not through Agent Framework. This sketch shows the non-streaming path only; the streaming branch is the same
state/finalization pattern shown in the FastAPI sketch and is omitted here to avoid duplicating it.
```python
import json
import os
from agent_framework import Agent
from agent_framework.openai import OpenAIChatClient
from agent_framework_hosting import AgentState # pyright: ignore[reportAttributeAccessIssue]
from agent_framework_hosting_responses import create_response_id, responses_from_run, responses_session_id, responses_to_run # pyright: ignore[reportAttributeAccessIssue]
from django.http import HttpRequest, HttpResponseBadRequest, HttpResponseForbidden, JsonResponse
from django.views import View
agent = Agent(
client=OpenAIChatClient(),
name="Assistant",
instructions="Be concise and helpful.",
)
state = AgentState(agent)
class ResponsesView(View):
async def post(self, request: HttpRequest) -> JsonResponse:
if request.headers.get("x-api-key") != os.environ["RESPONSES_API_KEY"]:
return HttpResponseForbidden("bad api key")
try:
body = json.loads(request.body)
except json.JSONDecodeError:
return HttpResponseBadRequest("invalid json")
run = responses_to_run(body)
candidate_session_id = responses_session_id(body)
response_id = create_response_id()
options = run["options"]
# verify this caller owns candidate_session_id before loading it; API-key auth
# alone does not prove ownership of a caller-supplied resp_* or conv_* id
session_id = candidate_session_id or response_id
session = await state.get_or_create_session(session_id)
target = await state.get_target()
result = await target.run(
run["messages"],
session=session,
options=options,
)
await state.set_session(response_id, session)
return JsonResponse(responses_from_run(result, response_id=response_id, session_id=candidate_session_id))
```
@@ -1,132 +0,0 @@
---
status: proposed
contact: eavanvalkenburg
date: 2026-06-11
deciders: eavanvalkenburg
---
# Hosting linking and multicast enhancements
## Context and Problem Statement
[ADR-0027](0027-hosting-channels.md) defines the minimal v1 hosting core: originating-channel responses, explicit `ChannelSession.isolation_key`, and no host-level identity linking, push, multicast, background delivery, or durable runners.
This ADR tracks the richer cross-channel behaviors that were removed from v1. These enhancements are **follow-up work** and are **not prerequisites** for shipping, using, or stabilizing the v1 host/channel core.
## Decision Drivers
- Cross-channel continuity must not create accidental cross-user, cross-tenant, or cross-channel data leaks.
- Non-originating delivery must be observable, idempotent, retryable, and supportable.
- Protocol payloads must remain channel-native while still being safe to persist and replay.
- App authors need opt-in policy controls, not hidden defaults.
- The enhancement stack should layer on top of the v1 host without reshaping the minimal channel contract.
## Enhancement Areas
The follow-up design should cover these capabilities together because they share identity, storage, delivery, and replay concerns:
- **Cross-channel identity linking** — a user can connect multiple `ChannelIdentity` values to one channel-neutral `isolation_key`.
- **Authorization and allowlist policy** — channels or hosts can require verified identity, allow specific native identities or claims, and deny unknown callers.
- **Non-originating response delivery** — a run can respond somewhere other than the request's originating protocol when explicitly configured.
- **Active-channel routing** — delivery can target the most recently observed linked channel for an `isolation_key`.
- **Multicast / all-linked delivery** — delivery can fan out to every linked channel or a selected set.
- **Background runs and continuation tokens** — long-running requests can return immediately and complete later, with a polling/status fallback.
- **Durable delivery runners** — delivery work can survive process restarts and support dead-letter handling.
- **Retry and replay semantics** — delivery attempts are bounded, deduplicated, and safe to replay.
- **Payload serialization** — channel-specific payloads can be persisted, redacted, versioned, and reconstructed without losing protocol fidelity.
Candidate API names from the broader design (`IdentityLinker`, `IdentityAllowlist`, `AuthPolicy`, `ResponseTarget`, `ChannelPush`, `ChannelPushCodec`, `DurableTaskRunner`, `InProcessTaskRunner`, `RetryPolicy`, `LinkPolicy`) remain design vocabulary for this ADR. They are not approved v1 APIs.
## Considered Options
### Option A — Leave all behavior to applications
Applications implement linking, authorization, push, retry, and serialization independently.
- Good: the hosting core stays very small.
- Neutral: advanced apps can still build what they need.
- Bad: every app must solve the same security and delivery problems, likely inconsistently.
### Option B — Add the full enhancement stack to v1
The first host release includes linking, authorization, active channel, multicast, background runs, durable runners, and codecs.
- Good: the original cross-channel experience is available immediately.
- Neutral: samples can demonstrate rich end-to-end flows.
- Bad: v1 becomes security-sensitive, storage-heavy, and harder to stabilize.
### Option C — Layer opt-in enhancement packages after v1
Ship the minimal host first, then add linking, authorization, and delivery packages behind explicit configuration.
- Good: v1 remains simple while leaving room for a reviewed, supportable enhancement stack.
- Neutral: apps that need advanced delivery wait for follow-up packages.
- Bad: the first release does not satisfy proactive or all-linked scenarios.
### Option D — Build only platform-specific integrations
Implement linking and proactive delivery separately in Telegram, Activity Protocol, Discord, and future channels.
- Good: each package can match its protocol exactly.
- Neutral: some shared abstractions may emerge later.
- Bad: cross-channel behavior becomes fragmented and hard to reason about.
## Decision Outcome
Proposed direction: **Option C — layered opt-in enhancement packages after v1**.
The minimal host remains the foundation. Follow-up packages may add linking, authorization, delivery, and durable execution, but must be explicitly enabled and must pass the validation gates below before becoming part of the public contract.
## Safety Requirements
### Threat model
The design must account for:
- spoofed channel-native identities,
- stolen or replayed link challenges,
- cross-tenant or cross-confidentiality data leakage,
- unsolicited proactive messages,
- malicious payloads persisted for replay,
- denial-of-service through fan-out or retry storms, and
- privacy leakage through logs, metrics, or support tooling.
Required mitigations include verified identity claims where available, signed and expiring link challenges, explicit user consent, per-channel capability checks, default-deny policy options, tenant partitioning, and uninformative denial messages on shared channels.
### Idempotency and replay
Exactly-once delivery is not a realistic guarantee. The design must provide:
- stable run, continuation, and delivery-attempt identifiers,
- channel-level idempotency keys where protocols support them,
- bounded retry with jitter and explicit terminal states,
- replay windows and expiration,
- duplicate suppression for persisted attempts, and
- clear semantics for "delivered", "accepted by platform", and "observed by user".
### Storage
Enhancement storage must stay distinct from v1 `AgentSession` history and workflow checkpoints unless an implementation deliberately backs them with the same physical store.
Stored data should be schema-versioned, minimized, encrypted or otherwise protected as appropriate, and partitioned by tenant/project. Link records, continuation records, active-channel state, delivery attempts, dead letters, and serialized payloads need independent TTL and deletion policies.
### Observability and support
The design must include structured logs, traces, and metrics for link attempts, authorization decisions, delivery scheduling, retries, replay, and dead-letter outcomes. Logs must avoid message content and sensitive identity claims by default. Operators need a way to inspect, revoke, replay, or purge stuck records safely.
## Validation Gates
Before these enhancements are accepted:
- A reviewed threat model covers identity linking, authorization, non-originating delivery, multicast, and replay.
- Cross-channel linking tests prove a verified identity can link two channels and that unlink/deny paths do not leak information.
- Authorization tests cover native-id allowlists, verified-claim allowlists, default-deny behavior, and misconfiguration failures.
- Delivery tests cover originating-only, specific-channel, active-channel, selected-channel, and all-linked routing.
- Background/continuation tests cover polling fallback, cancellation or expiration, process restart, retry, and dead-letter behavior.
- Codec tests prove payloads are versioned, redacted where needed, backward compatible, and rejected safely when unknown.
- Multicast tests prove fan-out is bounded, independently retried, and idempotent per destination.
- Observability tests or manual validation prove support operators can correlate a request to delivery attempts without exposing sensitive content.
## Relationship to ADR-0027
ADR-0027 remains valid without any of these enhancements. This ADR extends the hosting model only after the safety, storage, and support requirements above are satisfied.
@@ -1,641 +0,0 @@
---
status: proposed
contact: sergeymenshykh
date: 2026-06-23
deciders: sergeymenshykh
---
# Skills Over MCP: Implementation Design Options
This document explores design options for two SEP-2640 features. The decisions are not yet finalized.
- **Part 1: MCP Resource Template Skills** - skills described by a URI template with variables that must be resolved before loading.
- **Part 2: Direct Skill References** - reading `skill://` URIs referenced directly (e.g., in server instructions) without being listed in the index.
## Part 1: MCP Resource Template Skills
### Context and Problem Statement
The `AgentMcpSkillsSource` currently only supports `skill-md` type entries from `skill://index.json` (support for `archive` type is planned). The SEP-2640 specification also defines `mcp-resource-template` entries: **parameterized skill namespaces** described by a URI template with variables (e.g., `{product}`) that resolve to concrete `SKILL.md` URIs. Rather than materializing every skill in the index, the template's variables must be resolved before a skill can be loaded.
### Index Entry Format
```json
{
"$schema": "https://schemas.agentskills.io/discovery/0.2.0/schema.json",
"skills": [
{
"name": "git-workflow",
"type": "skill-md",
"description": "Follow this team's Git conventions for branching and commits",
"url": "skill://git-workflow/SKILL.md"
},
{
"type": "mcp-resource-template",
"description": "Per-product documentation skill",
"url": "skill://docs/{product}/SKILL.md"
}
]
}
```
Key differences from `skill-md`:
| Field | `skill-md` | `mcp-resource-template` |
|-------|------------|-------------------------|
| `name` | Required (the skill name) | **Omitted** (represents many skills) |
| `type` | `"skill-md"` | `"mcp-resource-template"` |
| `url` | Concrete URI to `SKILL.md` | URI template with variables |
| `description` | Describes the skill | Describes the addressable skill space |
### Use Cases
Template skills address two scenarios where listing concrete skills is impractical:
- **Large skill catalogs** - too many skills to enumerate every entry in the index.
- **Dynamically generated skills** - skill content generated on the fly from parameters, so the set of valid skills is not known at index-creation time.
### How Template Skills Are Consumed
Per SEP-2640, the consumption flow relies on the MCP `completion/complete` method:
1. **Server registers a resource template** - The MCP server registers the same `url` value (e.g., `skill://docs/{product}/SKILL.md`) as an MCP [resource template](https://modelcontextprotocol.io/specification/2025-11-25/server/resources#resource-templates), wiring template variables to the [completion API](https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/completion).
2. **Host reads `skill://index.json`** - Discovers the template entry with `type: "mcp-resource-template"`.
3. **Host surfaces template in UI** - Presents the template as an interactive discovery point where the user fills in variables.
4. **Host calls `completion/complete`** - For each template variable (e.g., `{product}`), the host calls the MCP completion API to get possible values from the server:
```json
{
"method": "completion/complete",
"params": {
"ref": {
"type": "ref/resource",
"uri": "skill://docs/{product}/SKILL.md"
},
"argument": {
"name": "product",
"value": ""
}
}
}
```
The server responds with possible completions:
```json
{
"completion": {
"values": ["widgets", "billing", "auth", "payments"],
"hasMore": false,
"total": 4
}
}
```
5. **User selects a value** - The user picks a value (e.g., `"billing"`) from the list.
6. **Host resolves the URI** - The template `skill://docs/{product}/SKILL.md` becomes the concrete URI `skill://docs/billing/SKILL.md`.
7. **Host reads the resolved skill** - Calls `resources/read` with the concrete URI and proceeds as with any `skill-md` skill.
### Potential Implementation Options
### Option 1: Callback on `AgentMcpSkillsSource` for Variable Value Selection
Add a callback to `AgentMcpSkillsSource` (or its options) that is invoked for each `mcp-resource-template` entry to let the caller select variable values.
**Flow:**
1. `AgentMcpSkillsSource.GetSkillsAsync()` reads `skill://index.json`
2. For each entry with `type: "mcp-resource-template"`:
- Parse the URI template to extract variable names (e.g., `{product}`)
- Call the MCP `completion/complete` API to get possible values for each variable
- Invoke the caller-provided callback with the variable name, description, and possible values
- The callback returns a selected value and a `bool` indicating whether to include the skill
3. Resolve the URI template with the selected values
4. Create an `AgentMcpSkill` from the resolved URI and add it to the skills list
**API sketch:**
```csharp
public delegate Task<(string? SelectedValue, bool IncludeSkill)> McpTemplateVariableSelector(
string templateDescription,
string variableName,
IReadOnlyList<string> possibleValues,
CancellationToken cancellationToken);
// Usage via builder:
var provider = new AgentSkillsProviderBuilder()
.UseMcpSkills(mcpClient, options => {
options.TemplateVariableSelector = async (description, variable, values, ct) =>
{
// Present to user, return selection
var selected = PromptUser(variable, values);
return (selected, IncludeSkill: selected is not null);
};
})
.Build();
```
**Pros:**
- Simple implementation
- Easy to understand and use
**Cons:**
- Cannot be used in server-side scenarios where there is no interactive user at skill-discovery time
- Does not integrate with the agent's conversational flow
---
### Option 2: Integrate into Agent Conversation via `ChatClientAgent` Decorator
Model the template variable resolution as a request/response interaction within the agent's conversational loop.
**Flow:**
1. A `DelegatingAIAgent` decorator (e.g., `McpTemplateSkillResolutionAgent`) intercepts `RunAsync`/`RunStreamingAsync` calls and checks whether the inner agent has an `AgentSkillsProvider` with an `AgentMcpSkillsSource` containing unresolved template entries. The check is performed via `GetService<AgentMcpSkillsSource>()` on the `AgentSkillsProvider`, which delegates to a `GetService` method on the `AgentSkillsSource` base class.
2. The decorator calls an internal member on `AgentMcpSkillsSource` to get the list of `mcp-resource-template` entries from the index. The `AgentMcpSkillsSource` needs to be extended with an internal member that exposes unresolved template entries separately from concrete skills.
3. For each template entry, the decorator calls an internal member on `AgentMcpSkillsSource` to retrieve possible values for the template's variables via the MCP `completion/complete` API.
4. For each variable needing resolution, the decorator returns an `McpResourceTemplateValueRequestContent` (inherits from MEAI's `InputRequestContent`) in the agent response - bypassing the call to the inner agent. The content carries the template description, variable name, and possible values.
5. The user app receives the response, identifies the `McpResourceTemplateValueRequestContent` content type, and displays UI to the user showing the variable name and possible values, or forwards it further downstream if the user app is a service.
6. The user selects a value, and the user app calls the agent again with a corresponding `McpResourceTemplateValueResponseContent` (inherits from MEAI's `InputResponseContent`) containing the selected value. The `RequestId` property (inherited from the base classes) correlates the response with the original request.
7. The decorator identifies the response content and provides the resolved values to `AgentMcpSkillsSource` so it can use them when constructing concrete skills.
8. Having resolved all template variables, the decorator calls `RunAsync`/`RunStreamingAsync` on the inner agent.
9. The inner agent invokes the `AgentSkillsProvider`, which calls `AgentMcpSkillsSource.GetSkillsAsync()`. The source now has all resolved variable values and constructs concrete `AgentMcpSkill` instances from the resolved URIs, so it can provide the skill content if requested by the model.
**API sketch:**
```csharp
// New content types inheriting from MEAI's InputRequestContent/InputResponseContent:
public sealed class McpResourceTemplateValueRequestContent : InputRequestContent
{
public string TemplateDescription { get; }
public string VariableName { get; }
public IReadOnlyList<string> PossibleValues { get; }
public string TemplateUrl { get; }
}
public sealed class McpResourceTemplateValueResponseContent : InputResponseContent
{
public string SelectedValue { get; }
public string TemplateUrl { get; }
}
// Decorator usage:
var provider = new AgentSkillsProviderBuilder()
.UseMcpSkills(mcpClient)
.Build();
AIAgent agent = new ChatClientAgent(chatClient, new ChatClientAgentOptions
{
AIContextProviders = [provider],
});
agent = new McpTemplateSkillResolutionAgent(agent);
```
**Pros:**
- Works in server-side scenarios
- Fits the existing `DelegatingAIAgent` decorator pattern
- Can be composed with other decorators (tool approval, etc.)
**Cons:**
- Complex implementation
- Requires user app awareness of the new content types
- Users need to know that an additional decorator is required for handling MCP template skills, in addition to registering the MCP skills source
- Resolved template variable values must be persisted across conversation turns so the decorator does not re-prompt on subsequent agent runs within the same session
**Note:** This writeup is high-level and may miss details that could change the design. A POC would be needed to validate the approach.
### Open Questions
1. **Completion API limit** - The MCP completion API returns at most 100 values per request and provides no offset/cursor mechanism for enumeration. If a variable has more than 100 possible values, it's unclear how to retrieve the rest - the API only supports prefix-based filtering (typeahead), not bulk pagination.
2. **Multi-variable templates** - A template like `skill://{org}/{product}/SKILL.md` has multiple variables. Should they be resolved sequentially (org first, then product - since product values may depend on org) or presented together?
3. **Caching** - Should resolved template values be saved in the `AgentSession` so the user isn't re-prompted on every agent run? How should they be persisted between sessions?
---
## Part 2: Direct Skill References
This part covers how to let the model read `skill://` URIs referenced directly (e.g., in an MCP server's `instructions`, in a resource, or in another skill's content) without being listed in `skill://index.json`.
### How MCP Skills and Relative Links Work Today
The `AgentMcpSkillsSource` discovers skills by reading the well-known `skill://index.json` resource from the MCP server:
```json
{
"$schema": "https://schemas.agentskills.io/discovery/0.2.0/schema.json",
"skills": [
{
"name": "unit-converter",
"type": "skill-md",
"description": "Convert between common units.",
"url": "skill://unit-converter/SKILL.md"
},
{
"name": "currency-converter",
"type": "skill-md",
"description": "Convert between world currencies using live rates.",
"url": "skill://currency-converter/SKILL.md"
}
]
}
```
For each `skill-md` entry it creates an `AgentMcpSkill` instance - frontmatter (name/description) comes straight from the entry. The `AgentSkillsProvider` lists the discovered skills in the model's context (name + description):
```xml
<available_skills>
<skill>
<name>unit-converter</name>
<description>Convert between common units.</description>
</skill>
<skill>
<name>currency-converter</name>
<description>Convert between world currencies using live rates.</description>
</skill>
</available_skills>
```
It also provides functions to the model so it can load a skill and access its resources:
```csharp
// Loads the full content of a specific skill.
load_skill(string skillName)
// Reads a resource associated with a skill (references, assets, dynamic data).
read_skill_resource(string skillName, string resourceName)
```
The model calls `load_skill("unit-converter")` and receives the skill content:
```markdown
---
name: unit-converter
description: Convert between common units.
---
## Usage
For the full conversion table, see references/units-table.md.
```
The skill body references `references/units-table.md` by relative path. The model calls `read_skill_resource("unit-converter", "references/units-table.md")` and receives the resource content:
```markdown
# Unit Conversion Table
| From | To | Factor |
| miles | km | 1.60934 |
| kg | lbs | 2.20462 |
```
### Direct Reference Examples
A `skill://` URI can appear in any of these locations:
**Server instructions** - the MCP server advertises a skill the model should load:
```text
Follow our coding standards. Load skill://code-standards/SKILL.md for details.
```
**A skill body** - a skill's `SKILL.md` links to a sibling resource:
```markdown
---
name: code-standards
description: Coding standards and conventions.
---
## Naming
Follow the naming rules in skill://code-standards/references/naming.md.
```
**A resource** - the linked resource holds the actual content:
```markdown
# Naming Rules
- Use PascalCase for public members and type names.
- Use camelCase for locals and parameters.
- Prefix interfaces with `I` (e.g. `ISkillReader`).
- Suffix async methods with `Async`.
For examples, see skill://code-standards/references/naming-examples.md.
```
How can the model access content by direct reference?
### Function for Reading Direct Skill References
### Option 1: Extend existing `load_skill` and `read_skill_resource` functions
```csharp
// Added optional 'origin' and a direct skill:// URI is passed in 'skillName'.
load_skill(string skillName, string? origin = null)
// Added optional 'origin', made 'skillName' optional, and a direct skill:// URI is passed in 'resourceName'.
read_skill_resource(string resourceName, string? skillName = null, string? origin = null)
```
The optional `origin` identifies the source/MCP server that should handle the direct URI.
| Case | Call |
|------|------|
| Load skill | `load_skill("commit-guidelines")` |
| Relative resource | `read_skill_resource("commit-guidelines", "examples/COMMIT_EXAMPLES.md")` |
| `skill://` link (skill) | `load_skill(skillName: "skill://commit-guidelines/SKILL.md", origin: "DirectRefServer")` |
| `skill://` link (resource) | `read_skill_resource(resourceName: "skill://commit-guidelines/examples/COMMIT_EXAMPLES.md", origin: "DirectRefServer")` |
**Pros:**
- No new functions added: existing tool surface stays at two functions.
**Cons:**
- Unreliable on some models (gpt-4o, gpt-4.1-mini): it often omits `origin` when it should not or calls the wrong function.
- Optional parameters create silent ambiguity - the model can pass `origin` for non-MCP skills or omit it for `skill://` URIs.
### Option 2 (Proposed): Add a dedicated `read_skill_uri` function alongside existing ones
```csharp
// Existing functions stay unchanged.
load_skill(string skillName)
read_skill_resource(string skillName, string resourceName)
// New function added alongside: reads content by direct skill:// URI.
read_skill_uri(string uri, string origin)
```
| Case | Call |
|------|------|
| Load skill | `load_skill("commit-guidelines")` |
| Relative resource | `read_skill_resource("commit-guidelines", "examples/COMMIT_EXAMPLES.md")` |
| `skill://` link (skill) | `read_skill_uri(uri: "skill://commit-guidelines/SKILL.md", origin:"DirectRefServer")` |
| `skill://` link (resource) | `read_skill_uri(uri: "skill://commit-guidelines/examples/COMMIT_EXAMPLES.md", origin: "DirectRefServer")` |
**Pros:**
- Purely additive - no changes to existing functions needed; `read_skill_uri` can be deferred and added later when direct `skill://` reference support is needed.
- Granular approval: each function can have its own approval gate (like the existing `ScriptApproval` for `run_skill_script`), making per-operation approval for skill loading, resource reading, and direct URI access straightforward to add.
- Both `uri` and `origin` are required - no silent misuse through optional parameters.
- Clean split: `load_skill`/`read_skill_resource` for named skills, `read_skill_uri` for `skill://` links - no parameter ambiguity.
**Cons:**
- Three read functions (`load_skill`, `read_skill_resource`, `read_skill_uri`), not counting `run_skill_script`: larger tool surface than a single-function design.
### Option 3: Collapse `load_skill` and `read_skill_resource` into a single `read_resource` function
```csharp
// Single entrypoint for all skill content. 'uri' is required; 'origin' is optional.
read_resource(string uri, string? origin = null)
```
- `uri` - what to read: a skill name, a relative resource path, or a `skill://` link.
- `origin` - determines how `uri` is interpreted:
- **omitted** → load skill by name (`uri` is the skill name).
- **skill name** → read a relative resource (`uri` is the path within that skill).
- **server name** → read content by the `skill://` link (`uri` is handled by the source identified by the `[Origin: X]` marker).
Dispatch is ordered: null `origin` routes to Case 1; if `origin` names a known skill, routes to Case 2; otherwise tries to find an `ISkillUriReader` whose `CanRead` returns true for `origin` (Case 3).
| Case | Call |
|------|------|
| Load skill | `read_resource(uri: "commit-guidelines")` |
| Relative resource | `read_resource(uri: "examples/COMMIT_EXAMPLES.md", origin: "commit-guidelines")` |
| `skill://` link (skill) | `read_resource(uri: "skill://commit-guidelines/SKILL.md", origin: "DirectRefServer")` |
| `skill://` link (resource) | `read_resource(uri: "skill://commit-guidelines/examples/COMMIT_EXAMPLES.md", origin: "DirectRefServer")` |
**Pros:**
- Minimal tool surface: one read function instead of two or three (not counting `run_skill_script`) reduces token usage and gives the model fewer choices.
**Cons:**
- No per-operation approval: all cases (skill loading, resource reading, direct URI access) share one function, so approval cannot be scoped to individual operations.
- Unreliable on gpt-4.1-mini: omits `origin` when reading `skill://` links, passes skill name as `origin` when loading a plain skill (should be omitted), and hallucinates resource names (e.g. `API_SPECIFICATION.md`) that do not exist.
---
### Origin Marker
A `skill://` URI does not carry an origin, but the model needs to provide one when reading it. The `origin` is what routes the read call to the source that can handle the URI - the provider uses it to pick the matching source. Since the URI itself carries no such hint, the MCP source injects an `[Origin: ...]` marker wherever a `skill://` URI appears, so the model can read it back and pass it as the `origin` argument.
The marker is only added when the content actually contains `skill://` references. If a piece of content (server instructions, a skill body, or a resource) has no `skill://` URIs, there is nothing for the model to read back, so no marker is injected.
Into **server instructions**, which may mention `skill://` URIs directly:
```
[Origin: code-standards-server]
Follow our coding standards. Load skill://code-standards/SKILL.md for details.
```
Into **skill bodies**, since a `SKILL.md` may reference other `skill://` URIs (a resource file or a related skill):
```
[Origin: code-standards-server]
# Code Standards
For naming conventions, load skill://code-standards/references/naming.md.
```
Into **skill resources**, since a resource may itself reference further `skill://` URIs:
```
[Origin: code-standards-server]
# Naming Rules
- Use PascalCase for public members and type names.
- Use camelCase for locals and parameters.
For examples, see skill://code-standards/references/naming-examples.md.
```
---
### Read-by-URI Capability: Interface vs Base Class Virtual Methods
Now let's look at how an `AgentSkillsSource` can opt in to reading `skill://` URIs and signal that capability to the provider.
### Option 1: New `ISkillUriReader` interface
```csharp
public interface ISkillUriReader
{
// Returns true if this reader can handle the given skill:// URI from the given origin.
bool CanRead(string uri, string origin);
// Reads and returns the content for the given skill:// URI.
Task<object?> ReadByUriAsync(string uri, string origin, CancellationToken cancellationToken = default);
}
```
Sources that support direct `skill://` URI reads - such as `AgentMcpSkillsSource` - implement this interface to opt in.
The provider discovers readers via a service locator and dispatches to the first that can handle the URI:
```csharp
// Discover all registered readers.
var readers = source.GetService<IEnumerable<ISkillUriReader>>();
// Pick the first reader that can handle the URI.
var reader = readers.FirstOrDefault(r => r.CanRead(uri, origin))
?? throw new InvalidOperationException($"No reader can handle URI '{uri}' from origin '{origin}'.");
// Delegate the read to it.
return await reader.ReadByUriAsync(uri, origin, cancellationToken);
```
The provider may treat a source implementing `ISkillUriReader` as the signal to advertise `read_skill_uri`: if at least one registered source implements the interface, the function is exposed to the model; otherwise it is not.
### Option 2 (Proposed): Virtual methods on `AgentSkillsSource` base class
```csharp
public abstract class AgentSkillsSource
{
// New members for reading by URI.
// Whether this source can read by URI; drives whether read_skill_uri is advertised. Off by default.
public virtual bool SupportsReadByUri => false;
// Returns true if this source can handle the given skill:// URI from the given origin.
public virtual bool CanReadByUri(string uri, string origin) => false;
// Reads and returns the content for the given skill:// URI.
public virtual Task<object?> ReadByUriAsync(string uri, string origin, CancellationToken cancellationToken = default)
=> Task.FromResult<object?>(null);
// Existing member.
public abstract Task<IList<AgentSkills>> GetSkillsAsync(CancellationToken cancellationToken = default);
}
```
Sources opt in by overriding, and the provider calls them directly:
```csharp
// AgentMcpSkillsSource opts in by overriding the virtuals.
public override bool SupportsReadByUri => true;
// Handles the URI when its origin matches this source's MCP server.
public override bool CanReadByUri(string uri, string origin)
=> string.Equals(origin, this.Origin, StringComparison.OrdinalIgnoreCase);
// Reads content by skill:// URI from the MCP server.
public override Task<string?> ReadByUriAsync(string uri, string origin, CancellationToken cancellationToken)
=> /* resolve uri via the MCP server identified by origin */;
```
All sources inherit the methods, so there is no type signal - `SupportsReadByUri` fills that role. The function is advertised when any registered source returns `true`.
### Comparison
| Aspect | Option 1: Interface | Option 2: Base class virtual methods |
|--------|---------------------|--------------------------------------|
| Discovery | Service locator | Direct call on source |
| Advertising signal | Interface implementation | `SupportsReadByUri` flag |
| Adding new members | Breaking change | Non-breaking |
| Complexity | Higher | Lower |
---
### Include MCP Server Instructions Into Agent Instructions
MCP server instructions may contain the `skill://` references the model needs, so we want to surface them in the agent's instructions. But they can also carry system prompts or behavioral directives irrelevant to the agent, polluting context - so inclusion is **opt-in** via the `IncludeServerInstructions` option:
```csharp
public sealed class AgentMcpSkillsSourceOptions
{
// When true, the MCP server's instructions are injected into the agent instructions. Off by default.
public bool IncludeServerInstructions { get; set; }
}
builder.UseMcpSkills(mcpClient, options => options.IncludeServerInstructions = true);
```
When enabled, the instructions travel alongside the discovered skills on `AgentSkillsResult`:
```csharp
public class AgentSkillsResult
{
// The skills discovered from the source.
public IList<AgentSkill> Skills { get; }
// The MCP server instructions, when IncludeServerInstructions is enabled; otherwise null.
public string? Instructions { get; }
}
```
The `AgentSkillsProvider` then appends them to its own skill-usage guidance when building the agent's instructions:
```csharp
var result = await source.GetSkillsAsync(cancellationToken);
var instructions = DefaultSkillsInstructionPrompt;
if (!string.IsNullOrWhiteSpace(result.Instructions))
{
// Combine the provider's skill-usage guidance with the server instructions.
instructions += Environment.NewLine + result.Instructions;
}
```
### Enabling Direct Skill References
Following direct `skill://` references is **disabled by default** and activated via an option. When enabled, the provider advertises the read function to the model, and the source injects the `[Origin: ...]` marker into all content provided by the MCP server that contains `skill://` references. When disabled, no function is advertised and no marker is injected.
```csharp
public sealed class AgentMcpSkillsSourceOptions
{
public bool EnableDirectReferences { get; set; }
}
builder.UseMcpSkills(mcpClient, options => options.EnableDirectReferences = true);
```
## Decision Outcome
### Template Variable Resolution: Callback vs Decorator (Part 1)
**Postponed.** Deferring this decision until:
- We have a concrete list of scenarios that require template variable resolution.
- The skills-over-MCP spec is released (it is still a draft, so the design may change).
- There is a strong signal of demand from users or the ecosystem.
### Function for Reading Direct Skill References (Part 2)
**Postponed.** Leaning toward **Option 2 - dedicated `read_skill_uri` function alongside existing ones** (purely additive, and each function can have its own approval gate for granular per-operation approval), but deferring the decision until:
- The skills-over-MCP spec is released (it is still a draft, so the design may change).
- There is a strong signal of demand from users or the ecosystem.
### Read-by-URI Capability: Interface vs Base Class (Part 2)
**Postponed.** Leaning toward **Option 2 - virtual methods on `AgentSkillsSource`** (non-breaking, lower complexity, and a natural fit with the existing base class hierarchy), but deferring the decision until:
- The skills-over-MCP spec is released (it is still a draft, so the design may change).
- There is a strong signal of demand from users or the ecosystem.
The method naming (`SupportsReadByUri`, `CanReadByUri`, `ReadByUriAsync`) should also be abstracted a little more before adoption, so the same members can be reused when a similar direct-reference concept is needed for other skill types (e.g. file skills).
## References
- [SEP-2640: Skills Extension](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2640) - Draft proposal
- [SEP-2640 Implementation Guidelines: Model-Driven Resource Loading](https://github.com/modelcontextprotocol/experimental-ext-skills/blob/main/docs/sep-draft-skills-extension.md#hosts-model-driven-resource-loading)
- [MCP Completion API](https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/completion) - Used for template variable resolution
- [MCP Resource Templates](https://modelcontextprotocol.io/specification/2025-11-25/server/resources#resource-templates)
- [Skills Over MCP Working Group](https://github.com/modelcontextprotocol/experimental-ext-skills)
- [Open Question #4: Multi-server skill dependencies](https://github.com/modelcontextprotocol/experimental-ext-skills/issues/39)
- [Anthropic Agent Skills - Overview](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview) - Prior art: single skill entrypoint + generic file reads
- [Anthropic Agent Skills in the SDK](https://code.claude.com/docs/en/agent-sdk/skills) - The `Skill` tool exposed to the model
@@ -1,356 +0,0 @@
---
status: accepted
contact: eavanvalkenburg
date: 2026-06-19
deciders: eavanvalkenburg, moonbox3, TaoChenOSU, chetantoshnival
consulted: westey-m
informed:
---
# Python identity lifetimes for sessions, tasks, and continuation
## Context and Problem Statement
Python `AgentSession` currently carries a local `session_id`, an optional opaque service continuation
`service_session_id`, and provider state. `service_session_id` is any service-owned value that lets that service continue
a conversation, session, or thread; chat clients happen to map it through the abstract `conversation_id` ChatOption, but
other agent types can use it differently. It is not a generic correlation field, and generic correlation should not
require parsing or understanding that opaque service-owned value.
The related issues mix values with different lifetimes:
- **Session / conversation identity**: values that group a multi-turn interaction. Examples: A2A `context_id`, OpenAI
Responses `conversation` (`conv_*`) or response-chain continuation (`previous_response_id`).
- **Task identity**: values that identify a protocol task and may affect future protocol calls. Example: A2A `task_id`.
- **Message / response identity**: values that identify an output message or response. Examples: A2A `message_id` /
`artifact_id`, OpenAI Responses response id (`resp_*`).
- **Continuation token**: a framework resume payload for in-progress work. It may contain the same underlying value as a
protocol id, such as A2A `task_id`, but it only exists when there is an unfinished operation to resume.
These values should not automatically live in the same object just because they all help "continue" something. A value
belongs in `AgentSession` only when it is needed to continue future calls across turns. A value that identifies one
result belongs on the response or message. A value that resumes in-progress work belongs in a `ContinuationToken`.
An `AgentSession` created for one agent is not expected to be guaranteed to work against another agent. When a session is
used with an incompatible agent, protocol, or service, the framework should still help users understand what is wrong as
early as possible, preferably before calling out to the remote service.
For #4673, native conversation identity propagation should be based on `AgentSession` where the value is durable session
state. For #4893, A2A `context_id` and `task_id` need a coherent Agent Framework mapping.
AG-UI is out of scope for the decision. Its `thread_id` already maps to `AgentSession.session_id` in the normal wrapper
path, and `run_id` is wrapper-owned event correlation. If AG-UI run correlation needs framework telemetry integration
later, that should be handled as a run-context/telemetry design, not as session identity.
### Concrete gap example
At the protocol level, the durable continuation payload shapes are different:
```json
// A2A: future calls may need multiple durable protocol fields
{
"context_id": "ctx_123",
"task_id": "task_789",
"task_state": "input_required"
}
```
```json
// OpenAI Responses: future calls usually need one continuation value
{
"previous_response_id": "resp_abc123"
}
```
The gap is that A2A continuation state is multi-field while OpenAI continuation is
typically single-field.
## Current implementation notes
- A2A currently has `A2AAgentSession`, but `A2AAgent.create_session(...)` does not automatically return it.
- A2A currently mirrors `context_id` into `service_session_id`; that is current behavior, not necessarily the target
abstraction.
- A2A `task_id` is not just cosmetic correlation. It is used for `task_id` when a task is `INPUT_REQUIRED`, for
`reference_task_ids` when refining a previous task, and inside `A2AContinuationToken` for in-progress tasks.
- `RawAgent._prepare_run_context(...)` currently forwards `active_session.service_session_id` as chat `conversation_id`,
so any non-string or formatted value affects existing chat-client paths.
- `OpenAIChatClient` maps chat options `conversation_id` to the Responses API as `previous_response_id` for `resp_*`,
`conversation` for `conv_*`, and defaults unrecognized strings to `previous_response_id`. When `store` is not `False`,
it returns `response.conversation.id` when available, otherwise `response.id`, as the next service continuation value.
- For Responses API, the response id (`resp_*`) is also the response/message identity surfaced as
`ChatResponse.response_id`; when used for continuation on the next request, it becomes the `previous_response_id`
value.
- Python A2A has not been released as stable yet, so its session factory or session shape can still be adjusted before
release.
## Decision Drivers
- Preserve `AgentSession.session_id` as the local/client conversation identity.
- Preserve `AgentSession.service_session_id` as an opaque service-owned continuation handle.
- Keep `AgentSession` for durable state needed across turns, not per-run bookkeeping.
- Store values needed by future calls in durable session state; keep values that only resume in-progress work in
`ContinuationToken`.
- Fix the current confusion where session, task, response, and continuation values can be treated as interchangeable
because they all participate in "continuing" something.
- Make the implementation following this ADR preserve the lifetime split clearly: future-call state, in-progress resume
tokens, response/message ids, and protocol event correlation must not be silently mixed.
- Expose durable continuation state in a typed way when future calls depend on it.
- Let telemetry correlate runs without parsing opaque service continuation handles.
- Reuse existing run/context surfaces before introducing a new identity abstraction.
- Keep MCP and other remote tool boundaries safe: framework identity must not be forwarded to remote tools unless an
existing explicit opt-in mechanism says so.
- Keep existing `AgentSession.to_dict()` / `from_dict()` migration and compatibility straightforward.
- Stay close to .NET where there is already behavior to match, especially A2A's `ContextId`, `TaskId`, and `TaskState`.
- Detect incompatible session identity shapes as early as practical, preferably before a remote service call.
## Non-goals
- Do not design a provider-agnostic conversation creation API here. That is tracked separately in #6622.
- Do not make `service_session_id` a generic telemetry or run-correlation field.
- Do not introduce a new identity object if existing run/context objects can carry the selected per-run correlation value.
- Do not make a session from one agent guaranteed to work against another agent.
- Do not optimize the public `agent.run(...)` API for protocol-wrapper internals.
## Remaining question: durable shape for additional continuation state
- Option A: Use protocol-specific `AgentSession` subclasses.
- Option B: Extend `service_session_id` with richer service-owned values.
- Option C: Add a dedicated dict for additional session details.
- Option D: Store additional durable state inside `AgentSession.state`.
### Option A: Use protocol-specific `AgentSession` subclasses
Each protocol or agent type that needs additional durable state keeps a specialized `AgentSession` subclass. For A2A,
that means keeping `A2AAgentSession` for A2A-specific durable state and changing `A2AAgent.create_session(...)` to return
that type.
Example:
```python
# First call returns a task that future A2A messages may need to reference.
session = await a2a_agent.create_session()
response = await a2a_agent.run(
message,
session=session,
)
# A2AAgent updates durable A2A protocol state from the returned task/status payload.
# The user does not set these manually.
assert isinstance(session, A2AAgentSession)
assert session.task_id is not None
assert session.task_state is not None
# Later call reuses the durable A2A session state. A2AAgent decides whether to send task_id
# for INPUT_REQUIRED or reference_task_ids for task refinement.
next_response = await a2a_agent.run(
next_message,
session=session,
)
```
- Good, because protocol-specific state stays in a protocol-specific type.
- Good, because it aligns with .NET A2A's `A2AAgentSession` shape.
- Good, because Python A2A can still make this pre-release session factory adjustment.
- Good, because `task_state` does not get promoted to a base `AgentSession` concept.
- Bad, because generic consumers cannot read protocol-specific state without knowing about the subclass or a helper API.
- Bad, because it depends on each subclass consistently setting shared session fields such as `service_session_id` where
those are part of the shared abstraction.
### Option B: Extend `service_session_id` with richer service-owned values
Keep the common `service_session_id` case as a plain string. When an agent/service needs more than one service-owned
continuation value, allow `service_session_id` to be a typed structured value, such as a `TypedDict`. The main session ID
used for `gen_ai.conversation.id` should still be extracted by the owning agent, not inferred by generic telemetry code.
Examples:
```python
simple_session = AgentSession(
service_session_id="resp_123",
)
structured_session = AgentSession(
service_session_id=A2AServiceSessionId(
context_id="ctx_123",
task_id="task_789",
task_state=TaskState.TASK_STATE_WORKING,
),
)
```
- Good, because the common case remains a plain string and stays simple.
- Good, because richer service-owned continuation state stays under the existing continuation property.
- Good, because a structured value can make framework-side validation possible before a value is sent back to a service.
- Good, because A2A can keep `context_id`, `task_id`, and `task_state` together as the service/protocol-owned continuation
value without adding A2A fields to base `AgentSession`.
- Neutral, because telemetry needs an agent-owned extractor to pick the `gen_ai.conversation.id` value from either a
string or structured `service_session_id`.
- Neutral, because Python A2A would need a pre-release adjustment to stop relying on `A2AAgentSession` for these fields.
- Bad, because changing the `service_session_id` type is a compatibility risk for users, providers, serialization, and
tests.
- Bad, because every path that sends `service_session_id` back to a service must consistently extract/adapt the
service-owned continuation component.
### Option C: Add a dedicated dict for additional session details
Keep `service_session_id` as the primary opaque service-owned continuation handle, and add a separate dictionary for
additional durable protocol/service values that need to travel with the session.
Example:
```python
session = AgentSession(
service_session_id="ctx_123",
session_details={
"task_id": "task_456",
"task_state": TaskState.TASK_STATE_WORKING,
},
)
```
- Good, because the main service continuation handle stays a plain `service_session_id` string.
- Good, because extra state has an explicit home and does not overload `service_session_id`.
- Good, because generic consumers can look in one documented place for additional session-scoped values.
- Neutral, because helper APIs can hide the raw dictionary access.
- Bad, because this still introduces string-keyed state unless the dict values are wrapped by typed helpers.
- Bad, because it adds another public session field that needs serialization, naming, and compatibility rules.
- Bad, because generic consumers still need to understand the shape or use helpers for the selected agent/session type.
### Option D: Store additional durable state inside `AgentSession.state`
Keep base `AgentSession` unchanged and store additional durable continuation/protocol state under namespaced keys in
`session.state`.
Example:
```python
session = AgentSession(session_id="ctx_123")
session.state["a2a"] = {
"task_id": "task_456",
"task_state": TaskState.TASK_STATE_WORKING,
}
```
- Good, because it avoids new public fields and avoids a subclass requirement.
- Good, because `AgentSession.state` already exists for provider/session state.
- Neutral, because helper APIs can hide the raw dictionary access.
- Bad, because stringly typed state is easier to corrupt and harder to validate.
- Bad, because generic consumers need helper APIs anyway; directly reading nested dictionaries is not a good abstraction.
- Bad, because users may accidentally overwrite or persist invalid protocol state.
## Decision
Chosen decision criteria for the future: **split identity by lifecycle**.
When a protocol emits an id/token, place it by answering "what lifecycle does this value serve?":
- **Future-call continuation state** -> durable session state. Examples: A2A `context_id` + `task_id` + `task_state`;
OpenAI Responses `previous_response_id`/`conversation`.
- **Single-result identity** -> response/message object only. Examples: OpenAI `resp_*`, A2A `message_id`,
A2A `artifact_id`.
- **Resume unfinished work** -> `ContinuationToken` only. Example: a token carrying in-progress task resume data.
- **Run-start-only request fields** -> run method arguments/options, not durable session state. Example: A2A
`reference_task_ids` for a specific follow-up/refinement request.
- **Per-run correlation/telemetry** -> protocol wrapper or run context, not `AgentSession`. Example: wrapper-managed
`run_id` used only for tracing/events.
Durable-state option decision: **Option B: Extend `service_session_id` with richer service-owned values**.
This does **not** add a new top-level identity abstraction; it keeps continuation identity under
`service_session_id` and keeps run correlation in existing run/telemetry context.
The immediate implementation gap is mainly in A2A mapping clarity, but the lifecycle split applies
consistently across providers.
To support telemetry, `BaseAgent` should expose a method that accepts an `AgentSession | None` and returns the value to
use for `gen_ai.conversation.id`. The default implementation should return `session.service_session_id` when it is a
string. Agents that use a structured `service_session_id`, such as `A2AAgent`, should override that method and return the
appropriate primary session/context value.
## Appendix: A2A `task_id` and `reference_task_ids` implementation check
The A2A protocol distinguishes a message's `task_id` from `reference_task_ids`:
- `task_id` associates the message with a specific task.
- `reference_task_ids` provides additional task context, for example when a new task refines or follows up on the result
of a previous task.
The protocol does not appear to prescribe that `task_id` and `reference_task_ids` are mutually exclusive. If both are
present, the natural reading is that the message is associated with one task while also referencing other tasks for
context. The serving agent decides how to interpret that context.
The Python implementation should check and likely adjust the current behavior:
- `task_id` should be updated by the current run when the remote A2A service returns a task/status payload.
- `task_id` should remain durable A2A session state when needed for future calls, for example when a task is
`INPUT_REQUIRED`.
- `reference_task_ids` should be a run parameter / caller intent for the current request, not implicit durable session
continuation state.
- A follow-up/refinement request should pass explicit `reference_task_ids` when it wants to reference previous tasks.
- If both session `task_id` and run `reference_task_ids` are present, the wrapper should preserve the protocol
distinction rather than treating one as a replacement for the other.
- If no `reference_task_ids` are supplied, the wrapper should not automatically infer them from the last session task
unless we deliberately keep that convenience for compatibility.
## Appendix: implementation notes for Option B
The exact names are implementation details, but the shape should be:
```python
class A2AServiceSessionId(TypedDict):
context_id: str
task_id: str | None
task_state: TaskState | None
class AgentSession:
def __init__(
self,
*,
session_id: str | None = None,
service_session_id: str | ServiceSessionId | None = None,
) -> None:
...
class BaseAgent:
def _get_otel_conversation_id(self, session: AgentSession | None) -> str | None:
service_session_id = session.service_session_id if session else None
return service_session_id if isinstance(service_session_id, str) else None
class A2AAgent(BaseAgent):
def _get_otel_conversation_id(self, session: AgentSession | None) -> str | None:
service_session_id = session.service_session_id if session else None
if isinstance(service_session_id, Mapping):
return service_session_id.get("context_id")
return service_session_id if isinstance(service_session_id, str) else None
class AgentTelemetryLayer:
def _trace_agent_invocation(...):
attributes = _get_span_attributes(
...,
thread_id=self._get_otel_conversation_id(session),
...,
)
```
This keeps the OpenTelemetry extraction decision with the agent that owns the service continuation shape. Generic OTel
code should not parse structured `service_session_id` values directly.
`AgentSession` must also be updated so `service_session_id` can store either the current string value or a structured
service-owned value. Serialization must preserve both shapes, and existing serialized sessions with string
`service_session_id` must continue to round-trip unchanged.
## More Information
Related work and issues:
- #4673: native conversation ID propagation.
- #4893: align A2A protocol concepts with Agent Framework session/continuation concepts.
- #2931: Foundry-specific conversation creation helper, split into a separate Python PR.
- #6622: broader provider-agnostic conversation creation API discussion requiring .NET sync.
- [ADR-0015](0015-agent-run-context.md): AgentRunContext for Agent Run.
- [ADR-0018](0018-agentthread-serialization.md): AgentSession serialization.
- [ADR-0026](0026-hosted-session-identity-context.md): hosted session identity context.
@@ -1,84 +0,0 @@
---
status: accepted
contact: rogerbarreto
date: 2026-06-29
deciders: rogerbarreto
consulted: []
informed: []
---
# Hosted platform context (user id + call id) for Foundry Hosting on AgentServer 2.0
Supersedes [ADR-0026](0026-hosted-session-identity-context.md).
## Context and Problem Statement
[ADR-0026](0026-hosted-session-identity-context.md) sourced the hosted-agent end-user identity from `ResponseContext.Isolation` (an `IsolationContext` typed `UserIsolationKey` / `ChatIsolationKey`), injected by the platform as the `x-agent-user-isolation-key` and `x-agent-chat-isolation-key` headers.
`Azure.AI.AgentServer.*` 2.0.0 (responses protocol `2.0.0`) removes that surface. `ResponseContext.Isolation` is gone; the platform now exposes `ResponseContext.PlatformContext` (a `PlatformContext` typed `UserIdKey` and `CallId`), populated from the `x-agent-user-id` and `x-agent-foundry-call-id` headers. The chat isolation key no longer exists, and a new per-request **call id** is introduced that first-party Foundry services (the toolbox proxy in particular) require on outbound calls to resolve the server-side-stored caller context. The hosting layer in `Microsoft.Agents.AI.Foundry.Hosting` had to migrate to this contract without changing the public shape that samples and providers depend on.
## Decision Drivers
- Track the breaking `Azure.AI.AgentServer.*` 2.0.0 surface (`PlatformContext` replacing `Isolation`) while keeping the same per-user partitioning guarantees from ADR-0026.
- Keep the change **internal**: existing hosted samples and `AIContextProvider`s must not need code changes. `session.GetHostedContext().UserId`, `HostedSessionIsolationKeyProvider`, and `AddFoundryResponses` stay source-compatible.
- Forward the new per-request call id verbatim on outbound calls to Foundry first-party services so per-user toolbox OAuth consent and other server-side caller-context lookups keep working.
- Remain resilient on protocol `1.0.0`: when only the legacy headers are present, `UserIdKey` still resolves and `CallId` is simply absent.
- Preserve the strict-resume tamper defense from ADR-0026 with identity now reduced to user only.
## Considered Options
For the identity source:
1. **Map `ResponseContext.PlatformContext.UserIdKey`** into the existing `HostedSessionContext` (user only), keeping ADR-0026's storage shape and read accessor.
2. Keep a `ChatId` slot on `HostedSessionContext` for backward source-compatibility, populated from `CallId` or left null.
For the call id propagation:
A. **A request-scoped ambient (`HostedCallContext`, an `AsyncLocal<string?>`)** set by the handler and re-applied before each egress point, read by the outbound delegating handler.
B. Thread the call id through every method signature down to the toolbox bearer handler.
For session keying (previously implied by the conversation/chat pairing):
I. **`HostedConversationKey`** resolving a stable partition from `conversation_id ?? partition(previous_response_id) ?? partition(responseId)`.
II. Continue keying on the container session id (`FOUNDRY_AGENT_SESSION_ID`).
## Decision Outcome
Chosen: **Option 1** for identity, **Option A** for call id, **Option I** for session keying.
Rationale:
- **`ChatId` dropped (Option 2 rejected).** The platform no longer supplies a chat key; carrying a synthetic one would invent identity the trust boundary does not provide. `HostedSessionContext` becomes user-only (`HostedSessionContext(string userId)` / `UserId`), and the strict-resume check validates `UserId` alone. The corresponding `HostedFoundryMemoryProviderScopes` values `PerChat` and `PerUserAndChat` are removed; `PerUser` is retained.
- **Ambient call id (Option B rejected).** Writing `HostedCallContext.CallId` inside the streaming `async IAsyncEnumerable` iterator is reverted across each `yield`, so a single up-front assignment is lost before the toolbox/MCP egress runs. The handler therefore captures `context.PlatformContext?.CallId` once and **re-applies it immediately before each egress point**; `FoundryToolboxBearerTokenHandler` forwards it as `x-agent-foundry-call-id`. The ambient is request-scoped and never leaks into the caller's execution context (guarded by a unit test).
- **`HostedConversationKey` (Option II rejected).** One container serves many conversations for its lifetime, so the container session id cannot key per-conversation state. The partition key is derived from the conversation/`previous_response_id`/minted response id instead.
Implementation summary in `Microsoft.Agents.AI.Foundry.Hosting`:
| Type | Visibility | Change vs ADR-0026 |
|---|---|---|
| `HostedSessionContext` | public sealed | Now user-only (`UserId`); `ChatId` removed. |
| `PlatformHostedSessionIsolationKeyProvider` | internal sealed | Maps `context.PlatformContext.UserIdKey` (was `context.Isolation.UserIsolationKey` / `ChatIsolationKey`). |
| `HostedCallContext` | internal static | New. Request-scoped `AsyncLocal<string?>` holding the `x-agent-foundry-call-id` value. |
| `HostedConversationKey` | internal | New. Resolves the per-conversation partition key. |
| `FoundryToolboxBearerTokenHandler` | internal | Now also forwards `x-agent-foundry-call-id` outbound. |
| `HostedFoundryMemoryProviderScopes` | public | `PerChat` / `PerUserAndChat` removed; `PerUser` kept. |
Package manifests bump the responses container protocol to `2.0.0` (invocations stays `1.0.0`).
## Consequences
Positive:
- Per-user memory partitioning and the strict-resume tamper defense from ADR-0026 are preserved with no public API churn for samples or providers.
- Per-user toolbox OAuth consent and other server-side caller-context lookups keep working because the per-request call id is forwarded on egress.
- Works unchanged on protocol `1.0.0` (no call id) and `2.0.0`.
Negative:
- `HostedSessionContext.ChatId` and the `PerChat` / `PerUserAndChat` memory scopes are removed; any out-of-tree consumer that referenced them must move to user-scoped partitioning.
- The call id must be re-applied before every egress point because of the async-iterator `AsyncLocal` revert; a missed re-apply silently drops the header. This is covered by unit tests.
## Out of scope
- HMAC tamper signatures over the persisted context remain unimplemented; equality comparison against `ResponseContext.PlatformContext` on every request is sufficient because the platform sets the header at the trust boundary.
- The per-request `User` field on `CreateResponse` is still intentionally not consumed.
@@ -1,119 +0,0 @@
---
status: accepted
contact: rogerbarreto
date: 2026-06-30
deciders: rogerbarreto
consulted: []
informed: []
---
# Per-agent and per-user session-storage isolation for Foundry Hosting
Builds on [ADR-0030](0030-hosted-platform-context-agentserver-2.0.md).
## Context and Problem Statement
A Foundry hosted container can serve many end users (and, in .NET, many agents) over its lifetime. The
`AgentSessionStore` persists each turn's `AgentSession` (which for a workflow agent carries the workflow
checkpoint, and which also carries the tool-approval mapping via `ToolApprovalIdMap` in the session state
bag). [ADR-0030](0030-hosted-platform-context-agentserver-2.0.md) protected cross-user access only through
the strict-resume identity check (a 403 when the persisted `HostedSessionContext.UserId` does not match the
live request). The persisted artifacts themselves were keyed by `conversationId` (+ agent name), not
physically partitioned per user, so a forged `conversation_id` would still resolve to another user's file
path before the identity check rejected it.
The Python hosting package added physical per-user partitioning (`<root>/<user_id>/<context_id>`) plus a
reject-style path-traversal guard. We want .NET to provide the same defense-in-depth, adapted to the .NET
hosting model.
## Decision Drivers
- Defense in depth: a forged/guessed id must not even resolve to another tenant's storage path, independent
of the identity check.
- Multi-agent hosting: a single .NET container hosts multiple agents resolved from keyed DI, so the layout
must isolate per agent as well as per user (Python hosts a single agent and needs no agent layer).
- Path-traversal safety (CWE-22) for the untrusted, platform-injected user id.
- Back-compat for local development (no `x-agent-user-id` header) and for direct/non-hosted store use.
- Keep the change contained and avoid the async-iterator `AsyncLocal` revert hazard from ADR-0030.
## Considered Options
- **Path partition inside `FileSystemAgentSessionStore`**, threading the user id explicitly through the
`AgentSessionStore` API, with self-describing prefixed segments.
- A delegating store that prefixes the conversation id with the user id (the
`IsolationKeyScopedAgentSessionStore` pattern from `Microsoft.Agents.AI.Hosting`). Rejected: still needs the
user id on the read path and yields a flat key rather than nested per-tenant directories.
- An `AsyncLocal<string?>` user-context set by the handler. Rejected: the session is saved in the handler's
`finally` after the streaming `yield`s, where an `AsyncLocal` set up front is reverted (the same hazard
that forced explicit call-id re-application in ADR-0030). Explicit threading is safer and clearer.
- A separate per-user approval store (as in Python). Rejected as unnecessary: see below.
## Decision Outcome
Path layout with self-describing, prefixed segments; user id threaded explicitly:
{root}/ a-{agentName} / u-{userId} / c-{contextId}.json
- `a-` (agent), `u-` (user), `c-` (context) are constant literals applied to the sanitized/validated value,
so a collapsed layout is never ambiguous and a user id can never masquerade as an agent name.
- `contextId` is `HostedConversationKey.Resolve` (conversation_id, else the partition of
previous_response_id, else of the minted response id).
- The agent and context layers are always present (Foundry always deploys a named agent). The only
collapse is the `u-` layer: present when a user id is resolved (Foundry header, or local dev fallback),
absent for raw local runs with no header (`{root}/a-{agent}/c-{conv}.json`). There is no user-only or
no-agent layout.
Other elements:
- `string? userId` was added as a **required** parameter (no default) on `AgentSessionStore.GetSessionAsync` /
`SaveSessionAsync` (a contained, breaking change to the experimental Foundry abstraction; both in-tree
implementations and the two handler call sites were updated). It is required rather than optional so a
caller can never silently persist a session unscoped; a genuine no-user caller (local without the header,
or a non-hosted direct caller) passes `null` explicitly. `AgentFrameworkResponseHandler` resolves the user
id before loading the session.
- Path-traversal guard: the user id is rejected (not sanitized) when it is not a single safe path segment
(path separators, NUL, drive letters, rooted paths, all-dot segments). After building the path, the
fully-resolved path is asserted to remain under the storage root.
- The strict-resume 403 identity check from ADR-0030 is **kept** as the second defense layer (it still
catches a session that reaches the wrong partition, e.g. via a non-partitioning custom store or in-process
tampering).
- **No separate approval store.** The tool-approval mapping lives in `ToolApprovalIdMap` ->
`AgentSessionStateBag`, which is serialized into the session checkpoint, so partitioning the session path
isolates pending approvals per tenant automatically. (Python needs a separate per-user approval store only
because it models approvals as a standalone store.)
## Consequences
Positive:
- Cross-tenant isolation is now defense-in-depth: physical per-agent/per-user partitioning plus the identity
check. Approvals and workflow checkpoints inherit the partitioning because they ride in the session.
- Self-describing prefixes make the on-disk layout auditable and collision-free across collapse cases.
Negative:
- Breaking change to the experimental Foundry `AgentSessionStore` API (added `userId`).
- The on-disk layout and leaf filename change (`<conv>.json` -> `c-<conv>.json`), orphaning sessions written
by the ADR-0030 release. Acceptable for an experimental package; a fresh session is created on next use.
## Out of scope
- Encryption at rest and quota enforcement remain platform concerns.
- Non-Foundry hosting layers can adopt an equivalent scheme independently.
## Update (2026-07-01): local runs no longer fail closed; sample dev provider removed
Superseding the ADR-0026/0030 behavior where a `null` result from `HostedSessionIsolationKeyProvider`
always became a 500, `AgentFrameworkResponseHandler` now branches on `FoundryEnvironment.IsHosted`:
- **Hosted** (`IsHosted == true`, production): a `null` identity is still a hard error (500). Isolation
stays strict; the platform always injects `x-agent-user-id`.
- **Not hosted** (local `docker run` / `dotnet run`): a `null` identity is tolerated. Per-user isolation
is simply not triggered — the handler passes `userId == null` to the store (the documented "no user
partition", `{root}/a-{agent}/c-{conv}.json`), stamps no `HostedSessionContext`, and runs no
strict-resume check. Contributors can run a hosted image locally with zero extra setup.
Consequently the sample-side `DevTemporaryLocalUserIdProvider` and `AddDevTemporaryLocalContributorSetup`
were removed. To simulate distinct users locally, send an `x-agent-user-id` request header; the default
`PlatformHostedSessionIsolationKeyProvider` reads it via `ResponseContext.PlatformContext.UserIdKey`
(the SDK's `PlatformContext.FromRequest` populates it from the header unconditionally, hosted or not).
@@ -1,177 +0,0 @@
---
status: accepted
contact: rogerbarreto
date: 2026-07-08
deciders: rogerbarreto
consulted: eavanvalkenburg
informed: []
---
# .NET hosting: OpenAI Responses protocol helpers for app-owned routing
Realizes the helper-first direction of [ADR-0027](0027-hosting-channels.md) for .NET.
## Context and Problem Statement
[ADR-0027](0027-hosting-channels.md) refocused the (Python) hosting design away from a channel
framework toward **protocol conversion helpers plus optional execution state**: Agent Framework owns
protocol-native <-> run conversion, while the application owns HTTP routing, authentication,
middleware, storage, and native SDK calls.
.NET already ships `Microsoft.Agents.AI.Hosting.OpenAI`, a route-owning server that **exposes an
`AIAgent` (or workflow) as the OpenAI Responses API** (`MapOpenAIResponses` + `IResponsesService`). It
owns the routes, an in-memory response/conversation store, streaming, and lifecycle. The question is
what, if anything, .NET must add to satisfy the ADR-0027 boundary.
## Decision Drivers
- Do not reinvent conversion logic that already exists and is battle-tested in `Hosting.OpenAI`.
- Give applications a way to own their own route/auth/middleware/storage while reusing Agent Framework
conversion (the ADR-0027 boundary).
- Keep the released public surface small.
- Stay consistent with the existing .NET hosting stack, which deliberately does **not** use the OpenAI
SDK Responses types server-side (it hand-rolled its own wire model).
## Considered Options
1. Self-contained new package that reimplements conversion using the OpenAI SDK Responses types
(mirrors the Python `agent-framework-hosting-responses` lineage).
2. New package that reuses `Hosting.OpenAI`'s internal converters (via `InternalsVisibleTo` or by
moving the conversion core out).
3. Thin public helper facade **inside** `Hosting.OpenAI` over the existing internal converters, plus
protocol-neutral execution-state holders in `Microsoft.Agents.AI.Hosting`.
### First-principles gap analysis
A capability comparison of the ADR-0027 / PR #6891 helper surface against the existing .NET stack:
| Python helper capability | .NET today | Status |
| --- | --- | --- |
| `responses_to_run` | `ResponseInput.GetInputMessages` + `InputMessage.ToChatMessage` + `OpenAIResponsesMapOptions.RunOptionsFactory` | exists, internal |
| `responses_from_run` | `AgentResponseExtensions.ToResponse` | exists, internal |
| `responses_from_streaming_run` | `AgentResponseUpdateExtensions.ToStreamingResponseAsync` + `SseJsonResult` (also renders workflow events) | exists, internal, richer |
| `responses_session_id` | continuity resolved inside `InMemoryResponsesService` | exists, internal, not standalone |
| `create_response_id` | `IdGenerator` | exists, internal |
| `AgentState` (target + store, get-or-create, callable/awaitable target) | `AgentSessionStore` (get-or-create + save + serialize + isolation) + DI container (target lifetime + async setup) | create-on-miss lives in the store; per-run instance and deferred/async target come from DI, so no separate holder is needed |
| `SessionStore` (get/set/delete) | `AgentSessionStore` + `InMemoryAgentSessionStore` | richer; `Delete` added |
| `WorkflowState` + checkpoint resume | `WorkflowCatalog`/`HostedWorkflowBuilder`; workflow events already render over Responses; `CheckpointManager` is session-keyed | partial; no per-session checkpoint cursor |
| App owns routing/auth/middleware/storage | `MapOpenAIResponses`/`IResponsesService` own routing + storage | **the one real gap** |
.NET already covers ~90% of the capability, and more richly (its streaming renderer even emits workflow
events; its session store serializes and supports per-principal isolation, neither of which Python's
in-memory `SessionStore` does). The single genuine gap is the **ownership model**: every conversion
primitive is bundled behind the route-owning server, so an application cannot own its own route and
call just the conversion.
Note on lineage: Python's Responses offering was introduced *as a channel* (PR #6580) and always used
the `openai` SDK Responses types. .NET's `Hosting.OpenAI` predates and is independent of channels and
hand-rolled its own server-side wire DTOs (the SDK's Responses types are client-shaped and awkward
server-side). So Option 1 would both reinvent a working asset and contradict the .NET codebase's own
precedent.
## Decision Outcome
Chosen option: **3. Thin public helper facade inside `Hosting.OpenAI` plus neutral state holders**,
because the only real gap is the ownership model, so the work is to *un-bundle* the existing
converters, not to rebuild them or add a package.
### Public surface
`Microsoft.Agents.AI.Hosting.OpenAI` gains a single public static facade, `OpenAIResponses`, whose
boundary is `System.Text.Json` (`JsonElement`/streamed events), matching Python's dict boundary and
keeping the hand-rolled wire DTOs internal:
- `OpenAIResponses.ToAgentRunRequest(JsonElement body)` -> messages + `AgentRunOptions?`.
- `OpenAIResponses.WriteResponse(AgentRunResponse response, string responseId, string? sessionId = null)`
-> a Responses-shaped `JsonElement`.
- `OpenAIResponses.WriteResponseStreamAsync(IAsyncEnumerable<AgentRunResponseUpdate> updates, string responseId, ...)`
-> Responses SSE `data:` frames.
- `OpenAIResponses.GetSessionId(JsonElement body)` -> `previous_response_id` or `conversation` id, or
`null`. Kept **separate** from `ToAgentRunRequest` so the trust boundary is visible: choosing to use
a request-derived key is an explicit application decision.
- `OpenAIResponses.CreateResponseId()` -> a `resp_*` id.
All helpers are side-effect-free and delegate to the existing internal converters. `MapOpenAIResponses`
public behavior is unchanged; it and the facade share one internal conversion core (an internal
`ToResponse` overload with an optional originating request is added so the facade can render without a
request object).
### Optional execution state (neutral package)
`Microsoft.Agents.AI.Hosting` gains:
- `AgentSessionStore.DeleteSessionAsync(...)` (+ `InMemoryAgentSessionStore` implementation and
isolation-decorator passthrough): the one missing store operation.
- No agent-side holder. Applications use `AgentSessionStore` directly: `GetSessionAsync(agent, id)`
already creates on miss and returns an independent session instance per call (so concurrent calls fork
the same stored state rather than sharing an instance), `SaveSessionAsync(agent, id, session)` persists
post-run (including under a newly minted id), and `DeleteSessionAsync(agent, id)` removes it. An earlier
draft added a `HostedAgentState` holder, but once create-on-miss lives in the store and the store does no
cross-call locking, the holder would only bind the `agent` argument, which is not enough to justify a
public type. Any coordination for concurrent runs against the same id is the application's concern.
(Unlike Python, whose `SessionStore` is get/set-only and whose `AgentState` therefore owns
create-on-miss, .NET's store already owns it.)
Python's `AgentState` carries two further responsibilities beyond create-on-miss: it accepts a callable
or awaitable target so the host can (1) obtain a fresh agent instance per run and (2) defer expensive or
asynchronous agent setup while keeping server construction synchronous. In .NET these two concerns are
owned by the dependency-injection container, not by a hosting type. Per-run lifetime is expressed by the
registration lifetime (`AddScoped`/`AddTransient` yields a fresh `AIAgent` per request or scope, resolved
by the framework), and deferred or asynchronous construction is expressed by an async factory registration
(for example an `async` factory delegate, `ActivatorUtilities`, or resolving the agent inside the request
after any async warm-up), so the route handler resolves an already-built agent from the container. An
`AIAgent` is also safe to invoke concurrently (per-turn state lives in `AgentSession`, not the agent), so
the "fresh instance per run" motivation does not apply to it the way it does to a workflow. This is the
deliberate asymmetry with `HostedWorkflowState` below: a `Workflow` instance is a stateful run engine that
cannot be driven by two runners at once, so the factory/`cacheWorkflow` affordance is load-bearing there
for correctness, whereas for agents the container already provides both per-run instances and async setup.
- `HostedWorkflowState`: a thin holder bundling a workflow target with a `CheckpointManager` and an
internal `sessionId -> CheckpointInfo` head cursor, exposing `RunOrResumeAsync`. .NET's checkpoint
store is already `sessionId`-keyed (unlike Python's workflow-name keying), but `CheckpointInfo` has
no ordering, so the holder remembers the head checkpoint per session to resume. On subsequent turns it
restores that checkpoint and runs the workflow forward with the new turn's input (mirroring the Python
host's restore-then-run semantics), rather than continuing a halted run with no input. When the
in-memory cursor misses (new holder / process restart) it reads the session's latest checkpoint from the
`CheckpointManager`, so a durable manager resumes across restarts. It accepts either a single workflow
instance (which cannot be run by two runners at once, so its turns are processed one at a time) or a
workflow factory (`Func<CancellationToken, ValueTask<Workflow>>`). By default the factory builds a fresh
instance per run so independent sessions run in parallel; with `cacheWorkflow: true` the factory is invoked
once lazily and its result is cached and reused (a deferred, cached target that, like the instance, cannot
run concurrent turns). A resume rehydrates an instance from the session's checkpoint in the shared store, so
per-run instances still continue the same run; concurrent turns against the same session id remain the
application's coordination responsibility.
### Scope
Responses only for v1; the facade is named so a parallel `OpenAIChatCompletions` facade can follow.
No new package, no OpenAI-SDK-typed reimplementation, no change to `MapOpenAIResponses` public
behavior.
### Security responsibilities
Consistent with ADR-0027, the application owns the trust boundary. `GetSessionId(...)` returns an
untrusted candidate key; the application must authenticate the caller and authorize/bind the id before
using it as an `AgentSessionStore` key or workflow checkpoint session id. Multi-user hosts must scope
the session store per principal (`IsolationKeyScopedAgentSessionStore`). Helpers stay side-effect-free;
persistence happens only after the run completes.
## Consequences
Positive:
- Smallest possible surface: the released addition is one facade type plus one thin workflow state
holder and one new store method (agents use `AgentSessionStore` directly, no holder).
- No duplicated conversion; the app-owned-routing path and the route-owning server share one core.
- `MapOpenAIResponses` users are unaffected.
Negative:
- The facade's `JsonElement` boundary is less strongly typed than the internal DTOs (accepted to keep
the wire model internal and mirror Python's dict boundary).
- Workflow resume relies on an in-memory head cursor by default; durable multi-replica hosts must
supply their own cursor persistence.
## More Information
- Parent ADR: [ADR-0027](0027-hosting-channels.md).
- Spec: `docs/specs/003-dotnet-hosting-protocol-helpers.md`.
@@ -1,104 +0,0 @@
---
status: Accepted
contact: cgillum
date: 2026-07-21
deciders: cgillum, vrdmr, chetantoshniwal
consulted: westey-m, eavanvalkenburg, kshyju, larohra, ahmedmuhsin
informed:
---
# Extract Durable Task and Azure Functions hosting into a separate repository
## Context and Problem Statement
The Durable Task and Azure Functions hosting integrations (`agent-framework-durabletask`,
`agent-framework-azurefunctions`, plus their samples, docs, and CI) currently live in the
`microsoft/agent-framework` (MAF) monorepo. They carry heavyweight specialized dependencies
(Azure Functions runtime, Durable Task) and need integration-test infrastructure (Functions Core
Tools, Azurite, a DTS emulator) that the core repo otherwise does not.
This ADR proposes moving them into a dedicated repository
([`microsoft/agent-framework-durable-extension`](https://github.com/microsoft/agent-framework-durable-extension))
and considers how to do so without breaking existing users who import them today.
## Decision Drivers
- **Independent lifecycle** — the hosting integrations should be able to version and release on their
own cadence, decoupled from core (extends [ADR-0008](0008-python-subpackages.md)'s goal of keeping
heavyweight/optional dependencies out of the main package).
- **Dependency & CI isolation** — keep core lean and its PR pipeline free of heavyweight hosting
dependencies and integration-test prerequisites.
- **Ownership** — a dedicated repo would give the integrations their own issues, CODEOWNERS, and
contribution flow.
- **No breaking change** — existing `from agent_framework.azure import …` code and
`pip install agent-framework[all]` should keep working (stable-import-path guarantee, ADR-0008).
## Considered Options
1. **Keep in the MAF repo** (status quo).
2. **Move out, drop the core shim** — the extension becomes standalone; core stops re-exporting the
types and removes them from `[all]`.
3. **Move out, keep core's backward-compat shim + `[all]`** (proposed) — the code would live in the
new repo; core would still lazily re-export the entry-point types from `agent_framework.azure` and
keep both packages in the `[all]` extra (resolved from PyPI).
## Decision Outcome
Proposed choice: **Option 3.** Extract the integrations for lifecycle, dependency, and ownership
isolation, while preserving the existing import surface so the move is invisible to consumers.
Option 1 forgoes the isolation benefits; Option 2 achieves them but would be a breaking change for
existing imports and the `[all]` extra.
### Consequences
- Good — would give independent release cadence, a leaner/faster core repo and CI, and clear
ownership for the hosting integrations.
- Good — no user-visible break: existing imports and `agent-framework[all]` would continue to work
unchanged.
- Neutral — type *definitions* would live once in the extension; the core shim would re-export only a
curated subset of entry-point types (no metadata duplication). The extension's own samples/docs
would import directly from `agent_framework_durabletask` / `agent_framework_azurefunctions`; the
shim would be compatibility-only.
- Neutral — users may still open GitHub issues against the core repo for problems in the extension,
but the extension's own repo would be the primary place for issues and PRs. These issues would
need to be triaged and transferred to the extension repo.
- Neutral — **.NET public API boundary.** The extension should prefer the smallest stable public core
API over friend-assembly access where the capability is useful to external hosts or tooling. For
workflow routing metadata, the agreed first step is to expose a read-only `Workflow.Edges` view plus
public `EdgeData.Connection` and `FanOutEdgeData`, while keeping graph construction internal
([#7448](https://github.com/microsoft/agent-framework/issues/7448),
[#7459](https://github.com/microsoft/agent-framework/pull/7459)). This reduces internal coupling but
adds a public API compatibility commitment. Any remaining internal dependencies would still need to
be evaluated individually before retaining `InternalsVisibleTo`.
- Bad — **Python version coordination.** Core's shim correctness would track the extension's publish
cadence. In the other direction, when an extension package adopts a new core API, maintainers would
need to choose per feature between raising its minimum core version (simpler, but forces every
extension user to upgrade) and conditional imports with fallback behavior (preserves support for
older core versions, but adds implementation and testing complexity).
## Validation
Compliance would be validated by:
- Python: `uv lock --check` passing with both packages resolving from PyPI; the shim entry-point
symbols importing at runtime after `uv sync --all-extras`; `pyright` staying clean on
`agent_framework/azure/__init__.pyi`; and extension tests running against both the minimum supported
and current core versions when conditional compatibility behavior is used.
- .NET: tests from an external assembly confirming that workflow routing metadata is inspectable
through the agreed public surface while graph construction remains internal.
A known risk is **publish-lag**: if a symbol is added to core's shim before the extension has
published a release that exports it, that symbol would not resolve at runtime. The mitigation would
be to omit any such symbol from the shim until the extension publishes it, then add the entry and
re-lock.
## More Information
- Related: [ADR-0008](0008-python-subpackages.md) (vendor namespaces + stable import paths),
[ADR-0021](0021-provider-leading-clients.md) (lazy-loading gateways),
[issue #7448](https://github.com/microsoft/agent-framework/issues/7448) and
[PR #7459](https://github.com/microsoft/agent-framework/pull/7459) (.NET workflow routing API).
- Follow-ups: during extraction, keep the shim's re-exported symbols in sync with each newly
published extension release (adding any symbol only once the extension publishes it); document the
direct-import convention in the extension's samples READMEs so samples are not switched back to the
shim.
@@ -1,642 +0,0 @@
---
status: accepted
contact: eavanvalkenburg
date: 2026-07-22
deciders: eavanvalkenburg, chetantoshniwal
consulted: TaoChenOSU, moonbox3, peibekwe, rogerbarreto, westey-m
informed:
---
# Feature-usage bitmask in the User-Agent
## Context and Problem Statement
We can see which Agent Framework packages are installed and that *some* framework
call happened (via the existing `agent-framework-python/{version}` User-Agent),
but we have no usage-based signal about **which features are actually exercised**
at runtime, nor which are used *together* (e.g. workflows + MCP + Foundry). How
can we collect a lightweight, privacy-respecting signal of feature usage for the
traffic we can actually read, without standing up new event pipelines?
The detailed mechanism is in [SPEC-004](../specs/004-feature-usage-telemetry.md);
the per-language bit tables are in
[feature-usage-bit-registry.md](../specs/feature-usage-bit-registry.md).
## Decision Drivers
- **Transparency** — openly documented, human-decodable, user-controllable. No
hidden or obfuscated telemetry.
- **First-party scope / no third-party leakage** — emission requires both an
explicitly approved client/pipeline family and an approved actual HTTPS origin
on every request (including redirects). Credentials or an Azure setting alone
never approve a custom gateway/origin.
- **Live signal** — read the process's observed-feature set *so far* at request
send time, rather than freezing it at client construction.
- **Low cost / few moving parts** — reuse telemetry already in the request path;
bounded fixed-width processing; as little machinery as the job needs.
- **Privacy** — encode only coarse "observed at least once" Boolean feature
state, never counts; no identifiers, arguments, prompts, payloads,
model/deployment names, endpoints, or customer-defined names.
- **Use, not presence** — package-level indexes mean a capability reached its
first meaningful activation, not that a package was installed/imported or a
DI container constructed an unused service.
- **Versioning discipline** — v1 is a point-in-time decision. Adding bits later is
easier than removing or redefining them, so the initial table should lean toward
fewer bits and avoid forcing v2 shortly after launch.
- **Allocation discipline** — each bit represents a stable framework-owned
capability with a concrete product/support question and an actual-use mark
point; implementation detail and speculative distinctions stay out.
## Considered Options
The options below are grouped by the decisions that matter: the **transport**,
the **granularity**, and the **registry sharing model**.
### Transport
#### A. User-Agent token, first-party only, per request (chosen)
Stamp a `(feat=...)` comment onto the UA, but only on approved Azure/Foundry
client pipelines, and re-evaluate it per request.
- Good, reuses telemetry already sent to approved backends we can read.
- Good, request-time stamping reflects the live mask (not frozen at construction).
- Good, first-party scoping means no fingerprint leaks to third-party providers.
- Good, two-factor destination approval (pipeline + actual origin) denies custom
`base_url` gateways and strips the token on unapproved redirect hops.
- Good, maps onto .NET's existing per-request UA pipeline policies unchanged.
- Neutral, v1 stamps only pipelines the framework creates or can configure
through supported public hooks. It does not mutate caller-owned clients or
reach into private SDK pipelines.
- Bad, no signal for traffic that never hits a first-party endpoint (accepted —
we couldn't read it anyway).
#### B. User-Agent token on all clients
- Good, simplest to wire (one static header).
- Bad, sends a deployment fingerprint to OpenAI/Anthropic/AWS/Google logs we
cannot read — privacy leak for zero benefit.
- Bad, baked into static `default_headers`, so it freezes at client construction
and reports a near-empty mask.
#### C. OpenTelemetry span/resource attribute
- Good, precise per-call usage; no UA change.
- Bad (**privacy — the main reason to hold it**), a span attribute broadcasts the
feature-combination fingerprint into the user's **general** telemetry pipeline,
which is typically exported to third-party APM vendors (Datadog, Honeycomb, …).
That re-introduces exactly the fingerprint leakage the first-party-only UA
scoping (A) was chosen to avoid — just into a different set of third parties.
- Bad (secondary), also a cardinality footgun (a growing, combinatorial value
must never become a metric dimension).
- Neutral, for the team's own goal it reaches us only if the user exports to
Azure Monitor and we query it.
- **Deferred, not rejected.** The version prefix lets us add it later **if** the
User-Agent path cannot answer a concrete query and there is an acceptable
scoped/redacted variant.
#### D. Bespoke usage events
- Good, richest detail and flexibility.
- Bad, new data flow and cost; larger privacy surface; heavy to build and review;
overkill for a coarse "which features" signal.
#### E. Install/import-time signal only (status quo-ish)
- Good, zero new runtime work.
- Bad, measures installation, not usage; cannot capture feature combinations —
does not solve the problem.
### Accumulation scope
#### S1. Process-global, monotonic mask (chosen)
A single mask per process; bits are OR-ed in as features are first used and never
cleared. The token reflects "what this process has used so far."
- **Binary interpretation:** a set bit means the feature was observed at least
once in this process before the request was sent. A bit repeated on later
requests is the same Boolean observation, not another feature use. It cannot be
summed into invocation, request, agent, user, or tenant counts.
- Good, fits our **mixed feature lifecycle**: many features are *not* bound to an
outbound service request — an agent/workflow may first run or build, a
context/history provider may first participate in a session, and a host may
start serving before the request that later emits the token. A process-wide
mask can carry those activations forward.
- Good, trivial and cheap: one OR under a lock (Python) / one atomic OR into one
of two 64-bit lanes (.NET); no per-request state plumbing.
- Good, deliberately coarse for privacy: it avoids emitting a sequence of exact
per-call feature combinations that could reconstruct a workload's behavioral
trace.
- Neutral, coarser than per-call — early requests carry fewer bits than later
ones, and the token says "this process used X", not "this call used X" or "X
was used this many times."
For example, at time 1 Agent A can use MCP and a Foundry chat client. At time 2,
Agent B in the same worker can make a normal Foundry chat call without MCP. The
time-2 request still carries the MCP bit because MCP was previously observed in
that process. It does **not** say Agent B used MCP, nor count a second MCP use.
#### S2. Per-request set, reset between calls (botocore's model — rejected)
AWS botocore scopes its `m/` feature codes to a `contextvars` set that is reset
between requests, giving exact per-call attribution (and it deliberately no-ops
when called outside a request context to avoid features bleeding across requests).
See [Prior art](#prior-art).
- Good, exact per-call attribution directly in the User-Agent.
- Bad, **assumes every feature is exercised inside a single service request**
true for botocore (an SDK natively bound to AWS service calls), but *not* for
us. Our features split into request-scoped ones (a chat call, an MCP tool
invocation) and decidedly non-request ones (workflow build/start, provider
participation, hosting startup). The latter have no service request to attach to, so a
per-request set would simply miss them.
- Bad, needs `contextvars` propagation through every async/threaded path and a
reset discipline, plus enable/disable calls around every scoped operation; the
bleed-guard botocore documents is the warning sign.
- Bad, creates a more detailed per-call behavioral trace, increasing the privacy
sensitivity and review burden compared with a coarse process-lifetime Boolean.
- Note, per-call attribution for the request-scoped subset is better served by
the deferred OTel span path (option C) than by reshaping the UA token.
### Granularity
The mechanism can support several granularities. The remaining decision before
implementation is how detailed v1 should be. The estimates below are
intentionally rough; v1 uses a fixed 128-bit bound to leave useful headroom
without making the registry unbounded.
#### F0. Package-level bits
One bit per package, set on first use of a package-owned public API, client,
provider, or tool. It is **not** set on install, import, or assembly load.
Examples that get bits:
- `agent-framework-core` when `Agent`, `AgentSession`, `Workflow`, etc. is used.
- `agent-framework-tools` when a `LocalShellTool` or `DockerShellTool` first
executes/probes its shell capability.
- `agent-framework-foundry` when a `FoundryChatClient`, `FoundryAgent`, etc.
performs its first Foundry operation.
- `agent-framework-openai` when `OpenAIChatClient`,
`OpenAIEmbeddingClient`, etc. performs its first provider operation.
- `agent-framework-azure-ai-search` when `AzureAISearchContextProvider` is used.
- `agent-framework-azure-cosmos` when `CosmosHistoryProvider` is used.
- `agent-framework-redis` when `RedisContextProvider` or `RedisHistoryProvider`
is used.
Examples that do **not** get separate bits: merely installed dependencies;
imports or DI construction with no activation; `Agent` vs `AgentSession` vs
`InMemoryHistoryProvider`; `FunctionTool` vs `MCPStdioTool` vs `LocalShellTool`
vs `DockerShellTool`; `FoundryChatClient` vs `FoundryAgent`; `OpenAIChatClient`
vs `OpenAIEmbeddingClient`.
Rough estimate: Python ~25-35 bits; .NET ~15-25 bits.
- Good, lowest specificity and simplest registry.
- Good, clearly measures usage rather than dependency inventory if bits are set
only at package-owned public API/client/provider/tool use sites.
- Bad, does not answer which major capability within a package is used.
#### F1. Package + major capability bits
Package bits plus selected major capabilities that are product-distinct and stable
across implementations.
Examples that get bits:
- `agent-framework-core` plus `Agent`.
- `AgentSession` plus `InMemoryHistoryProvider` / `FileHistoryProvider` as one
history capability.
- `Workflow` / `FunctionalWorkflow` as one workflow capability.
- `FunctionTool`; MCP transports as one MCP capability; shell tools as one shell
capability.
- Skills provider plus stable source types: file, in-memory/programmatic, and
MCP-backed skills (with .NET inline/class skill distinctions).
- Foundry chat/agent/embedding capabilities; OpenAI chat/embedding capabilities.
Examples that do **not** get separate bits: `InMemoryHistoryProvider` vs
`FileHistoryProvider`; `WorkflowBuilder`, `AgentExecutor`, `FunctionExecutor`, or
`FanOutEdgeGroup`; `MCPStdioTool` vs `MCPStreamableHTTPTool` vs
`MCPWebsocketTool`; `LocalShellTool` vs `DockerShellTool` vs
`ShellEnvironmentProvider` vs `ShellPolicy`; `OpenAIChatClient` vs
`OpenAIChatCompletionClient`; skill-source decorators such as caching, filtering,
deduplication, and aggregation.
Rough estimate: Python ~60-70 indexes; .NET ~45-55 indexes. The current candidate
registry is at 63 Python / 52 .NET assigned indexes.
- Good, likely answers the first product adoption questions while staying compact.
- Good, fits comfortably within 128 bits while leaving room for additive package
and feature growth.
- Neutral, some provider internals remain collapsed until a later additive bit is
justified.
#### F2. Public construct / concrete type bits
One bit per public construct that users intentionally instantiate or configure.
Examples that get bits:
- `Agent`, `AgentSession`, `InMemoryHistoryProvider`, `FileHistoryProvider`.
- `Workflow`, `WorkflowBuilder`, `FunctionalWorkflow`.
- `FunctionTool`, `MCPStdioTool`, `MCPStreamableHTTPTool`, `MCPWebsocketTool`.
- `LocalShellTool`, `DockerShellTool`, `ShellEnvironmentProvider`, `ShellPolicy`.
- `FoundryChatClient`, `FoundryAgent`, `OpenAIChatClient`,
`OpenAIChatCompletionClient`, `OpenAIEmbeddingClient`.
Examples that do **not** get separate bits: `Agent.run` vs
`Agent.run_streamed`; workflow edge/executor internals such as `AgentExecutor`,
`FunctionExecutor`, or `FanOutEdgeGroup`; `LocalShellTool` persistent vs
stateless mode; `ShellPolicy` allowlist vs denylist configuration; `FunctionTool`
approval mode or result parser choices.
Rough estimate: Python ~70-100 bits; .NET ~55-80 bits.
- Good, concrete and directly tied to public API use.
- Neutral, fits within 128 bits at the current estimate, but consumes much of the
deliberate growth reserve.
- Bad, adds many call sites and more fingerprint specificity for v1.
#### F3. Construct subtype / configuration bits
Split important constructs by mode, transport, storage, or workflow primitive
when that distinction matters.
Examples that get bits:
- `InMemoryHistoryProvider` and `FileHistoryProvider` separately.
- `FunctionalWorkflow`, `WorkflowBuilder`, `AgentExecutor`, `FunctionExecutor`.
- `FanOutEdgeGroup`, `FanInEdgeGroup`, `SwitchCaseEdgeGroup`.
- `LocalShellTool` persistent, `LocalShellTool` stateless, `DockerShellTool`.
- `MCPStdioTool`, `MCPStreamableHTTPTool`, `MCPWebsocketTool`;
`OpenAIChatClient` vs `OpenAIChatCompletionClient`.
Examples that do **not** get separate bits: exact session id or persisted history
file path; exact shell command, workdir, timeout, or output cap; exact MCP server
command, URL, or tool names from the server; exact workflow graph shape or edge
count; model/deployment names, prompts, tool arguments, payloads.
Rough estimate: Python ~110-150 bits; .NET ~85-125 bits.
- Good, useful where mode-level distinctions are decision-relevant.
- Bad, trades simplicity for precision, increases fingerprint specificity, and
may exhaust or exceed 128 bits in Python.
#### F4. Option / behavior flag bits
The most detailed framework-owned option: bits for specific modes and behavior
switches, still excluding customer/runtime values.
Examples that get bits:
- Agent streaming used vs non-streaming used.
- `FunctionTool` `approval_mode="always_require"` vs `"never_require"`.
- `FunctionTool` `SKIP_PARSING` / result-parser path used.
- MCP sampling configured; MCP long-running task support used.
- `LocalShellTool` `clean_env` / `confine_workdir`; `DockerShellTool` container
mode.
Examples that do **not** get separate bits: function names wrapped by
`FunctionTool`; approval rule arguments or approval decisions; MCP remote tool
names or schemas; shell command text or policy regex patterns; prompt/message
content, model names, URLs, tenant/user/session identifiers.
Rough estimate: Python 150+ bits; .NET 120+ bits.
- Good, maximum framework-owned detail.
- Bad, exceeds or nearly exhausts 128 bits and is too detailed for v1 without a
concrete decision that requires it.
### Registry sharing model
#### H. Per-language bit lists (chosen)
Each SDK owns an independent list; the decoder picks the list using the language
already present in the UA product token.
- Good, **no cross-language coordination**: each SDK numbers and evolves its
features independently; adding a Python feature never touches .NET numbering.
- Good, no null placeholders for one-SDK features, no "same bit, same meaning"
rule, no SDK-aware decode caveats.
- Good, decoding is trivial: language (from UA) + version -> list -> AND.
- Neutral, two small lists to maintain instead of one (but they were going to
diverge anyway — the packages differ).
#### I. Single shared cross-language registry
- Good, one list, one number space.
- Bad, forces synchronized numbering and null placeholders for features that
exist in only one SDK, plus SDK-aware decode rules.
- Bad, the synchronization is pure accidental complexity — **the language is
already in the User-Agent**, so sharing the number space buys nothing.
### Registry maintenance
#### J. Package-local indexes + parity/no-overlap test (chosen)
- Good, each package owns private `FeatureIndex` declarations only for its own
rows; adding an optional-provider index does not require a core release after
the marker API exists.
- Good, one repository test compares the package-local declarations with the
per-language table and rejects missing rows, wrong ids, out-of-range indexes,
and any duplicate/overlapping index.
- Good, no build step, no generator to own.
#### K. Code-generate the enums from the registry
- Bad, a generator + drift test + schema test to maintain a short list of
integer constants; likely justified only if v1 deliberately chooses the most
detailed L3/L4 granularities.
### Representation (how the mask is rendered as text)
All examples below encode the same mask — bits 0, 2, 32, 48, 56 set
(agent + workflow + sequential-orchestration + foundry.chat_client + openai, in
the Python v1 list) = decimal `72339073309605893`.
#### L. Decimal — `feat=v1.72339073309605893`
- Good, human-familiar; trivial to parse.
- Neutral, no visual alignment to four-bit groups; slightly longer than hex for
large masks. No advantage over hex.
#### M. Hex (chosen) — `feat=v1.101000100000005`
- Good, compact (≤32 chars for a 128-bit mask).
- Good, decodes with one stdlib call in every language (`int(x, 16)` /
two 64-bit lane parses in .NET); each hex character corresponds to four
consecutive bit positions.
- Good, lowercase, no `0x` prefix, no leading zeros — unambiguous and stable.
A grouped variant such as `feat=v1.101.0001.0000.0005` was also considered.
Separators make the value longer and must be removed before `int(x, 16)` can
parse it, while the ordinary hex digits already preserve fixed four-bit groups.
#### N. Binary — `feat=v1.100000001000000000000000100000000000000000000000000000101`
- Good, directly shows every zero/one position.
- Bad, grows to 128 payload characters and is difficult to scan reliably.
#### O. Bit-list — `feat=v1.0,2,32,48,56`
- Good, most directly human-readable ("which bits").
- Bad, needs delimiter handling and grows with the number of set bits; a full
128-bit list is substantially larger than every fixed-width representation.
#### P. Alphabet / base-N (e.g. Crockford base32 `feat=v1.208004000005`, base62 `feat=v1.5LJRx1i6xJ`)
- Good, shortest representation.
- Bad, needs a custom alphabet + decode table on both ends; base62 is
case-sensitive (fragile through case-normalizing intermediaries); not
directly readable. Premature optimization for a value that is already ≤32
chars in hex.
All forms are ASCII. The table shows total bytes added to the existing
User-Agent, including the leading space and `(feat=v1.)` wrapper:
| Representation | Example (5 bits) | All current Python rows (63) | All current .NET rows (52) | Full 128-bit v1 |
| --- | ---: | ---: | ---: | ---: |
| Hex | 26 | 34 | 30 | 43 |
| Grouped hex | 29 | 39 | 34 | 50 |
| Decimal | 28 | 38 | 34 | 50 |
| Binary | 68 | 100 | 86 | 139 |
| Bit-list | 23 | 189 | 156 | 412 |
| Crockford base32 | 23 | 29 | 26 | 37 |
| Base62 | 21 | 26 | 24 | 33 |
There is no defensible average before rollout, and the design does not depend on
one: a process-global mask may eventually contain every assigned row. There is
no smaller per-request bit budget because the bits are not request-scoped; the
registry allocation tenet controls how many distinctions v1 assigns. Client
processing is bounded by the fixed 128-bit width: marking performs one
lock/atomic OR, and request-time stamping reads the mask, formats at most 32 hex
characters, and replaces one User-Agent comment. It performs no registry scan,
network call, or per-feature enable/disable bookkeeping.
## Decision Outcome
Chosen: **a request-time-stamped, first-party-only User-Agent `(feat=...)` token (A),
with a 128-bit process-global monotonic accumulator (S1), per-language bit lists
(H), package-local index enums kept honest by parity and no-overlap tests (J),
rendered as lowercase hex (M).**
This is a bounded design with enough v1 headroom. A 128-bit
**process-global, monotonic** mask accumulates from universal
`mark_feature_used()` calls (so it spans build/start/participation activations
that aren't bound to any service request — the per-request set model (S2) can't);
the token is **stamped per request** only when both the client/pipeline and the
actual HTTPS origin are approved, so custom origins and cross-origin redirects
cannot inherit the fingerprint; each
SDK owns an independent bit list selected by the language already in the UA; the
mask is rendered as hex (`feat=v1.101000100000005`). The dedicated
`AGENT_FRAMEWORK_FEATURE_MASK_DISABLED` opt-out drops only the mask while
keeping the base SDK identity/version User-Agent. Python's existing
`AGENT_FRAMEWORK_USER_AGENT_DISABLED` continues to suppress its entire
contribution, including the mask; this decision does not introduce a matching
whole-User-Agent switch in .NET. OTel (C) is deferred — mainly because a
broadly-emitted span attribute would leak the fingerprint into the user's
general telemetry, against the first-party-only stance and would require
user-side OTel setup that may still not make the data available to us — but left
open behind the version prefix. Per-request scoping (S2), a shared registry (I),
codegen for the initial registry (K), and the decimal/grouped-hex/binary/bit-list/
base-N representations (L, M variant, N, O, P) are rejected as complexity or
length the problem does not require.
The remaining choice before implementation is the **v1 granularity level** among
F0-F4. This is a point-in-time decision: adding new bits later is easier than
removing or redefining them, because removals/redefinitions require a new
registry version and historical decode tables. For v1, prefer the least detailed
level that answers the known product/support questions so we do not force a v2
shortly after launch. The refreshed candidate registry uses **63 Python indexes and
52 .NET indexes**, leaving 65 and 76 positions respectively. That headroom supports
normal growth; it does not waive the registry's
[allocation tenet](../specs/feature-usage-bit-registry.md#allocation-tenet).
### Consequences
- Good, adds a bounded-cost usage signal with no new data flow and few moving
parts.
- Good, transparent (public registry, human-decodable token) and disabled by a
dedicated `AGENT_FRAMEWORK_FEATURE_MASK_DISABLED` mask-only opt-out. Python's
existing whole-User-Agent opt-out also suppresses the mask.
- Good, first-party-only + request-time stamping gives a live mask and no
third-party fingerprint leak.
- Good, 128 bits leaves useful v1 headroom; .NET remains lock-free by storing two
independently atomic 64-bit lanes; per-language lists remove all cross-language
sync; package-local enums avoid both codegen and provider→core release coupling.
- Neutral, the token's reach equals eligible framework-configured first-party
traffic; broader per-call signal (OTel) can be added later if needed.
- Neutral, every set bit is a repeated Boolean observation after first use;
request rows carrying it are not feature invocation counts.
- Neutral, v1 granularity is intentionally a separate choice; the registry should
start with fewer bits unless a more detailed bit answers a concrete question.
- Bad, each feature must add an activation mark, first-party clients need a
per-request destination-aware hook, and the registry validator must scan all
package-local index declarations.
## Prior art
SDK telemetry-in-the-User-Agent is well-established; this design is closest to
AWS's, and conventional in the rest. Summary of what comparable SDKs do:
| SDK | What's in the UA / headers | Usage-based? | Opt-out | Closest to ours? |
| --- | --- | --- | --- | --- |
| **AWS botocore** | structured UA with an `m/` token: a per-request set of **short feature codes** for features actually exercised (`WAITER``B`, `PAGINATOR``C`, retry mode, checksums, credential source, …) | **Yes** — registered at call time via `register_feature_id`, contextvar-scoped per request | `AWS_SDK_UA_APP_ID` sets app id (no opt-out for `m/`) | **Yes — direct analog** |
| **OpenAI / Anthropic** (Stainless) | sidecar `X-Stainless-*` headers: lang, package version, OS, arch, runtime, runtime version; plus per-request `x-stainless-retry-count`, `x-stainless-read-timeout` | Mostly static identity (retry/timeout are per-request) | none | No (static identity) |
| **Azure SDK** (`azure-core`) | `User-Agent: azsdk-python-{pkg}/{ver} Python/{pyver} ({platform})` | No | `AZURE_TELEMETRY_DISABLED` (tracing spans only, **not** the UA) | No |
| **Google API core** | `x-goog-api-client: gl-python/… grpc/… gax/… gapic/…` | No | none | No |
| **LangSmith** | `User-Agent: langsmith-py/{ver}`; usage lives in trace payloads | No (header) | opt-in via `LANGSMITH_TRACING_V2`/`LANGCHAIN_TRACING_V2`; `…HIDE_INPUTS/OUTPUTS` | No |
Takeaways that shaped (or validate) our choices:
- **AWS `m/` is the precedent for usage-based feature flags in a first-party
User-Agent.** It validates the core idea. Its key *difference* is the encoding:
AWS uses a **comma-separated set of 12 char short codes** (open-ended, no bit
coordination, but variable length), whereas we use a fixed-width **hex
bitmask** (compact, bounded, decode-by-AND, but needs per-language bit
allocation). We keep the bitmask for boundedness and trivial AND-decoding;
AWS's short-code set is recorded as a viable alternative if bit-position
coordination ever becomes painful (it would also drop the fixed 128-bit bound).
- **A fixed-width bitmask gives bounded token size for free.** botocore must cap
the `m/` component at 1024 bytes and truncate at delimiter boundaries (with a
fallback log) precisely *because* its short-code set is unbounded. Our 128-bit
hex is ≤32 chars by construction — no size cap, no truncation logic.
- **Scope is where we diverge most — and deliberately.** botocore collects
features into a per-request `contextvars` set that is **reset between
requests**, and no-ops outside a request context to prevent cross-request
bleed. That works because every botocore feature is exercised *inside* an AWS
service request. We are more general: some features are request-scoped (a chat
call, an MCP tool invocation) but many are **not bound to any request**
(workflow build/start, provider participation, hosting startup). So we use a
**process-global, monotonic** mask (option S1), which is the only scope that can
represent the non-request features. Our mask therefore intentionally "bleeds"
(accumulates) for the life of the process — the opposite of botocore's reset —
and that is the intended semantic, not the bug botocore guards against.
- **The mechanism is private; the wire format is the contract.** botocore marks
its whole user-agent module private and "subject to abrupt breaking changes."
Same for us: the Python/.NET helpers are internal, and only the emitted token +
the per-language registry tables are the stable, decodable contract.
- **First-party-only emission** is stricter than any of the above; the closest in
spirit is Stainless headers, which only reach the owning API. We make the
client/pipeline allowlist explicit (initially Foundry/Azure OpenAI) rather than
attempting to infer safety from arbitrary request URLs. Other Azure clients
join only after telemetry access is confirmed.
- **Opt-out naming.** `AZURE_TELEMETRY_DISABLED` is the family precedent for our
`AGENT_FRAMEWORK_*_DISABLED` names. Separately, the cross-tool `DO_NOT_TRACK`
convention (honored by e.g. HuggingFace Hub) is worth considering — see Open
Questions.
Sources: botocore [`useragent.py`](https://github.com/boto/botocore/blob/develop/botocore/useragent.py)
(`_USERAGENT_FEATURE_MAPPINGS`, `register_feature_id`, `_build_feature_metadata`);
openai-python [`_base_client.py` `platform_headers()`](https://github.com/openai/openai-python/blob/main/src/openai/_base_client.py);
anthropic-sdk-python [`_base_client.py`](https://github.com/anthropics/anthropic-sdk-python/blob/main/src/anthropic/_base_client.py);
azure-core [`_universal.py` `UserAgentPolicy`](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/core/azure-core/azure/core/pipeline/policies/_universal.py);
google-api-core [`client_info.py`](https://github.com/googleapis/python-api-core/blob/main/google/api_core/client_info.py);
langsmith-sdk [`client.py`](https://github.com/langchain-ai/langsmith-sdk/blob/main/python/langsmith/client.py) /
[`utils.py`](https://github.com/langchain-ai/langsmith-sdk/blob/main/python/langsmith/utils.py);
huggingface_hub [`constants.py`](https://github.com/huggingface/huggingface_hub/blob/main/src/huggingface_hub/constants.py).
## Registry versioning and migration (v1 → v2)
The token carries a **per-language** version (`feat=v1.<hex>`); a version bump is
independent for Python and .NET.
- **Additive growth stays on v1 — no bump.** Allocating a new feature to a
reserved/unused bit is backward-compatible: an older decoder simply sees an
unknown bit and ignores it. Normal package growth never needs a new
version.
- **A bump (v2) is required only for breaking changes:** renumbering or
re-partitioning existing bits, changing the *meaning* of an already-assigned
index, or widening beyond 128-bit. Within a version an index is **never** reused or
reassigned — that invariant is what lets old decoders stay correct.
- **The draft 64→128 change is still v1.** No v1 token or enum has shipped, so
this pre-implementation repartition establishes the initial contract rather
than migrating an existing one.
- **Mixed-version coexistence is the norm.** A fleet runs many SDK releases at
once, so `v1` and `v2` tokens appear simultaneously for a long time (old SDKs
keep emitting `v1`). The decoder keeps **every** published `(language,
version)` table and selects by the token's version; the `v1` table is retained
indefinitely for historical decode.
- **Unknown version → do not guess.** A decoder without the `vN` table must
record "unknown registry version" rather than decode against an older table —
bit meanings may differ across versions, so mis-attribution is worse than
no data.
- **Producing v2:** publish the v2 table alongside v1, update the affected
package-local `FeatureIndex` declarations and SDK version constant, and emit
`v2` from the release that ships them. Prefer staying on v1 (additive) and
reserving a clean v2 for an eventual deliberate re-partition.
## Limitations
| Limitation | Caused by (choice) | Why we accepted it |
| --- | --- | --- |
| **No signal for self-hosted or third-party-only traffic.** If a process never calls Azure/Foundry, we see nothing. | First-party-only emission (A) | We can't read third-party logs anyway, and must not leak a fingerprint into them. Reach traded for privacy. |
| **Not every first-party client is stampable.** Caller-supplied `AIProjectClient` / OpenAI clients and toolkit-owned clients may not expose a supported per-request policy hook. | Supported-hook-only emission (A) | V1 does not mutate caller-owned clients or private SDK pipelines. Those features may still appear on another eligible request from the same process-global mask. |
| **Custom origins intentionally receive no feature token.** A customer gateway may use Azure credentials or Azure-named settings but route to a non-approved origin. | Two-factor destination classification (A) | Credentials and configuration names are not proof of telemetry ownership. Unknown/custom origins and cross-origin redirects are denied by default. |
| **No OTel / per-call signal in v1.** | OTel deferred (C) — primarily on **privacy** and availability grounds | A broadly-emitted span attribute would push the fingerprint into the user's general telemetry / third-party APM vendors, undoing the first-party-only scoping. It also requires customer/user OTel setup, and even Foundry users may not export data where we can query it. Left open only if there is a compelling reason to add. |
| **Mask reflects "usage so far," not the whole session.** Early requests carry fewer bits than later ones. | Process-global accumulator + request-time stamping | Honest and still useful as a Boolean process-lifetime observation. Repeated request rows must not be summed as additional uses. Reading the mask at request time makes it *grow* rather than freeze. |
| **No per-agent / per-call attribution.** The mask is one process-wide value — "this process used X", not "this agent/call used X". | Process-global monotonic scope (S1) | A deliberate choice, not a transport limit: botocore *does* per-call attribution in the UA via a per-request `contextvars` set, but many AF activations (workflow build/start, provider participation, hosting startup) occur outside the service request that later emits the token. Per-call detail remains deferred to OTel. |
| **Shared processes intentionally carry usage across agents and tenants.** A request can include bits first set by another workload in the same worker. | Process-global monotonic scope (S1) | The token must be interpreted only as process-level "used so far," never as request/user/tenant attribution. Privacy review must explicitly accept this. |
| **Bits are binary, sticky observations — not countable events.** Once set, a bit appears on every later eligible request from that process, so raw request counts repeat the same observation and long-lived/high-traffic processes dominate. | Monotonic mask stamped at request time | The signal supports coarse observed-feature and co-occurrence questions only. It cannot provide first-use counts, unique-process counts, request attribution, or feature invocation frequency. |
| **Granularity may be too coarse or too detailed.** The chosen level may miss useful distinctions or create more specificity than needed. | v1 granularity choice (F0-F4) | This is the main remaining decision. Adding bits later is easier than removing/redefining them, so v1 should lean toward fewer bits that answer known questions. |
| **.NET snapshots span two atomic lanes.** A bit can be marked between the low/high reads, so one request may omit that just-added bit. | 128-bit width without a global lock | The mask is monotonic: the snapshot cannot invent or clear a bit, and the next request includes the addition. This matches the existing "usage so far" timing semantics. |
| **Fingerprinting risk is reduced, not eliminated.** A feature-combination mask is still a deployment signature, and it transits intermediaries (proxies/CDNs) even when first-party-scoped. | Emitting any feature-combination value | Scope + opt-out + coarse granularity mitigate it; v1 should avoid unnecessary detailed bits. |
## Open Questions (for decider discussion)
These are unresolved and should be decided before implementation:
1. **Which v1 granularity level (F0-F4)?** This is the primary remaining choice.
Adding bits later is easier than removing or redefining bits, so v1 should
choose the least detailed level that answers known questions and avoids a quick
v2.
2. **Privacy approval for the v1 User-Agent signal.** Before implementation,
confirm that a transparent, opt-out, first-party-only feature-combination
fingerprint is acceptable, including the exact client allowlist, retention,
access, and permitted product queries. This is a rollout precondition.
3. **When (if ever) to add the OTel path?** Held back mainly for **privacy** and
data availability: a span attribute broadcasts the fingerprint into the user's
general telemetry and onward to third-party APM vendors, contradicting the
first-party-only stance, and it requires user-side OTel setup that may not make
the data available to us even for Foundry users. It also carries a
metric-cardinality hazard. Revisit only if the User-Agent path cannot answer a
concrete question.
4. **Honor the cross-tool `DO_NOT_TRACK` convention?** Several ecosystems treat
`DO_NOT_TRACK=1` as a universal telemetry opt-out (HuggingFace Hub honors it;
see [Prior art](#prior-art)). Should our mask opt-out also respect
`DO_NOT_TRACK` (in addition to `AGENT_FRAMEWORK_FEATURE_MASK_DISABLED` and
Python's pre-existing whole-UA flag)? Cheap to add and
community-friendly, but it widens the opt-out surface and needs a clear
precedence rule. Recommend yes; confirm with the deciders.
### Decided
- **Dedicated opt-out flag — included.** In addition to the existing
Python `AGENT_FRAMEWORK_USER_AGENT_DISABLED` (drops the whole UA), v1 ships
`AGENT_FRAMEWORK_FEATURE_MASK_DISABLED`, which drops **only** the feature mask
while keeping the base SDK identity/version User-Agent. This lets a
privacy-conscious user withhold the usage signal without losing the
support/compat value of the SDK-version header. .NET adopts the dedicated
mask-only flag; adding a .NET whole-User-Agent switch is outside this decision.
- **Caller-owned clients are not modified.** V1 stamps only framework-created
clients or clients with a supported public policy/hook registration point. It
does not patch private pipelines; injected clients are an explicit coverage
limitation.
- **Destination approval is explicit and redirect-aware.** An eligible pipeline
still emits only to a reviewed HTTPS origin. Custom origins are default-deny,
and the token is removed on an unapproved redirect hop.
- **Telemetry does not replace transport defaults.** Framework-created OpenAI
clients use the SDK's default async HTTP client with the request hook added,
preserving redirect, timeout, connection-limit, and pooling behavior.
- **Marking uses activation, not DI construction.** Operational surfaces mark on
first real use; a constructor marks only when construction itself exercises or
registers the capability.
## More Information
- Mechanism & API: [SPEC-004](../specs/004-feature-usage-telemetry.md)
- Per-language bit tables, encoding, opt-out, governance: [feature-usage-bit-registry.md](../specs/feature-usage-bit-registry.md)
- Existing accumulator pattern: `python/packages/core/agent_framework/_telemetry.py`
- .NET emission policies: `dotnet/src/Microsoft.Agents.AI.Foundry/AgentFrameworkUserAgentPolicy.cs`,
`dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/HostedAgentUserAgentPolicy.cs`
@@ -1,308 +0,0 @@
---
status: proposed
contact: eavanvalkenburg
date: 2026-07-24
deciders: eavanvalkenburg, chetantoshnival, taochenosu, moonbox3, giles17
---
# Python session storage and serialization
## Context and Problem Statement
Python does not have a broadly shared session-store API in
`agent-framework-core`. The alpha `agent-framework-hosting` package has a small process-local `SessionStore`, but that
type is hosting-specific, in-memory only, and unavailable to packages such as Foundry Hosting without taking a
dependency on the hosting helper package.
The alpha implementation is a prototype, not a compatibility constraint. This decision may replace its location,
names, method shape, and behavior if another design is preferable.
The existing file-backed persistence surfaces solve narrower problems:
- `FileHistoryProvider` stores conversation `Message` records, not complete `AgentSession` snapshots;
- `FileCheckpointStorage` stores workflow checkpoints; and
- the Responses provider stores protocol history, but not Agent Framework runtime state carried in
`AgentSession.state`.
`AgentSession.to_dict()` / `from_dict()` already provide a dictionary snapshot shape. Session state may contain
framework or application-defined objects, and `register_state_type` provides dynamic type restoration, but the
registration and collision behavior is not yet strong enough to serve as a durable, cold-start persistence contract.
The framework therefore needs to decide:
- where a reusable in-memory and file-backed session store belongs;
- how a complete `AgentSession` should be serialized atomically and validated;
- how custom nested state types are registered and restored after process restart; and
- how to provide the required readable JSON format while leaving room for an optional optimized binary format.
## Decision Drivers
### Session-store ownership and API
- Make session storage reusable by core, hosting, and provider packages without creating dependency cycles.
- Keep the smallest public API that supports in-memory use, durable implementations, and application-defined stores.
- Define the minimum async operations required for lookup, replacement, and deletion.
- Decide explicitly whether reads return shared instances or independent snapshots suitable for branching.
- Simpler is better
### Serialization and type restoration
- Provide readable JSON serialization as a required capability.
- Treat an optimized binary format as a nice-to-have only when the chosen JSON implementation supports it without a
separate state model or substantial additional complexity.
- Perform one typed encode and decode operation per file write/read.
- Preserve dynamic registration of nested state types by the provider modules that own them.
- Fail before persistence when an object cannot be restored after a cold start.
- Keep the existing serialized `{"type": "<id>", ...}` representation compatible.
## Decision 1: Session-store ownership and API shape
### Keep `SessionStore` in `agent-framework-hosting`
- Good: keeps the abstraction local to app-owned hosting scenarios.
- Bad: Foundry Hosting and other packages cannot reuse it without depending on the hosting helper package.
- Bad: a generic session snapshot store is not inherently or only a web-hosting concern.
- Bad: durable implementations would either be duplicated or placed in an unrelated package.
### Add an abstract store plus separate in-memory and file implementations
For example, define a `SessionStore` protocol/ABC with `InMemorySessionStore` and `FileSessionStore`.
- Good: clearly separates the contract from implementations.
- Good: implementation names state their storage behavior explicitly.
- Neutral: follows a familiar repository/adapter pattern.
- Bad: introduces an additional public type and rename for a three-method experimental API.
- Bad: callers must choose an implementation even for the default in-memory case.
- Bad: the abstraction adds little value while every implementation still needs the same method overrides.
### Move the concrete store to core and use it as the overridable base
Move `SessionStore` to `agent-framework-core`, retain its in-memory behavior, and implement `FileSessionStore` by
overriding the same async methods.
- Good: one public type is both the useful default and the extension point.
- Good: existing custom stores can continue subclassing and overriding `get` / `set` / `delete`.
- Good: core and provider packages can share the API without depending on hosting helpers.
- Good: `FileSessionStore` remains a focused subclass while the base stays free of file-system concerns.
- Bad: the class name does not explicitly say "in memory" when used without overrides.
## Decision 2: Serialization and type restoration
Once a file-backed store exists, it needs an on-disk format and a reliable way to reconstruct the complete
`AgentSession`, including nested framework and application-defined state. Serialization belongs to each durable store
implementation rather than the `SessionStore` API: the default in-memory store does not serialize, and custom stores
remain free to choose another protocol.
The alternatives below compare top-level snapshot validation, JSON encoding/decoding cost, and how each option
interacts with the dynamic custom-state registry. Binary storage is not a primary selection criterion.
### Considered options
The standard-library and optimized-JSON options are not mutually exclusive. A store can default to `json` while
accepting caller-supplied `dumps` / `loads` callables for `orjson` or another compatible implementation. This is the
pre-msgspec `FileHistoryProvider` design; those hooks remain only as a deprecated compatibility path.
### Standard library `json`
- Good: no additional dependency and familiar readable output.
- Good: accepts the existing dictionary snapshots without a schema.
- Good: can remain the fallback/default behind pluggable `dumps` / `loads`.
- Neutral: custom state restoration still requires the framework registry.
- Bad: slower encoding and decoding than optimized native implementations.
- Bad: provides no typed snapshot validation during file reads.
### Optimized drop-in JSON libraries such as `orjson`
- Good: substantially faster JSON encoding and decoding than the standard library.
- Good: can preserve the existing dictionary-oriented snapshot and custom `dumps` / `loads` shape.
- Good: can be an opt-in codec without making the optimized package a framework dependency.
- Neutral: returns bytes when encoding, which the file stores can already handle.
- Neutral: custom state restoration still requires the framework registry.
- Bad: remains an untyped top-level decode; the framework must separately validate the session snapshot shape.
- Bad: choosing one drop-in implementation as a core dependency adds a dependency without providing typed construction.
### Pydantic `model_dump` / `model_validate`
- Good: Pydantic is already a core dependency.
- Good: a typed session snapshot model can validate top-level fields and provide `model_dump_json` /
`model_validate_json` for file serialization.
- Good: validation errors include useful field paths.
- Neutral: the dynamic `state` field remains `dict[str, Any]`, so custom nested state restoration still requires the
framework registry.
- Neutral: the public `AgentSession` does not need to become a Pydantic model; an internal snapshot model can bridge it.
- Bad: benchmarked encode/decode includes model construction and dumping overhead on every operation.
- Bad: core dependency on Pydantic run the risk of us not being able to use different versions or users of the framework being unable to upgrade or having additional extra code dealing with major version bumps in Pydantic.
### msgspec typed/tagged unions only
- Good: msgspec owns validation and reconstruction end to end.
- Neutral: works well for a closed set of framework-owned `msgspec.Struct` types.
- Bad: every external type must be known when the decoder schema is constructed; dynamic registration is lost.
### msgspec codecs plus an explicit dynamic registry
- Good: one typed file encode/decode and dynamic nested custom types.
- Good: it satisfies the required readable JSON format.
- Neutral: the same typed snapshot can also support optional MessagePack as a low-cost implementation detail.
- Good: the registry can enforce stable IDs, codec completeness, and collision handling.
- Neutral: a single state-payload hook still recursively applies registry codecs.
- Bad: msgspec cannot infer dynamic types from JSON without the framework's type tags.
## Benchmark Evidence
A benchmark using a large `AgentSession` with 2,000 `Message` objects stored through
`InMemoryHistoryProvider`, nested standard dictionaries, registered custom classes, and registered Pydantic models
measured the complete `AgentSession.to_dict()` / codec / `AgentSession.from_dict()` path.
The reproducible harness is
[`python/scripts/session_serialization_benchmark.py`](../../python/scripts/session_serialization_benchmark.py):
```bash
cd python
uv run --with orjson python scripts/session_serialization_benchmark.py
```
| Codec | File size | Encode median (ms) | Decode median (ms) | Round-trip median (ms) | Disk round-trip median (ms) |
| --- | ---: | ---: | ---: | ---: | ---: |
| Standard library JSON | 1.57 MiB | 33.503 | 14.316 | 55.261 | 75.226 |
| orjson | 1.57 MiB | 25.808 | 11.754 | 39.398 | 63.319 |
| Pydantic JSON | 1.57 MiB | 28.330 | 18.344 | 53.522 | 77.096 |
| msgspec JSON | 1.57 MiB | 26.019 | 11.379 | **38.060** | 62.230 |
| msgspec MessagePack | **1.45 MiB** | **25.134** | **11.201** | 38.512 | **58.112** |
The JSON encodings produced the same 1.57 MiB file size. msgspec JSON had the best median JSON round-trip latency,
slightly ahead of orjson, while also supporting typed top-level decoding. Pydantic validation added measurable decode
and disk-round-trip overhead without eliminating the dynamic state registry.
MessagePack reduced file size to 92.2% of JSON (about 7.8% smaller) and produced the best encode, decode, and disk
round-trip medians. Its in-memory round-trip median was effectively tied with msgspec JSON. This supports offering it
as a nice-to-have, but it is not required to justify choosing msgspec for JSON.
These results are workload- and machine-dependent. The small differences between optimized JSON implementations are
not the basis for the architectural choice. The benchmark instead confirms that the typed design does not impose a
material regression for this representative payload:
- use msgspec JSON as the readable default;
- optionally offer msgspec MessagePack when storage size or disk latency matters;
- retain the explicit registry for dynamic custom state in both formats;
- do not add orjson solely for a small JSON performance difference without typed decoding; and
- do not use Pydantic as the file codec when its validation overhead does not replace the registry.
## Decision Outcome
### Decision 1: Move the concrete overridable store to core
`SessionStore` moves to `agent-framework-core` as an experimental public API. It remains a concrete in-memory store and
the default used by `AgentState` in the `hosting` package. Its async `get`, `set`, and `delete` methods remain overridable for custom storage
implementations.
`FileSessionStore` subclasses `SessionStore` and provides durable atomic file persistence. No separate
`InMemorySessionStore`, protocol, or ABC is introduced. `agent-framework-hosting` consumes the core type and no longer
owns or re-exports `SessionStore` (this will be a breaking change in the `hosting` package).
Actual `SessionStore` and `FileSessionStore` operations mark Python feature-usage index 17,
`core.session_store`, following ADR-0033's use-not-presence policy. Construction and import alone do not mark the bit.
`SessionStore` accepts opaque non-empty keys so custom backends can use their native key contracts. `FileSessionStore`
accepts opaque keys up to 128 characters and encodes values that are not portable filename stems; this supports
provider IDs such as `telegram:<bot-id>:<chat-id>` without permitting path traversal. `AgentState` remains
storage-agnostic and passes keys through unchanged; each store implementation owns backend-specific validation or
normalization. Protocol-specific hosts such as Foundry may still derive their own stable storage key before calling the
store.
Foundry Hosting exposes an experimental `FoundrySessionStore`, which is the
default `ResponsesHostServer` store when hosted; local hosting defaults to the
in-memory `SessionStore`. `FoundrySessionStore` currently subclasses
`FileSessionStore`, stores snapshots under
`/.sessions/<user-id>/<conversation-id-or-response-id>.json`, and derives the
validated user partition from
`azure.ai.agentserver.core.get_request_context()`. A Foundry session controls
hosted compute and filesystem lifetime and may host multiple users and
Responses conversations, so its ID is not used as the MAF session identifier.
Stored-conversation requests read and write one snapshot under
`conversation_id`. Response-chain requests read under `previous_response_id`
and write the updated, loaded MAF session under the current `response_id`, which
allows branching without overwriting the parent snapshot. Because Foundry does
not infer `agent_session_id` from `previous_response_id`, response-chain callers
must also reuse the prior response's hosted session ID so the request reaches
the same persistent `$HOME`; conversation objects bind a stable hosted session
automatically.
The Foundry-specific type is the host configuration seam; its implementation
may later move from files to a Foundry storage API without changing the generic
core store contract. The session file API maps `/` to the hosted `$HOME`
directory, so this API path is persisted on disk under `$HOME/.sessions`.
### Decision 2: Use msgspec codecs plus an explicit dynamic registry
Chosen option: **msgspec codecs plus an explicit dynamic registry**.
`FileSessionStore` uses a typed internal `msgspec.Struct` snapshot with reusable JSON and MessagePack encoders/decoders.
JSON is the required and default format. Because msgspec can reuse the same typed snapshot and registry hooks,
`serialization_format="msgpack"` is also exposed as an optional compact binary convenience. The complete state
dictionary is wrapped in one custom field; its encode/decode hooks recursively translate explicitly registered types
to and from the existing tagged mappings in either format.
The dependency range is `msgspec>=0.20.0,<0.22`: version 0.20.0 added Python 3.14 support, and the upper bound limits
core to the tested 0.20/0.21 minor lines.
Three dependency placements were considered:
1. Make msgspec a standard core dependency.
2. Make msgspec optional in core but standard in Foundry hosting.
3. Make msgspec optional in both packages.
Option 3 moves installation failures to application developers even though durable session persistence is required for
the primary `ResponsesHostServer` API to preserve Agent Framework state. Option 2 removes that burden from Foundry
hosting but makes core's shared `_sessions` module and public types conditionally defined or lazily imported without
removing msgspec from the default Foundry installation. Option 1 is therefore selected: msgspec is a standard core
dependency, giving both core file providers and Foundry hosting one predictable implementation path.
Core already depends on the native `pydantic-core` extension, so native-wheel availability is not a new packaging
constraint. The msgspec project is also actively tracking upcoming Python support; its merged
[`Add 3.15-dev to CI` PR](https://github.com/msgspec/msgspec/pull/1037) exercises Python 3.15 development builds. This gives confidence that they will add support for new python version quickly.
The public `AgentSession` remains a normal framework class. The msgspec Struct is an internal persistence DTO rather
than the inheritance base for runtime sessions. The Struct gives persistence one typed encode/decode operation, validates
the snapshot envelope, and carries an explicit payload version. The benchmark's small timing spread was not used to
choose the Struct.
`register_state_type` supports stable type IDs and optional codecs, rejects collisions, and provides defaults for
`to_dict` / `from_dict` classes and Pydantic models. Type IDs share one process-wide registry, so provider packages
should use stable package-qualified identifiers and register their own state types at module import time; consumers do
not need to know those implementation details. One recursive serializer is shared by `AgentSession.to_dict()` and the
durable codecs. The established implicit Pydantic registration behavior remains temporarily for compatibility, but now
emits `DeprecationWarning`. Same-process round-trips continue to work; cold-start deserialization is not guaranteed
without explicit provider registration. Unknown persisted type IDs remain raw dictionaries.
File snapshots are quarantined only when their bytes cannot be parsed as the selected JSON or MessagePack format.
Schema errors, unsupported snapshot versions, and registered state-decoder failures leave the original file in place so
an application fix, rollback, or compatible reader can recover it.
`FileHistoryProvider` also adds msgspec JSON as its default JSON Lines codec. It supports the same explicit
`serialization_format="msgpack"` choice using length-prefixed append-only MessagePack records. Its existing `dumps` /
`loads` extension points remain temporarily for JSON compatibility, emit `DeprecationWarning` when supplied, and do
not apply to MessagePack. New code uses the built-in codecs. The default JSON reader falls back to the standard library
for legacy JSON Lines containing `NaN` or infinity, and writes those non-finite values with the standard library so
existing history semantics are preserved.
## Follow-up Work
Audit the remaining file-backed stores to determine whether they benefit from the same typed msgspec treatment and
optional JSON / MessagePack formats. `FileCheckpointStorage` is the first candidate because it persists large,
structured workflow state and currently uses JSON plus custom checkpoint value encoding. Its existing
`WorkflowCheckpoint.version` field already provides a payload-shape discriminator.
Checkpoint migration should be reader-first. A compatibility release can detect the codec from the first byte, widen
the two `glob("*.json")` readers to discover future formats, and continue writing only JSON. A later release can add
opt-in MessagePack writes while retaining JSON as the default. The payload `version` should describe the checkpoint
shape rather than the codec, which is discoverable from the bytes. MessagePack should not become the default while
mixed-version fleets may share one checkpoint directory: older readers silently ignore non-JSON files and could resume
from no checkpoint instead of surfacing an incompatibility.
`MemoryContextProvider` is another candidate because its file-backed path combines `MemoryFileStore` state with
transcript files and still exposes `history_dumps` / `history_loads` passthroughs to the deprecated
`FileHistoryProvider` codec hooks.
The follow-up should measure real framework payloads before changing formats, preserve compatibility or define a clear
migration path for existing files, and consider whether each store needs readable JSON, compact binary storage, append
semantics, or atomic whole-file replacement. Other candidates include file-backed todo state, but each should be
evaluated independently rather than adopting msgspec by default solely for consistency.
@@ -1,42 +0,0 @@
---
status: proposed
contact: MohammadHaroonAbuomar
date: 2026-08-07
deciders: agent-framework .NET maintainers
---
# .NET agent-hooks enforcement: composed factory over three seams
## Context and Problem Statement
The [AGENT-HOOKS-0.1](https://github.com/responsibleai/agent-hooks) interception contract shipped for Python as a first-class experimental core feature (#7515): a middleware bundle emitting eight interception points with three-verdict, fail-closed enforcement, transform write-back, buffered streaming, and verdict-before-durability persistence gating. The .NET side needs the same semantics, but the .NET framework has no category-based middleware lists — interception is decorator composition (`DelegatingAIAgent`, Microsoft.Extensions.AI `DelegatingChatClient`, the function-invocation middleware seam). How should the contract's indivisibility and enforcement properties be realized in that model?
## Decision Drivers
- Identical enforcement semantics to the merged Python feature (same spec, same fail-closed rules), diverging only where the .NET seam model requires it — never by weakening an enforcement property.
- Partial installation of the enforcement must be impossible or loudly rejected, not silently degraded.
- Denied content must never become durable; transformed content must persist post-transform.
- No changes to existing framework source; the optional native-runtime dependency (`ResponsibleAI.AgentHooks`) must not be referenced by core packages.
## Decision Outcome
**A single factory (`AsAIAgentWithAgentHooks`, per-run and host-owned-session overloads) in a new package `Microsoft.Agents.AI.AgentHooks` composes the full enforcement itself** instead of exposing middleware values:
- **Seam order (fixed by construction):** `AgentHooksAgent` (agent seam: `agent_startup`/`input`/`output`/`agent_shutdown`, per-run `AsyncLocal` state, buffered streaming, persistence gate) → framework function-invocation middleware (`pre_tool_call`/`post_tool_call`) → `ChatClientAgent` with its default pipeline → `AgentHooksChatClient` **below** `FunctionInvokingChatClient` (so `pre_model_call`/`post_model_call` bracket every model service call of the tool loop individually).
- **Indivisibility:** the seam decorators are `internal`; only the factory composes them. Two pipeline-replacement affordances of `ChatClientAgent` are rejected loudly (fail closed): a caller-supplied per-run `ChatClientFactory` (the framework's own function-middleware factory is recognized and allowed — it wraps, not replaces), and a supplied chat client that already contains a `FunctionInvokingChatClient` (it would execute tools below the verdicts).
- **Verdict-before-durability:** end-of-run history and context-provider writes defer behind the `output` verdict via gating provider wrappers installed by the factory (dropped on deny, flushed post-transform with verdicted-message substitution for streamed runs). The implicit default `InMemoryChatHistoryProvider` is materialized and gated, with the history-conflict flags set to mimic implicit-default semantics. Per-service-call persistence sits above the chat seam, so it is covered by its own `post_model_call` verdict. Per-run provider overrides are wrapped in both `AdditionalProperties` dictionaries, copy-on-write. Nested agents persist inline at their own boundaries (they have their own providers) — no run-identity bookkeeping is needed, unlike Python.
- **Fail-closed error behavior:** interceptor crashes/timeouts surface as `host_error:*` denies; enforcement-layer failures at the tool seam halt the run through `FunctionInvocationContext.Terminate` (the loop's only loud escape — thrown exceptions are converted to tool errors by the loop, which would fail open); wire projections run inside the guarded blocks; failure notifications to providers are redacted (empty request messages) once a deny/halt stands.
- **Streaming:** fully buffered per the spec's `buffered_output` semantics — zero egress ahead of a verdict; transformed responses re-derive the released updates (preserving continuation tokens) so egress never diverges from verdicted content.
### Considered Alternatives
- **Port Python's middleware-value model (a `MiddlewareBundle` type):** rejected — .NET has no middleware list to put a bundle into; indivisibility via runtime validation is weaker than construction ownership.
- **Core-framework persistence gate (as Python added in `_sessions.py`):** rejected — unnecessary in .NET; construction ownership of the provider instances gives the same property with zero core changes.
- **Per-run `ChatClientFactory` as the chat-seam install point:** rejected — it wraps the whole pipeline above the function-invocation loop, so per-model-call points would be impossible.
## Consequences
- Good: zero existing-source changes; the optional native dependency is isolated in one leaf package; enforcement properties are structural rather than convention-based.
- Accepted: the package ships in the release solution filter as an **alpha** package (maintainer decision on the PR) — the version suffix follows the maturity of the `ResponsibleAI.AgentHooks` dependency it is built on, and the whole surface stays `[Experimental]`; a sample follows once the API shape settles.
- Known limitations (documented on the factory): hosted (service-executed) tools never reach the function seam and are intercepted via the `post_model_call` content projection; service-managed (conversation-id) history is durable at the service and ungateable; the deferred-OTel decorator sits above the chat seam, so sensitive-data request spans observe pre-transform content; a chat-seam projection failure fails the run closed but without a synthesized `host_error` record (SDK affordance gap, responsibleai/agent-hooks#70).
- The trust model is the spec's: cooperative contract, not a security boundary — the misuse rejections catch accidental foot-guns loudly, not in-process adversaries.
@@ -1,191 +0,0 @@
---
status: proposed
contact: rogerbarreto
date: 2026-08-21
deciders: rogerbarreto
consulted: Tao Chen, Sergey M., Ben Thomas, Shanmukha
informed: Agent Framework .NET team
---
# Resilient long-running agents in Microsoft.Agents.AI.Foundry.Hosting
## Context and Problem Statement
The Foundry Hosted Agents platform can run a hosted agent as a long job that continues when no
client is connected, and that the platform restarts after the container crashes or is recycled.
On restart the platform re-invokes the handler with the same input, sets `ResponseContext.IsRecovery`
to true, and supplies the last durable `ResponseObject` snapshot as `PersistedResponse`. The
snapshot is not itself a workflow checkpoint. For workflow agents, hosting records the ID of the
matching workflow checkpoint inside AgentServer internal response metadata before it persists the
response snapshot.
This applies only to **background** requests (`background=true`) whose `store` value is omitted or
true. Omitted `store` uses the Responses API default of true. Foreground requests and explicit
`store=false` requests have no crash-recovery contract.
Python currently supports resilient background execution for workflow agents and steering for
single agents. .NET hosting must offer the same opt-in capabilities on top of the durable session
and checkpoint storage introduced for Foundry state stores (PR #7649).
## Decision Drivers
- Match the Python recovery contract.
- Pair each persisted workflow response snapshot with the exact workflow checkpoint it represents.
- Opt-in and off by default; non-resilient hosts pay nothing.
- Prefer workflows: they already checkpoint between supersteps.
- Keep a lean API on `FoundryResponsesOptions`, forwarded to `ResponsesServerOptions`.
- Persist agent sessions through the Foundry state store (or its local fallback), not a second disk layout.
## Decision Outcome
Chosen option: **turn resilience on through the existing handler and registration path**.
### Public surface
`FoundryResponsesOptions.ResilientBackground` and `FoundryResponsesOptions.SteerableConversations`
are forwarded to `ResponsesServerOptions` so the AgentServer SDK enables recovery and steering.
This forwarding must happen in the callback passed directly to `AddResponsesServer`. The SDK makes
two process-level choices during that registration call: whether local SSE replay uses durable
storage and whether the conversation task accepts steering. Configuring the options only through
the later `IOptions` pipeline is too late for those choices.
The first `AddFoundryResponses` call owns this host-level configuration. Repeated calls do not
register another Responses server or redefine its resilience mode. Later calls can still configure
MAF-only options such as `AllowStoredOutputEnabled`; attempting to enable an AgentServer task
feature after the first call fails immediately instead of leaving AgentServer and MAF with
different settings.
```csharp
builder.Services.AddFoundryResponses(agent, configure: o => o.ResilientBackground = true);
```
### Handler contract on recovery
When `IsRecovery` is true:
1. Seed `ResponseEventStream` from the `PersistedResponse` that AgentServer provides. This preserves
its response fields, completed output items, and internal metadata.
2. When the snapshot contains `_last_checkpoint_id` and a persisted workflow `AgentSession` was
restored, select that exact checkpoint as the workflow resume point. This prevents a newer
checkpoint already present in workflow storage from being combined with an older response
snapshot. Foundry Hosting obtains the experimental `WorkflowSessionCheckpointRecovery` service
from the restored `AgentSession`; the internal `WorkflowSession` remains hidden. The resumed run
continues the work already queued in that checkpoint without sending a new `TurnToken` to the
start executor.
3. When `_last_checkpoint_id` is absent, retain the checkpoint already referenced by the restored
session. This covers a crash after the workflow wrote its first checkpoint but before AgentServer
persisted the first paired response snapshot. If the process stopped before the first session
save, no resumable MAF state exists, so the handler re-injects the original input instead of
invoking a fresh session with no messages. A regular agent has no equivalent within-turn workflow
checkpoint, so recovery remains best-effort and depends on its serialized session state.
4. On graceful shutdown of a resilient turn, call `ExitForRecoveryAsync` instead of emitting
incomplete. The AgentServer shutdown token is linked to the token passed into the MAF agent so
long-running model, tool, and workflow operations stop promptly. The handler also checks
`IsShutdownRequested` after each agent update, because an agent may consume cancellation and
return normally instead of throwing. If shutdown becomes visible after the agent advanced but
before the corresponding event was emitted, the final session save is skipped. Recovery uses
the last session snapshot that corresponds to output already handed to AgentServer.
5. For non-workflow agents, best-effort save the agent session after each
`ResponseOutputItemDoneEvent`, with an authoritative end-of-turn save in `finally` (skipped when
the turn failed). Workflow agents use only the paired superstep path below for incremental saves,
so their persisted session cannot advance independently through ordinary output-item saves.
### Workflow response checkpoint alignment
When `OutputConverter` receives a `SuperStepCompletedEvent` with a new workflow checkpoint ID:
1. Close any response output item still open for that superstep.
2. Compare the new ID with `_last_checkpoint_id` in `ResponseEventStream.InternalMetadata`. If they
match, do nothing.
3. Save the `AgentSession` that references the new workflow checkpoint. If this save fails, keep the
prior response snapshot and metadata. The turn continues, and a later workflow checkpoint or the
final save can try again.
4. Write the new ID to `_last_checkpoint_id`.
5. Emit `response.in_progress` with the updated response state. AgentServer beta.8 tracks a
separate authoritative response object, so this event copies the internal metadata into the
snapshot that its checkpoint operation persists. The reserved metadata remains stripped from
client payloads.
6. Yield `ResponseEventStream.Checkpoint()`. AgentServer persists the response snapshot before it
resumes the handler.
The workflow checkpoint itself is already durable before `SuperStepCompletedEvent` is emitted. The
session save and response checkpoint therefore establish a recoverable boundary with three matching
parts: completed response output, serialized session state, and workflow checkpoint ID.
If a crash occurs after the workflow creates a newer checkpoint but before the next response
checkpoint, recovery deliberately uses the older ID from `PersistedResponse`. The workflow may
repeat work after that older boundary, but it does not duplicate output already present in the
response snapshot or lose output by resuming ahead of it.
### Handler contract on steering
When a second input arrives for an active steerable conversation:
1. AgentServer returns a response with `status=queued`, records the input, increments
`PendingInputCount` on the active handler context, and signals that handler's cancellation token.
2. The superseded handler invocation has `IsSteeredTurn=false`. If a cancellation-aware MAF
operation throws `OperationCanceledException`, Foundry Hosting uses `PendingInputCount > 0` to
distinguish steering from shutdown and client cancellation.
3. Foundry Hosting completes the superseded response cleanly and saves its `AgentSession` with a
non-cancelled save token. This gives the queued turn the latest committed MAF state.
4. AgentServer invokes the handler again with `IsSteeredTurn=true`. This is not crash recovery:
`IsRecovery=false`, so the new input is converted to MAF messages normally. The same
`conversation_id` resolves the same persisted `AgentSession`.
No special MAF branch is required merely because `IsSteeredTurn=true`. The classification is
available for handlers that need different application behavior; the generic adapter treats the
drained input as the next normal turn on the same session.
Steering does not create a response checkpoint merely because another input was queued. Completed
workflow supersteps have already been paired with response checkpoints. An interrupted superstep
has no new `SuperStepCompletedEvent`, so its partial output and session state do not advance the
paired recovery boundary. The superseded response still reaches a terminal `completed` event.
### State ownership
| State | Owner | Recovery purpose |
|---|---|---|
| Resilient task, SSE events, `ResponseObject` snapshots, `_last_checkpoint_id` | AgentServer | Re-invoke the handler and identify the workflow checkpoint represented by each response snapshot |
| Serialized `AgentSession` | Foundry Hosting | Restore agent-owned state and the workflow checkpoint reference |
| Workflow execution checkpoints | Workflow runtime through `FoundryJsonCheckpointStore` | Restore executors, queued messages, pending requests, and workflow state |
The handler calls `ResponseEventStream.Checkpoint()` only after a workflow superstep supplies a new
checkpoint ID and the matching `AgentSession` save succeeds. `PersistedResponse.Output.Count` is not
the workflow cursor. `_last_checkpoint_id` is the explicit link between the response snapshot and
workflow storage.
### Relationship to durable storage (PR #7649)
Sessions and workflow checkpoints already go through `FoundryAgentSessionStore` /
`FoundryJsonCheckpointStore`. AgentServer separately owns resilient task records, response snapshots,
and SSE event replay. Resilience does not invent another store; it coordinates handler re-entry with
the existing session and workflow stores.
## Consequences
- Samples: `Hosted-Workflow-Resilient`, `Hosted-Workflow-Resilient-Long-Running`, and
`Hosted-Steering`.
- `Using-E2E-Resilience` runs the complete local crash-recovery demonstration in one console:
it consumes the server through a MAF agent created by `AIProjectClient`, force-kills the process,
restarts it, reconnects with a sequence-aware `ResponseContinuationToken`, then uses a third call
on the same agent and session without a sequence cursor to replay the full stream. It validates
the exact final countdown against the client accumulator and cursor-free replay.
- Handler-level tests cover recovery input skip, consumption of an available response snapshot,
response checkpoint deduplication by workflow checkpoint ID, and session-save failure that keeps
the prior paired boundary.
- A local two-lifetime integration test starts a real Responses host, persists a MAF
`AgentSession`, stops the host, starts a new host over the same local AgentServer state, and
verifies that the same response completes without re-injecting the original input.
- A deterministic countdown recovery test interrupts a workflow after outputs `6`, `5`, and `4`,
starts a new host, and verifies the final output is exactly `6`, `5`, `4`, `3`, `2`, `1`,
`Countdown complete.` with no missing or duplicated items.
- A local steering integration test sends two real HTTP turns through AgentServer and the MAF
adapter. It verifies `queued`, serial execution, delivery of the steering input, and reuse of the
persisted session.
- Live Foundry tests cover background continuation without client traffic, hard process
termination through `Environment.Exit`, recovery in a different process incarnation, transient
`404`/`424` polling responses during replacement, and long-running steering on the same
conversation.
- The checkpoint-index optimistic-concurrency retry count is configurable through
`FoundryJsonCheckpointStore`, with a default of eight attempts.
- Package floor: Azure.AI.AgentServer Core beta.28, Invocations beta.6, Responses beta.8.
@@ -8,8 +8,6 @@
- **Context Provider Pattern** - `SecureAgentConfig` extends `ContextProvider`, injecting tools, instructions, and middleware automatically
- **Automatic Variable Hiding** - UNTRUSTED content is automatically hidden without requiring manual intervention
- **Per-Item Embedded Labels** - Tools return `list[Content]` with `Content.from_text()` for proper label propagation
- **SecureMCPToolProxy Auto-Labeling** - MCP tools are labeled automatically from MCP `ToolAnnotations` hints
- **MCP `_meta.ifc` Support** - Per-result IFC labels from servers (for example GitHub MCP with `X-MCP-Features: ifc_labels`) are parsed and enforced
- **SecureAgentConfig** - One-line secure agent configuration via `context_providers=[config]`
- **Data Exfiltration Prevention** - `max_allowed_confidentiality` prevents sensitive data leakage
- **Message-Level Label Tracking** (Phase 1) - Track labels on every message in the conversation
@@ -25,7 +23,6 @@ The FIDES defense system consists of seven main components:
5. **Security Tools** - Specialized tools for safe handling of untrusted content (`quarantined_llm`, `inspect_variable`)
6. **SecureAgentConfig** - Context provider for easy secure agent configuration
7. **Message-Level Label Tracking** - Track labels on every message in the conversation (Phase 1)
8. **MCP Tool/Result Label Integration** - MCP hint-based tool labeling and `_meta.ifc` result label parsing
## Implementation Details
@@ -187,17 +184,6 @@ agent = Agent(
)
```
### 7. MCP Labeling Pipeline (Hints + `_meta.ifc`)
FIDES now secures remote MCP integration end-to-end:
- **Tool labels from hints**: `apply_mcp_security_labels(...)` maps MCP hints (`readOnlyHint`, `openWorldHint`) to FIDES tool properties.
- **Safe sink defaults**: tools not explicitly marked `readOnlyHint=True` are treated as potential sinks and receive `max_allowed_confidentiality=public`.
- **Result labels from metadata**: MCP result `_meta` is propagated via `__mcp_result_meta__`; `_meta.ifc` is parsed into `security_label` per result item.
- **`SecureMCPToolProxy` convenience**: wraps MCP tools/URLs and applies this labeling automatically on connect.
This behavior is used with the GitHub MCP server when `X-MCP-Features: ifc_labels` is passed, which causes the server to return IFC labels in `_meta` (for example `{"ifc": {"integrity": "untrusted", "confidentiality": "public"}}`).
## Security Properties
### Deterministic Defense
+48
View File
@@ -0,0 +1,48 @@
# AGENTS.md
Instructions for AI coding agents working on durable agents documentation.
## Scope
This directory contains feature documentation for the durable agents integration. The source code and samples live elsewhere:
- .NET implementation: `dotnet/src/Microsoft.Agents.AI.DurableTask/` and `dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/`
- Python implementation: `python/packages/durabletask/` and `python/packages/azurefunctions/` (package `agent-framework-azurefunctions`)
- .NET samples: `dotnet/samples/04-hosting/DurableAgents/`
- Python samples: `python/samples/04-hosting/durabletask/`
- Official docs (Microsoft Learn): <https://learn.microsoft.com/agent-framework/integrations/azure-functions>
## Document structure
| File | Purpose |
| --- | --- |
| `README.md` | Main technical overview: architecture, hosting models, orchestration patterns, and links to samples. |
| `durable-agents-ttl.md` | Deep-dive on session Time-To-Live (TTL) configuration and behavior. |
Add new sibling documents when a topic is too detailed for the README (e.g., a new feature like reliable streaming or MCP tool exposure). Keep the README focused on orientation and link out to siblings for depth.
## Writing guidelines
- **Audience**: Developers already familiar with the Microsoft Agent Framework who want to understand what durability adds and how to use it.
- **Host-agnostic first**: Durable agents work in console apps, Azure Functions, and any Durable Taskcompatible host. Show host-agnostic patterns (plain orchestration functions, `IServiceCollection` registration) before Azure Functionsspecific patterns. Avoid giving the impression that Azure Functions is the only hosting option.
- **Both languages**: Always include C# and Python examples side by side. Keep them equivalent in functionality.
- **Callout syntax**: Use GitHub-flavored callouts (`> [!NOTE]`, `> [!IMPORTANT]`, `> [!WARNING]`) rather than bold-text callouts (`> **Note:** ...`).
- **Line length**: Do not wrap long lines. Rely on text viewers / renderers for line wrapping.
- **Tables**: Use spaces around pipes in separator rows (`| --- |` not `|---|`).
- **Code snippets**: Keep them minimal and self-contained. Omit boilerplate (using statements, environment variable reads) unless the snippet is specifically about setup.
- **Cross-references**: Link to Microsoft Learn for conceptual background (Durable Entities, Durable Task Scheduler, Azure Functions). Link to sibling docs within this directory for feature deep-dives.
## Linting
Run markdownlint on all documents before committing, with line-length checks disabled:
```bash
markdownlint docs/features/durable-agents/ --disable MD013
```
## When to update these docs
- A new durable agent feature is added (e.g., a new orchestration pattern, hosting model, or configuration option).
- The public API surface changes in a way that affects how developers use durable agents.
- New sample directories are added — update the sample links in README.md.
- The official Microsoft Learn documentation is restructured — update external links.
+237 -7
View File
@@ -1,9 +1,239 @@
# Durable Agents Have Moved
# Durable agents
Durable Task and Azure Functions integrations for Microsoft Agent Framework are now maintained in the [Durable Agent Framework extension repository](https://github.com/microsoft/agent-framework-durable-extension).
## Overview
- [.NET source](https://github.com/microsoft/agent-framework-durable-extension/tree/main/dotnet/src)
- [.NET samples](https://github.com/microsoft/agent-framework-durable-extension/tree/main/dotnet/samples)
- [Python source](https://github.com/microsoft/agent-framework-durable-extension/tree/main/python/packages)
- [Python samples](https://github.com/microsoft/agent-framework-durable-extension/tree/main/python/samples)
- [Durable agent documentation](https://github.com/microsoft/agent-framework-durable-extension/tree/main/docs/features/durable-agents)
Durable agents extend the standard Microsoft Agent Framework with **durable state management** powered by the Durable Task framework. An ordinary Agent Framework agent runs in-process: its conversation history lives in memory and is lost when the process ends. A durable agent persists conversation history and execution state in external storage so that sessions survive process restarts, failures, and scale-out events.
| Capability | Ordinary agent | Durable agent |
| --- | --- | --- |
| Conversation history | In-memory only | Durably persisted |
| Failure recovery | State lost on crash | Automatically resumed |
| Multi-instance scale-out | Not supported | Any worker can resume a session |
| Multi-agent orchestrations | Manual coordination | Deterministic, checkpointed workflows |
| Human-in-the-loop | Must keep process alive | Can wait days/weeks with zero compute |
| Hosting | Any process | Console app, Azure Functions, or any Durable Taskcompatible host |
> [!NOTE]
> For a step-by-step tutorial and deployment guidance, see [Azure Functions (Durable)](https://learn.microsoft.com/agent-framework/integrations/azure-functions) on Microsoft Learn.
## How durable agents work
Durable agents are implemented on top of [Durable Entities](https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-entities) (also called "virtual actors"). Each **agent session** maps to one entity instance whose state contains the full conversation history. When you send a message to a durable agent, the following happens:
1. The message is dispatched to the entity identified by an `AgentSessionId` (a composite of the agent name and a unique session key).
2. The entity loads its persisted `DurableAgentState`, which includes the complete conversation history.
3. The entity invokes the underlying `AIAgent` with the full conversation history, collects the response, and appends both the request and the response to the state.
4. The updated state is persisted back to durable storage automatically.
Because the entity framework serializes access to each entity instance, concurrent messages to the same session are processed one at a time, eliminating race conditions.
### Agent session identity
Every durable agent session is identified by an `AgentSessionId`, which has two components:
- **Name** the registered name of the agent (case-insensitive).
- **Key** a unique session key (case-sensitive), typically a GUID.
The session ID is mapped to an underlying Durable Task entity ID with a `dafx-` prefix (e.g., `dafx-joker`). This naming convention is consistent across both .NET and Python implementations.
## Architecture
### .NET
The .NET implementation consists of two NuGet packages:
| Package | Purpose |
| --- | --- |
| `Microsoft.Agents.AI.DurableTask` | Core durable agent types: `DurableAIAgent`, `AgentEntity`, `DurableAgentSession`, `AgentSessionId`, `DurableAgentsOptions`, and the state model. |
| `Microsoft.Agents.AI.Hosting.AzureFunctions` | Azure Functions hosting integration: auto-generated HTTP endpoints, MCP tool triggers, entity function triggers, and the `ConfigureDurableAgents` extension method on `FunctionsApplicationBuilder`. |
Key types:
- **`DurableAIAgent`** A subclass of `AIAgent` used *inside orchestrations*. Obtained via `context.GetAgent("agentName")`, it routes `RunAsync` calls through the orchestration's entity APIs so that each call is checkpointed.
- **`DurableAIAgentProxy`** A subclass of `AIAgent` used *outside orchestrations* (e.g., from HTTP triggers or console apps). It signals the entity via `DurableTaskClient` and polls for the response.
- **`AgentEntity`** The `TaskEntity<DurableAgentState>` that hosts the real agent. It loads the registered `AIAgent` by name, wraps it in an `EntityAgentWrapper`, feeds it the full conversation history, and persists the result.
- **`DurableAgentSession`** An `AgentSession` subclass that carries the `AgentSessionId`.
- **`DurableAgentsOptions`** Builder for registering agents and configuring TTL.
### Python
The core Python implementation is in the `agent-framework-durabletask` package (`python/packages/durabletask`). Azure Functions hosting (including `AgentFunctionApp`) is in the separate `agent-framework-azurefunctions` package (`python/packages/azurefunctions`).
Key types:
- **`DurableAIAgent`** A generic proxy (`DurableAIAgent[TaskT]`) implementing `SupportsAgentRun`. Returns a `TaskT` from `run()` — either an `AgentResponse` (client context) or a `DurableAgentTask` (orchestration context, must be `yield`ed).
- **`DurableAIAgentWorker`** Wraps a `TaskHubGrpcWorker` and registers agents as durable entities via `add_agent()`.
- **`DurableAIAgentClient`** Wraps a `TaskHubGrpcClient` for external callers. `get_agent()` returns a `DurableAIAgent[AgentResponse]`.
- **`DurableAIAgentOrchestrationContext`** Wraps an `OrchestrationContext` for use inside orchestrations. `get_agent()` returns a `DurableAIAgent[DurableAgentTask]`.
- **`AgentEntity`** Platform-agnostic agent execution logic that manages state, invokes the agent, handles streaming, and calls response callbacks.
## Hosting models
### Azure Functions
The recommended production hosting model. A single call to `ConfigureDurableAgents` (C#) or `AgentFunctionApp` (Python) automatically:
- Registers agent entities with the Durable Task worker.
- Generates HTTP endpoints at `/api/agents/{agentName}/run` for each registered agent.
- Supports `thread_id` query parameter / JSON field and the `x-ms-thread-id` response header for session continuity.
- Supports fire-and-forget via the `x-ms-wait-for-response: false` header (returns HTTP 202).
- Optionally exposes agents as MCP tools.
**C# example:**
```csharp
using IHost app = FunctionsApplication
.CreateBuilder(args)
.ConfigureFunctionsWebApplication()
.ConfigureDurableAgents(options => options.AddAIAgent(agent))
.Build();
app.Run();
```
**Python example:**
```python
app = AgentFunctionApp(agents=[agent])
```
### Console apps / generic hosts
For self-hosted or non-serverless scenarios, register durable agents via `IServiceCollection.ConfigureDurableAgents` (.NET) or `DurableAIAgentWorker` (Python) with explicit Durable Task worker and client configuration.
**C# example:**
```csharp
IHost host = Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
services.ConfigureDurableAgents(
options => options.AddAIAgent(agent),
workerBuilder: b => b.UseDurableTaskScheduler(connectionString),
clientBuilder: b => b.UseDurableTaskScheduler(connectionString));
})
.Build();
```
**Python example:**
```python
worker = DurableAIAgentWorker(TaskHubGrpcWorker(host_address="localhost:4001"))
worker.add_agent(agent)
worker.start()
```
## Deterministic multi-agent orchestrations
Durable agents can be composed into deterministic, checkpointed workflows using Durable Task orchestrations. The orchestration framework replays orchestrator code on failure, so completed agent calls are not re-executed.
### Patterns
| Pattern | Description |
| --- | --- |
| **Sequential (chaining)** | Call agents one after another, passing outputs forward. |
| **Parallel (fan-out/fan-in)** | Run multiple agents concurrently and aggregate results. |
| **Conditional** | Branch orchestration logic based on structured agent output. |
| **Human-in-the-loop** | Pause for external events (approvals, feedback) with optional timeouts. |
### Using agents in orchestrations
Inside an orchestration function, obtain a `DurableAIAgent` via the orchestration context. Each agent gets its own session (created with `CreateSessionAsync` / `create_session`), and you can call the same agent multiple times on the same session to maintain conversation context across sequential invocations.
**C#:**
```csharp
static async Task<string> WritingOrchestration(TaskOrchestrationContext context)
{
// Get a durable agent reference — works in any host (console app, Azure Functions, etc.)
DurableAIAgent writer = context.GetAgent("WriterAgent");
// Create a session to maintain conversation context across multiple calls
AgentSession session = await writer.CreateSessionAsync();
// First call: generate an initial draft
AgentResponse<TextResponse> draft = await writer.RunAsync<TextResponse>(
message: "Write a concise inspirational sentence about learning.",
session: session);
// Second call: refine the draft — the agent sees the full conversation history
AgentResponse<TextResponse> refined = await writer.RunAsync<TextResponse>(
message: $"Improve this further while keeping it under 25 words: {draft.Result.Text}",
session: session);
return refined.Result.Text;
}
```
**Python:**
```python
def writing_orchestration(context, _):
agent_ctx = DurableAIAgentOrchestrationContext(context)
# Get a durable agent reference — works in any host (standalone worker, Azure Functions, etc.)
writer = agent_ctx.get_agent("WriterAgent")
# Create a session to maintain conversation context across multiple calls
session = writer.create_session()
# First call: generate an initial draft
draft = yield writer.run(
messages="Write a concise inspirational sentence about learning.",
session=session,
)
# Second call: refine the draft — the agent sees the full conversation history
refined = yield writer.run(
messages=f"Improve this further while keeping it under 25 words: {draft.text}",
session=session,
)
return refined.text
```
> [!IMPORTANT]
> In .NET, `DurableAIAgent.RunAsync<T>` deliberately avoids `ConfigureAwait(false)` because the Durable Task Framework uses a custom synchronization context — all continuations must run on the orchestration thread.
## Streaming and response callbacks
Durable agents do not support true end-to-end streaming because entity operations are request/response. However, **reliable streaming** is supported via response callbacks:
- **`IAgentResponseHandler`** (.NET) or **`AgentResponseCallbackProtocol`** (Python) Implement this interface to receive streaming updates as the underlying agent generates them (e.g., push tokens to a Redis Stream for client consumption).
- The entity still returns the complete `AgentResponse` after the stream is fully consumed.
- Clients can reconnect and resume reading from a cursor-based stream (e.g., Redis Streams) without losing messages.
See the **Reliable Streaming** samples for a complete implementation using Redis Streams.
## Session TTL (Time-To-Live)
Durable agent sessions support automatic cleanup via configurable TTL. See [Session TTL](durable-agents-ttl.md) for details on configuration, behavior, and best practices.
## Observability
When using the [Durable Task Scheduler](https://learn.microsoft.com/azure/azure-functions/durable/durable-task-scheduler/durable-task-scheduler) as the durable backend, you get built-in observability through its dashboard:
- **Conversation history** View complete chat history for each agent session.
- **Orchestration visualization** See multi-agent execution flows, including parallel branches and conditional logic.
- **Performance metrics** Monitor agent response times, token usage, and orchestration duration.
- **Debugging** Trace tool invocations and external event handling.
## Samples
- **.NET** [Console app samples](../../../dotnet/samples/04-hosting/DurableAgents/ConsoleApps/) and [Azure Functions samples](../../../dotnet/samples/04-hosting/DurableAgents/AzureFunctions/) covering single-agent, chaining, concurrency, conditionals, human-in-the-loop, long-running tools, MCP tool exposure, and reliable streaming.
- **Python** [Durable Task samples](../../../python/samples/04-hosting/durabletask/) covering single-agent, multi-agent, streaming, chaining, concurrency, conditionals, and human-in-the-loop.
## Packages
| Language | Package | Source |
| --- | --- | --- |
| .NET | `Microsoft.Agents.AI.DurableTask` | [`dotnet/src/Microsoft.Agents.AI.DurableTask`](../../../dotnet/src/Microsoft.Agents.AI.DurableTask) |
| .NET | `Microsoft.Agents.AI.Hosting.AzureFunctions` | [`dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions`](../../../dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions) |
| Python | `agent-framework-durabletask` | [`python/packages/durabletask`](../../../python/packages/durabletask) |
| Python | `agent-framework-azurefunctions` | [`python/packages/azurefunctions`](../../../python/packages/azurefunctions) |
## Further reading
- [Azure Functions (Durable) — Microsoft Learn](https://learn.microsoft.com/agent-framework/integrations/azure-functions)
- [Durable Task Scheduler](https://learn.microsoft.com/azure/azure-functions/durable/durable-task-scheduler/durable-task-scheduler)
- [Durable Entities](https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-entities)
- [Session TTL](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.
-498
View File
@@ -1,498 +0,0 @@
---
status: proposed
contact: eavanvalkenburg
date: 2026-07-08
deciders: eavanvalkenburg
---
# Python protocol helpers and optional execution state
## Scope
This specification is the Python implementation plan for
[ADR-0027](../decisions/0027-hosting-channels.md). It documents the helper-first v1 contract for Python hosting.
The v1 contract is:
- protocol packages expose helper functions that convert protocol-native input to Agent Framework run values;
- protocol packages expose helper functions that convert Agent Framework run results or streams back to protocol-native
payloads or operations;
- application/framework code owns routes, native SDK clients, authentication, command policy, webhooks, response status
codes, and outbound sends;
- `agent-framework-hosting` provides small optional state holders for Agent Framework targets;
- state helpers do not own web apps, route contribution, protocol dispatch, command projection, or native SDK calls.
## Goals
- Let apps expose agents and workflows from FastAPI, Starlette, Django, Azure Functions, native SDK webhooks, CLIs, and
tests without adopting a host/channel framework.
- Keep protocol parsing and response formatting inside protocol packages.
- Keep session continuity explicit and app-owned at the trust boundary.
- Reuse Agent Framework primitives: `AgentSession`, `CheckpointStorage`, `Agent.run(...)`, `Workflow.run(...)`, and
`ResponseStream`.
- Preserve full-fidelity Agent Framework results until a protocol helper renders them.
## Non-goals for v1
### App-owned in v1
The app builder owns these concerns with normal web-framework, SDK, platform, or application code:
- authentication, authorization policy, and allowlists;
- deciding whether identities across protocols map to the same `session_id`;
- non-originating sends using native SDK clients;
- background work, durable execution, retry, and replay when app code owns the work;
- routing between multiple agents.
The helper-first model makes app-owned linking and non-originating delivery easier than the old host/channel model because
app code already owns the native SDK clients, authenticated caller context, session id selection, and outbound sends.
### Future framework work
The following require a separate reviewed design before becoming reusable framework features:
- reusable cross-channel identity linking;
- framework-owned proactive or non-originating delivery;
- fan-out, multicast, selected-channel, active-channel, or all-linked delivery;
- framework-owned delivery observability, dead-letter handling, and replay semantics;
- cross-channel confidentiality and link policy.
[ADR-0028](../decisions/0028-hosting-linking-multicast-enhancements.md) tracks possible follow-up work in this area and
must be aligned with the helper-first model before implementation. Old vocabulary such as `IdentityLinker`,
`ResponseTarget`, `ChannelPush`, `ChannelPushCodec`, `DurableTaskRunner`, `RetryPolicy`, and `LinkPolicy` is not v1 API.
## Packages
| Package | Import surface | v1 helper-first contents |
|---|---|---|
| `agent-framework-hosting` | `agent_framework_hosting` | `AgentState`, `WorkflowState`, `SessionStore`, and run-argument `TypedDict`s. |
| `agent-framework-hosting-a2a` | `agent_framework_hosting_a2a` | A2A `Message` to run conversion and Agent Framework output to A2A `Part` conversion. |
| `agent-framework-hosting-mcp` | `agent_framework_hosting_mcp` | Agent and workflow MCP tool adapters, MCP tool arguments to run conversion, and Agent Framework output to MCP `ContentBlock` conversion. |
| `agent-framework-hosting-responses` | `agent_framework_hosting_responses` | Responses helpers: request parsing, session id extraction, response id creation, response rendering, streaming rendering. |
| `agent-framework-hosting-telegram` | `agent_framework_hosting_telegram` | Telegram Bot API helpers: update parsing, chat/session/command/media extraction, final rendering, and streaming edit rendering. |
| Future protocol packages | e.g. `agent_framework_hosting_activity_protocol` | Protocol-specific helpers such as `activity_to_run(...)`, `activity_from_run(...)`, `activity_session_id(...)`, and command/media helpers when useful. |
The core hosting package must not depend on protocol SDKs. Protocol packages may depend on their native protocol SDKs if
needed, but helper functions should stay usable from plain app code and tests.
## Helper naming and families
Helper names are protocol-specific. Avoid a generic `protocol_to_run(...)` public surface.
Protocol packages may provide the following helper families when the protocol has the concept:
| Helper family | Shape | Purpose |
| --- | --- | --- |
| Run conversion | `<protocol>_to_run(...)` | Convert one protocol-native call/update/request into `Agent.run` or `Workflow.run` values. |
| Final rendering | `<protocol>_from_run(...)` | Convert a final `AgentResponse` or workflow result into protocol-native response payloads or operations. |
| Stream rendering | `<protocol>_from_streaming_run(...)` | Convert `ResponseStream` or workflow updates into protocol-native events or operations. |
| Session id extraction | `<protocol>_session_id(...)` | Extract the protocol's natural continuation/partition key from the call, if present. |
| Command/action parsing | `<protocol>_command(...)` | Parse a protocol-native command/action/operation name without deciding app policy. |
Examples:
- `responses_to_run(...)`, `responses_from_run(...)`, `responses_from_streaming_run(...)`,
`responses_session_id(...)`;
- `a2a_to_run(...)`, `a2a_from_run(...)`;
- `telegram_to_run(...)`, `telegram_from_run(...)`, `telegram_from_streaming_run(...)`,
`telegram_session_id(...)`, `telegram_command(...)`;
- `activity_to_run(...)`, `activity_from_run(...)`, `activity_session_id(...)`, `activity_command(...)`;
- `discord_to_run(...)`, `discord_from_run(...)`, `discord_session_id(...)`, `discord_command(...)`.
This table is a naming guide, not a required checklist. A protocol package should add only the helpers that match native
protocol concepts and current samples.
Protocol-specific helpers may also exist for native details such as `telegram_chat_id(...)`,
`telegram_callback_query_id(...)`, `telegram_media_file_id(...)`, `discord_interaction_id(...)`, `a2a_task_id(...)`,
`a2a_context_id(...)`, or MCP tool/prompt/resource helpers. These helpers should stay side-effect-free. App/native SDK
code performs acknowledgements, sends/edits messages, resolves protected file URLs, applies rate limits, and registers
handlers.
## `agent-framework-hosting` state helpers
### `SessionStore`
`SessionStore` is an in-memory async lookup:
```python
class SessionStore:
async def get(self, session_id: str) -> AgentSession | None: ...
async def set(self, session_id: str, session: AgentSession) -> None: ...
async def delete(self, session_id: str) -> None: ...
```
The store does not create sessions. It stores `session_id -> AgentSession` values supplied by callers.
The built-in store has no TTL or eviction. This is intentional for local/dev and simple process-local scenarios: protocols
such as OpenAI Responses can continue from any prior response id. Durable or multi-replica deployments should provide a
durable store and their own TTL/eviction policy.
### `AgentState`
`AgentState` holds an agent target and an optional `SessionStore`:
```python
state = AgentState(agent)
state = AgentState(create_agent)
state = AgentState(create_agent, cache_target=False)
```
The target may be:
- a `SupportsAgentRun` instance;
- a synchronous factory;
- an asynchronous factory;
- an awaitable target.
`AgentState` provides:
- `await get_target()`;
- synchronous `target` only after a target is already available/resolved;
- `session_store`;
- `await get_or_create_session(session_id)`;
- `await set_session(session_id, session)`.
`get_or_create_session(...)` resolves the target and calls `target.create_session(session_id=...)` only when the store has
no session for that id.
Apps must store the post-run session explicitly after `agent.run(...)` or stream finalization:
```python
session = await state.get_or_create_session(session_id)
target = await state.get_target()
result = await target.run(messages, session=session, options=options)
await state.set_session(response_id, session)
```
### `WorkflowState`
`WorkflowState` resolves a workflow target. It does not own checkpoint storage.
The target may be:
- a `Workflow` instance;
- a `WorkflowBuilder` or other object with `build() -> Workflow`;
- a synchronous factory;
- an asynchronous factory;
- an awaitable target.
`WorkflowState` provides:
- `await get_target()`;
- synchronous `target` only after a target is already available/resolved.
A workflow instance permits one active run. Concurrent hosts use a factory or
builder with `cache_target=False` to resolve a fresh instance per run.
Workflow checkpointing uses Agent Framework's existing `CheckpointStorage` abstraction directly. Apps that need
per-session workflow resume should keep an app-owned cursor such as `session_id -> checkpoint_id`. When the app uses
file-backed cursor storage, the file-based checkpoint storage should share the same app storage root and should be
scoped to the current authenticated user/tenant/session bucket, for example
`storage/checkpoints/<session-bucket>/` beside `storage/checkpoint_cursors.json`:
```python
# session_id must already be authenticated and authorized for this caller
target = await workflow_state.get_target()
checkpoint_id = await checkpoint_cursor_store.get(session_id)
if checkpoint_id is None:
result = await target.run(message=workflow_input, checkpoint_storage=checkpoint_storage)
else:
result = await target.run(checkpoint_id=checkpoint_id, checkpoint_storage=checkpoint_storage)
latest = await checkpoint_storage.get_latest(workflow_name=target.name)
if latest is not None:
await checkpoint_cursor_store.set(session_id, latest.checkpoint_id)
```
`Workflow.run(...)` does not currently emit a checkpoint id on `WorkflowRunResult` or normal workflow events by default.
The runner receives checkpoint ids internally from `CheckpointStorage.save(...)`. Apps that own the storage can query
`get_latest(workflow_name=...)` after the run if they need to update a cursor.
## `agent-framework-hosting-responses`
The Responses package provides the helper-first surface for OpenAI Responses-shaped requests.
### Request helpers
- `messages_from_responses_input(input) -> list[Message]`
- `responses_to_run(body) -> AgentRunArgs`
- `responses_session_id(body) -> str | None`
- `create_response_id() -> str`
`responses_to_run(...)` returns values corresponding to `Agent.run(...)`:
```python
run = responses_to_run(body)
messages = run["messages"]
options = run["options"]
stream = run["stream"]
```
It excludes protocol transport/session fields from `options` and remaps known Responses option names such as
`max_output_tokens -> max_tokens`.
`responses_session_id(...)` returns:
- `previous_response_id` when present (`resp_*`);
- otherwise `conversation_id` when present (`conv_*`);
- otherwise `None`.
The helper only extracts the candidate key. App code decides whether to trust and use that key.
### Response helpers
- `responses_from_run(result, *, response_id, session_id=None) -> dict[str, Any]`
- `responses_from_streaming_run(stream, *, response_id, session_id=None) -> AsyncIterator[str]`
`responses_from_run(...)` renders a full Responses JSON payload from an `AgentResponse`. It renders the full set of
OpenAI Responses output item types supported by Agent Framework content.
`responses_from_streaming_run(...)` renders Server-Sent Event strings for a `ResponseStream`. It emits a created event,
text deltas, and a completed event. The final completed payload is produced through `responses_from_run(...)`; the helper
also preserves the model id observed on streaming updates when the finalized `AgentResponse` no longer carries raw model
metadata.
## `agent-framework-hosting-a2a`
The A2A package provides only the conversion seam between the native A2A SDK
and Agent Framework:
- `a2a_to_run(message, *, stream=False) -> AgentRunArgs`
- `a2a_from_run(result) -> list[a2a.types.Part]`
`a2a_to_run(...)` accepts a native A2A `Message` and converts its text, URL,
raw-byte, and structured-data parts into one Agent Framework user message.
`a2a_from_run(...)` accepts an `AgentResponse`, `Message`, or
`AgentResponseUpdate` and converts supported text, URI, and data content into
native A2A `Part` values. This one helper is usable for both completed and
streaming runs.
The package does not provide an A2A `AgentExecutor`, application, route,
request handler, task store, event queue, `TaskUpdater`, task-state policy,
artifact-id policy, or session-key policy. Application code composes the two
helpers with those native A2A SDK constructs and may use any server framework
supported by the SDK.
## `agent-framework-hosting-mcp`
The MCP package provides only the conversion seam between native MCP SDK values
and Agent Framework:
- `MCPAgentTool(target, ...)`
- `MCPWorkflowTool(target, ...)`
- `mcp_to_run(arguments, *, argument_name="task", chat_option_arguments=()) -> AgentRunArgs`
- `mcp_from_run(result) -> list[mcp.types.ContentBlock]`
`MCPAgentTool` represents one Agent Framework agent as one native MCP tool. It
derives the default tool name and description from the agent, accepts
overrides for those values and the main text parameter, includes app-owned
additional parameter schemas, and explicitly maps selected parameter schemas
to ChatOptions. Its asynchronous `list_tools()` returns the native `Tool` list,
and `call_tool(...)` performs conversion, agent execution, and final result
conversion.
The adapter accepts either an agent or an existing `AgentState`. With a
configured `session_id_parameter`, it loads and stores the corresponding
`AgentSession`. The application remains responsible for deriving and
authorizing the session id and preventing concurrent updates to the same
session.
`MCPWorkflowTool` represents one Agent Framework workflow as one native MCP
tool. It derives the tool name and description from the workflow and derives
the input schema from the start executor's single declared input type.
Object-shaped dataclass and Pydantic inputs become top-level MCP arguments;
primitive inputs are wrapped in one configurable argument. The adapter
validates the arguments against that type, runs the workflow, and converts
terminal outputs to MCP content blocks.
Workflow instances preserve state and reject concurrent runs. Applications
that need independent calls should provide a `WorkflowState` factory with
`cache_target=False`. Checkpoint restoration, human-in-the-loop responses, and
continuation identifiers remain application-owned contracts. If a workflow
stops to request external input, the adapter raises rather than returning an
empty successful tool result.
`mcp_to_run(...)` accepts the argument mapping from a native MCP `call_tool`
handler. The application owns the tool schema and may select which required
string argument contains the user request. The application should define that
argument name once and use the same value in the native tool schema and the
`argument_name` parameter so those two sides of the contract remain aligned.
Applications may also expose selected ChatOptions fields in their native tool
schema and pass those names through `chat_option_arguments`. Only explicitly
selected names are copied to run options; the helper does not forward all MCP
arguments or own their JSON Schema validation.
MCP `tools/call` arguments are JSON-only and do not have a native multimodal
content-block union. The package does not impose a non-standard JSON
representation for multimodal tool arguments.
`mcp_from_run(...)` accepts an `AgentResponse` or `Message`. It converts text,
URI, image data, audio data, and other binary data into native MCP content
blocks.
Its output is specifically the content union accepted by `CallToolResult`.
Sampling-only values such as `ToolUseContent` belong to the separate MCP
sampling response path and are not emitted by this hosting helper.
MCP `tools/call` returns one final `CallToolResult`. Streamable HTTP can carry
multiple MCP messages and progress notifications can report operation status,
but the protocol does not define partial tool-result content chunks.
Experimental MCP tasks defer retrieval of the same final result. Therefore the
conversion helpers do not expose Agent Framework streaming updates.
The package does not provide an MCP `Server`, handler registration, transport, route,
session policy, authentication, authorization, or deployment wrapper.
Application code composes the adapters and conversion helpers with native MCP SDK constructs and
may use stdio, streamable HTTP, or another transport supported by the SDK.
## `agent-framework-hosting-telegram`
The Telegram package provides side-effect-free helpers around Telegram Bot API
update and method payloads. It does not provide a Bot API client, polling loop,
webhook route, command registry, retry policy, or rate limiter.
### Update helpers
- `telegram_to_run(update, *, resolve_file_url=None, stream=False) -> AgentRunArgs`
- `telegram_chat_id(update) -> int | None`
- `telegram_session_id(update, *, bot_id) -> str | None`
- `telegram_command(update) -> str | None`
- `telegram_callback_query_id(update) -> str | None`
- `telegram_media_file_id(update_or_message) -> tuple[str, str] | None`
`telegram_to_run(...)` handles `message`, `edited_message`, and
`callback_query` updates. Text and captions become AF text content. When the
app supplies an async `resolve_file_url` callback, supported Telegram media
file ids can become AF URI content. The package does not call Telegram's
`getFile` method itself.
`telegram_session_id(..., bot_id=...)` includes the bot identity in every key.
Private chats return `telegram:<bot_id>:<user_id>`; other chats return
`telegram:<bot_id>:<chat_id>`, giving groups a shared session by default. This
matches Telegram's native isolation boundaries while preventing two bots from
sharing state accidentally. Apps that want per-user sessions inside a group
can construct a key that includes both chat and sender ids. The app must
authorize those Telegram identities before loading session state.
`telegram_command(...)` parses Telegram's `/name` and `/name@bot` syntax. It
does not register commands or invoke handlers.
### Response helpers
- `telegram_from_run(result, *, chat_id, parse_mode=None)`
- `telegram_from_streaming_run(stream, *, chat_id, message_id, initial_text=None, parse_mode=None)`
The helpers produce Telegram method/payload values for app-owned Bot API
calls. Final rendering supports text and image URI output and applies
Telegram's text-length boundary. Streaming rendering produces cumulative
`editMessageText` payloads for a placeholder message id supplied by the app,
omitting edits that match an optional `initial_text`, then renders the final
rich output. Image-only responses remove the placeholder with `deleteMessage`
before sending the image. The app owns the initial placeholder send, Bot API
calls, edit throttling, retries, and failure policy.
## Security responsibilities
Protocol helper packages parse and render. They do not authenticate callers, authorize access to state, or decide which
side effects are allowed.
Application code that uses these helpers is responsible for:
- authenticating the caller through the app's normal mechanism before using protocol-provided ids;
- authorizing any caller-supplied session, checkpoint, task, context, conversation, thread, or response id before loading
state for it;
- binding externally supplied ids to the authenticated user, tenant, workspace, installation, or chat context before
using them as `SessionStore` keys or checkpoint cursor keys;
- treating `<protocol>_session_id(...)` results as untrusted candidate keys until that ownership check has passed;
- keeping platform-provided isolation helpers fail-closed outside their trusted hosting environment;
- authorizing command/action effects such as reset, cancel, approve, submit, or tool invocation after parsing them;
- opting in explicitly before resolving protected media/resource/file URLs and passing them to a remote model provider;
- persisting post-run session or checkpoint state only after `agent.run(...)`, `workflow.run(...)`, or stream finalization
has updated that state.
## Persistent versus transient hosting
The application builder decides whether the server is persistent or transient.
- Persistent single-process apps, such as a long-running container or web app, may use in-memory state for local
development or simple deployments. Multi-replica persistent apps still need durable state for continuity.
- Transient apps, such as Azure Functions, Foundry Hosted Agents, or any environment where process memory is not a
reliable boundary, must not rely on in-memory `SessionStore` state between calls. They need a durable session store or
a service-owned continuation id.
- Workflow hosts must choose an explicit `CheckpointStorage` and, when they need per-session resume, a durable
`session_id -> checkpoint_id` cursor. File-backed checkpoint storage and file-backed cursor storage should live under
the same app storage root, with checkpoints scoped to the current authenticated user/tenant/session bucket so a
"latest checkpoint" lookup cannot cross conversations. In-process workflow state and in-memory checkpoint cursors do
not survive transient execution.
## Minimal FastAPI Responses shape
This is the shape the local Responses sample should demonstrate. It is not an app framework.
```python
from collections.abc import AsyncIterator
from agent_framework import ResponseStream
from agent_framework_hosting import AgentState
from agent_framework_hosting_responses import (
create_response_id,
responses_from_run,
responses_from_streaming_run,
responses_session_id,
responses_to_run,
)
from fastapi import Body, FastAPI, HTTPException
from fastapi.responses import JSONResponse, StreamingResponse
app = FastAPI()
state = AgentState(create_agent)
@app.post("/responses", response_model=None)
async def responses(body: dict = Body(...)) -> JSONResponse | StreamingResponse:
run = responses_to_run(body)
candidate_session_id = responses_session_id(body)
response_id = create_response_id()
# Verify this caller owns candidate_session_id before loading it.
session_id = candidate_session_id or response_id
session = await state.get_or_create_session(session_id)
target = await state.get_target()
if run["stream"]:
stream = target.run(run["messages"], stream=True, session=session, options=run["options"])
if not isinstance(stream, ResponseStream):
raise HTTPException(status_code=500, detail="agent did not return a response stream")
async def events() -> AsyncIterator[str]:
async for event in responses_from_streaming_run(
stream,
response_id=response_id,
session_id=candidate_session_id,
):
yield event
await state.set_session(response_id, session)
return StreamingResponse(events(), media_type="text/event-stream")
result = await target.run(run["messages"], session=session, options=run["options"])
await state.set_session(response_id, session)
return JSONResponse(responses_from_run(result, response_id=response_id, session_id=candidate_session_id))
```
## Validation
Implementation validation must cover:
- `SessionStore` plain get/set/delete behavior;
- `AgentState` target resolution, target caching, and get-or-create session behavior;
- `WorkflowState` target resolution for direct workflows, factories, `WorkflowBuilder`, and orchestration-style builders;
- Responses request parsing and option remapping;
- Responses session id extraction;
- Responses response rendering, including rich output item mapping;
- Responses streaming SSE rendering;
- HTTP round-trip tests showing a native FastAPI route using `AgentState` and Responses helpers;
- sample type checking for the local Responses sample.
- Telegram update parsing, chat/session/command/media extraction, final
rendering, and streaming edit rendering;
- sample type checking for the local Telegram polling and webhook entry points.
@@ -1,246 +0,0 @@
---
status: accepted
contact: rogerbarreto
date: 2026-07-08
deciders: rogerbarreto
consulted: eavanvalkenburg
informed: []
---
# .NET hosting: OpenAI Responses protocol helpers and optional execution state
Implements [ADR-0032](../decisions/0032-dotnet-hosting-protocol-helpers.md), which realizes the
helper-first direction of [ADR-0027](../decisions/0027-hosting-channels.md) for .NET.
## What is the goal of this feature?
Let application developers expose an `AIAgent` or workflow over the OpenAI Responses protocol **while
owning their own ASP.NET Core route, authentication, middleware, and storage**, by calling small,
side-effect-free Agent Framework conversion helpers instead of adopting the batteries-included,
route-owning `MapOpenAIResponses` server.
Success: an application can implement a working `POST /responses` endpoint (sync + streaming) in its
own minimal-API handler using only the public helpers plus its own auth/storage, with no dependency on
`MapOpenAIResponses` or `IResponsesService`.
## What is the problem being solved?
.NET already exposes agents as the OpenAI Responses API, but only through the route-owning
`MapOpenAIResponses`/`IResponsesService`, which also owns routing, response/conversation storage,
streaming, and lifecycle. An application that wants its own routing (custom auth, middleware, status
codes, durable storage, or a different framework surface) currently has no supported way to reuse the
framework's Responses<->agent conversion. Every conversion primitive that would make this possible
already exists in `Microsoft.Agents.AI.Hosting.OpenAI` but is `internal`.
This feature un-bundles that conversion into a public, app-callable surface, and adds the minimal
execution-state helpers an app needs for session continuity and workflow checkpoint resume.
## API Changes
### `Microsoft.Agents.AI.Hosting.OpenAI` (new public static facade `OpenAIResponses`)
Boundary is `System.Text.Json`; the wire DTOs stay internal. All members are side-effect-free.
```csharp
namespace Microsoft.Agents.AI.Hosting.OpenAI;
public static class OpenAIResponses
{
// Wire -> Agent Framework run input.
public static OpenAIResponsesRunRequest ToAgentRunRequest(
JsonElement body,
OpenAIResponsesMapOptions? mapOptions = null);
// Agent Framework result -> Responses payload (no originating request required).
public static JsonElement WriteResponse(
AgentResponse response,
string responseId,
string? sessionId = null);
// Agent Framework stream -> Responses SSE `data:` frames.
public static IAsyncEnumerable<string> WriteResponseStreamAsync(
IAsyncEnumerable<AgentResponseUpdate> updates,
string responseId,
string? sessionId = null,
CancellationToken cancellationToken = default);
// Untrusted candidate continuation key: previous_response_id or conversation id (or null).
// Kept SEPARATE from ToAgentRunRequest so using a request-derived key is an explicit decision.
public static string? GetSessionId(JsonElement body);
// Mint a `resp_*` id.
public static string CreateResponseId();
}
// Result of ToAgentRunRequest.
public sealed class OpenAIResponsesRunRequest
{
public IList<ChatMessage> Messages { get; }
public AgentRunOptions? Options { get; }
}
```
`ToAgentRunRequest` honors `OpenAIResponsesMapOptions.RunOptionsFactory` exactly as the route model
does (by default no request setting is mapped onto the run; unsupported settings surface as a
`NotSupportedException`). `WriteResponse`/`WriteResponseStreamAsync` reuse the existing internal
`AgentResponseExtensions.ToResponse` / `AgentResponseUpdateExtensions.ToStreamingResponseAsync`
converters (an internal `ToResponse` overload with an optional originating request is added so the
facade can render without one). The streaming renderer's existing workflow-event support is preserved.
### `Microsoft.Agents.AI.Hosting` (execution state, protocol-neutral)
```csharp
namespace Microsoft.Agents.AI.Hosting;
public abstract class AgentSessionStore
{
// ... existing members ...
// New: the one missing store operation. Virtual (not abstract) with a default that throws
// NotSupportedException, so existing external stores (e.g. the Foundry hosting stores) keep
// compiling; the in-box Hosting stores override it. In-box overrides treat deleting a missing
// session as a no-op.
public virtual ValueTask DeleteSessionAsync(
AIAgent agent, string conversationId, CancellationToken cancellationToken = default);
}
// Thin holder: pairs a workflow target with checkpointing + a per-session head cursor.
public sealed class HostedWorkflowState
{
// Shared-instance mode: one instance cannot be run by two runners at once, so turns run one at a time.
public HostedWorkflowState(Workflow workflow, CheckpointManager? checkpointManager = null);
// Factory mode: by default a fresh instance is built per run, so independent sessions run in parallel.
// With cacheWorkflow: true the factory is invoked once lazily and the built instance is cached and reused.
public HostedWorkflowState(Func<CancellationToken, ValueTask<Workflow>> workflowFactory, CheckpointManager? checkpointManager = null, bool cacheWorkflow = false);
// First turn runs forward from the start; subsequent turns restore the session's latest
// checkpoint and run forward with the new turn's input, then record the new head checkpoint.
public ValueTask<HostedWorkflowRunResult> RunOrResumeAsync(
string sessionId, object input, CancellationToken ct = default);
}
```
For agents, the application uses `AgentSessionStore` directly: `GetSessionAsync(agent, id)` creates a
session on miss and returns an independent instance per call (so concurrent calls can fork the same
stored state — for example branching from a `previous_response_id` or managing several `conversation`
ids side by side — without one branch observing another's in-flight mutations). The store performs no
cross-call locking; an application that needs concurrent runs against the same id to be serialized owns
that coordination. `SaveSessionAsync(agent, id, session)` persists post-run, including under a newly
minted `resp_*` id when the protocol mints a new continuation id. `DeleteSessionAsync` uses the new
store method. No agent-side holder is needed: create-on-miss already lives in the store, so a
pass-through wrapper would only bind the `agent` argument.
`HostedWorkflowState` defaults to `CheckpointManager.CreateInMemory()` and an in-memory
`sessionId -> CheckpointInfo` cursor. Because the checkpoint store is already `sessionId`-keyed but
`CheckpointInfo` carries no ordering, the holder remembers the head checkpoint per session so
`RunOrResumeAsync` can resume the correct one. On subsequent turns it restores that checkpoint to
rehydrate accumulated workflow state and then runs the workflow forward with the new turn's input,
rather than continuing a halted run with no input (which would wait for input
indefinitely). For agent (chat-protocol) workflows the new input is accompanied by a `TurnToken` so the
turn is driven. When the in-memory cursor misses (a new holder or a process restart), the holder falls
back to `CheckpointManager.GetLatestCheckpointAsync(sessionId)`, so a durable `CheckpointManager` resumes
correctly across restarts (the default in-memory manager does not persist, so a restart starts fresh). A
resume that produces no events is logged as a warning (possible stale checkpoint or mismatched input).
Concurrency depends on how the holder is constructed. With a single shared workflow instance, concurrent runs
are not supported, because a workflow instance cannot be run by two runners at once; process turns one at a
time. With a workflow factory
(`Func<CancellationToken, ValueTask<Workflow>>`) it builds a fresh instance per run by default, so independent
sessions run in parallel; a resume rehydrates a fresh instance
from the session's checkpoint in the shared store, and concurrent turns against the same session id remain the
application's coordination responsibility. Passing `cacheWorkflow: true` instead builds the workflow once,
lazily on first use, and reuses it (a deferred, cached target that — like the instance — cannot run concurrent
turns). A
streaming counterpart, `RunOrResumeStreamingAsync`, yields the turn's `WorkflowEvent`s as they occur (for
example to render agent updates over the Responses SSE wire) and records the head checkpoint once the
stream is fully enumerated, keeping the blocking and streaming workflow paths in lockstep.
Because `RunOrResumeAsync`/`RunOrResumeStreamingAsync` are generic over the input type, the application
adapts the Responses input into the workflow's start-executor input type at the call site (for example
parsing a structured payload into a typed record), without coupling the holder to a specific wire type.
## Non-goals for v1
- ChatCompletions / Conversations helper surfaces (the facade is named so `OpenAIChatCompletions` can
follow).
- Changing `MapOpenAIResponses` public behavior.
- A new package or an OpenAI-SDK-typed reimplementation.
- Durable/pluggable workflow checkpoint-cursor storage (in-memory default only for v1).
## Security responsibilities (application-owned)
- Authenticate the caller before using any `GetSessionId(...)` result.
- Authorize and bind the candidate id to the authenticated principal/tenant before using it as an
`AgentSessionStore` key or a workflow checkpoint session id.
- For multi-user hosts, wrap the store with `IsolationKeyScopedAgentSessionStore` (for example via
`UseClaimsBasedAgentIsolation(...)`), so the session namespace is scoped per principal.
- Persist session/checkpoint state only after the run or stream has completed.
## E2E Code Samples
### Agent over Responses, app-owned route (non-streaming + SSE)
```csharp
var agent = /* an AIAgent */;
AgentSessionStore sessionStore = new InMemoryAgentSessionStore(); // in-memory session store
app.MapPost("/responses", async (HttpContext http, CancellationToken ct) =>
{
using var doc = await JsonDocument.ParseAsync(http.Request.Body, cancellationToken: ct);
JsonElement body = doc.RootElement;
// App owns auth + id trust decisions.
string? candidate = OpenAIResponses.GetSessionId(body);
string sessionId = Authorize(http.User, candidate) ?? OpenAIResponses.CreateResponseId();
var run = OpenAIResponses.ToAgentRunRequest(body);
var session = await sessionStore.GetSessionAsync(agent, sessionId, ct);
string responseId = OpenAIResponses.CreateResponseId();
if (body.TryGetProperty("stream", out var s) && s.GetBoolean())
{
http.Response.ContentType = "text/event-stream";
var updates = agent.RunStreamingAsync(run.Messages, session, run.Options, ct);
await foreach (var frame in OpenAIResponses.WriteResponseStreamAsync(updates, responseId, sessionId, ct))
{
await http.Response.WriteAsync(frame, ct);
await http.Response.Body.FlushAsync(ct);
}
await sessionStore.SaveSessionAsync(agent, responseId, session, ct);
return Results.Empty;
}
var result = await agent.RunAsync(run.Messages, session, run.Options, ct);
await sessionStore.SaveSessionAsync(agent, responseId, session, ct);
return Results.Json(OpenAIResponses.WriteResponse(result, responseId, sessionId));
});
```
### Workflow over Responses with checkpoint resume
Workflow checkpoint resume requires a **stable** session key across turns. `previous_response_id` changes
every turn, so it is not a valid checkpoint key; use the `conversation` id (constant for the conversation).
Because `GetSessionId(...)` prefers `previous_response_id`, a workflow route reads the conversation id
directly rather than calling `GetSessionId(...)`.
```csharp
var state = new HostedWorkflowState(workflow); // in-memory checkpoints + cursor
app.MapPost("/responses", async (HttpContext http, CancellationToken ct) =>
{
using var doc = await JsonDocument.ParseAsync(http.Request.Body, cancellationToken: ct);
JsonElement body = doc.RootElement;
// Stable, authorized checkpoint key. GetConversationId(...) reads the conversation id (string or object).
string sessionId = Authorize(http.User, GetConversationId(body))
?? OpenAIResponses.CreateResponseId();
var run = OpenAIResponses.ToAgentRunRequest(body);
// Runs forward on first call, resumes from the session's head checkpoint thereafter.
var result = await state.RunOrResumeAsync(sessionId, run.Messages, ct);
return Results.Json(OpenAIResponses.WriteResponse(result.AsAgentResponse(),
OpenAIResponses.CreateResponseId(), sessionId));
});
```
-500
View File
@@ -1,500 +0,0 @@
---
status: proposed
contact: eavanvalkenburg
date: 2026-07-22
deciders: eavanvalkenburg
consulted:
informed:
---
# Feature-usage telemetry via an accumulating bitmask
> Companion design for [ADR-0033](../decisions/0033-feature-usage-bitmask-user-agent.md).
> The per-language bit tables, encoding, opt-out, and governance live in
> [feature-usage-bit-registry.md](feature-usage-bit-registry.md). The registry
> allocates indexes; package-local `FeatureIndex` declarations implement them.
## What is the goal of this feature?
Give the Agent Framework team a lightweight signal about **which framework
features are actually exercised** at runtime (not merely installed), so we can
prioritise investment based on real usage. We emit a single small number — a
*feature mask* — on the User-Agent that already goes out with each request.
**Reach is deliberately bounded.** The mask accumulates from *all* feature usage,
but the `feat=` token is only stamped through an explicit allowlist of
**first-party Azure/Foundry client pipelines** whose User-Agent telemetry the
team can ingest (initially Foundry/Azure OpenAI). We do **not** send the token to
third-party providers (OpenAI direct, Anthropic, Bedrock, Gemini, Ollama,
Mistral), or to an Azure service merely because its hostname is first-party;
doing so would leak a deployment fingerprint into logs we cannot read (see
[Emission](#emission)).
The current candidate uses package-level bits plus selected major capabilities:
one bit per orchestration pattern (sequential / concurrent / group-chat /
magentic / handoff), **one bit per built-in context/history provider**, selected
skill source types, and separate Foundry chat/agent/memory/evals/toolbox bits
(plus embedding in Python).
See the
[registry](feature-usage-bit-registry.md). ADR-0033 still leaves final v1
granularity open. The refreshed candidate assigns 63 Python indexes and 52 .NET
indexes. V1 uses 128 bits, leaving 65 Python and 76 .NET positions for additive
growth.
Success metric: within one release after rollout, ≥80% of **eligible,
framework-created** first-party (Foundry) requests carry a **non-empty** feature
token whose mask reflects features activated **after** client construction (i.e.
the token is live, not frozen — see the request-time stamping requirement
below). This measures transport coverage, not feature invocation volume.
Secondary: ability to describe which process-lifetime feature bits are observed
together in eligible traffic (e.g. "requests observed from processes that have
used workflows"). Repeated requests carrying a bit are not additional uses.
This is done **transparently**: the bit registry is public, the emitted value is
human-decodable, and a dedicated `AGENT_FRAMEWORK_FEATURE_MASK_DISABLED`
disables the mask while preserving the base User-Agent. Python's existing
`AGENT_FRAMEWORK_USER_AGENT_DISABLED` continues to suppress its entire
User-Agent contribution, mask included.
## What is the problem being solved?
Today we only know which packages are *installed* (from package telemetry) or
that *some* Agent Framework call happened (the existing
`agent-framework-python/{version}` User-Agent). We have no usage-based signal
about feature combinations, and no way to tell that, say, a process uses
workflows + MCP + Foundry together. Collecting this through bespoke events would
add cost and new data flows; folding a tiny accumulating integer into telemetry
we already send is far cheaper and easier to reason about for privacy.
## Mechanism
### Process-global accumulator in `core`
The accumulator and its helpers live in the existing
`agent_framework/_telemetry.py` (alongside `get_user_agent()` /
`prepend_agent_framework_to_user_agent()`), so the User-Agent machinery stays in
one module. It owns a process-global 128-bit accumulator. Python's arbitrary-size
`int` stores it directly. A **dedicated**
`AGENT_FRAMEWORK_FEATURE_MASK_DISABLED` that drops **only** the feature mask
while keeping the base `agent-framework-python/{version}` User-Agent is
introduced by this design. The existing Python
`AGENT_FRAMEWORK_USER_AGENT_DISABLED` continues to drop the whole User-Agent
contribution, mask included:
```python
# agent_framework/_telemetry.py (same module as get_user_agent)
# IS_TELEMETRY_ENABLED already defined here (AGENT_FRAMEWORK_USER_AGENT_DISABLED)
FEATURE_MASK_DISABLED_ENV_VAR = "AGENT_FRAMEWORK_FEATURE_MASK_DISABLED"
REGISTRY_VERSION = 1
_feature_mask = 0
_feature_mask_lock = threading.Lock()
def _feature_mask_enabled() -> bool:
"""Mask is on unless the UA is disabled or the dedicated flag is set."""
if not IS_TELEMETRY_ENABLED:
return False
return os.environ.get(FEATURE_MASK_DISABLED_ENV_VAR, "false").lower() not in ("true", "1")
def mark_feature_used(index: int) -> None:
"""OR a feature bit into the process-global mask.
Called the first time a feature is exercised. Cheap and idempotent;
a no-op when the feature mask is disabled.
"""
global _feature_mask
if not _feature_mask_enabled():
return
if not 0 <= index < 128:
raise ValueError(f"Feature index must be in range 0..127, got {index}")
with _feature_mask_lock:
_feature_mask |= 1 << index
def get_feature_token() -> str | None:
"""Return ``v<version>.<hex_mask>`` for the accumulated mask, or None."""
if not _feature_mask_enabled() or _feature_mask == 0:
return None
return f"v{REGISTRY_VERSION}.{_feature_mask:x}"
```
- **Per package/feature, usage-based:** `mark_feature_used()` is called at the
feature's first meaningful activation, never at import/install time. For
operational clients, tools, providers, and hosts, activation is the first
public operation that exercises the capability. Construction is a valid mark
point only when construction itself performs the capability (for example,
registering/starting runtime resources), not merely because a DI container
instantiated an otherwise-unused object.
- **Process-global and monotonic — intentionally never reset.** Unlike a
per-request scheme (e.g. botocore's `contextvars` feature set that resets
between calls), our mask spans the whole process because many features are not
bound to any service request — an agent or workflow may first run, a provider
may first participate in a session, and a host may start serving independently
of the later request that emits the token. The single global
mask is the only scope that can represent them, and its monotonic "usage so
far" growth is the intended semantic, not a bleed bug. Concurrency-safe via the
module lock (Python) / two atomic 64-bit lanes in .NET.
- **Binary and non-countable.** A set bit means "this feature was observed at
least once in this process before this request." Repeating that bit on every
later eligible request does not represent additional uses and must not be
interpreted as request, invocation, agent, user, or tenant counts.
- **No scoped enable/disable bookkeeping.** Making the mask exact per operation
would add hot-path state changes, context propagation, and reset/error-path
handling. It would also produce a more detailed behavioral trace and therefore
increase privacy sensitivity. V1 deliberately keeps the coarser process-level
Boolean.
- **Token is safe by construction.** The emitted value is `v{int}.{hex}`
characters limited to `[0-9a-fv.]` — so no header-injection sanitization is
required. A 128-bit mask is at most 32 hex characters (contrast botocore,
which must sanitize and cap arbitrary component strings).
- **Private API.** `mark_feature_used`, `get_feature_token`, `apply_feature_token`
and the mask itself are internal helpers; only the emitted token and the
per-language registry tables are the stable, decodable contract.
- **No import cycles:** the accumulator lives in core, while each package owns
private index constants for its own features and calls the core marker. Core
never imports optional packages.
### Interpretation contract
At time 1, Agent A in a worker can use MCP and a Foundry chat client. At time 2,
Agent B in the same worker can make a normal Foundry chat call without MCP. The
time-2 request still carries the MCP bit because MCP was observed earlier in the
process.
That request means only "this process has used MCP." It does not mean Agent B
used MCP, that MCP was used on the time-2 request, or that two requests carrying
the bit equal two MCP uses. Without a separate stable process identifier, the
signal also cannot produce unique-process counts. Supported analysis is limited
to coarse observed-feature prevalence and feature co-occurrence, with the
request-weighting limitation called out explicitly.
### Bit constants
The registry is the allocation authority. Each package defines a private,
hand-written `FeatureIndex` IntEnum (or equivalent constants) containing only
the rows it owns. Core owns core indexes plus the accumulator; optional packages
can allocate and ship new indexes without requiring a core release after the
marker API exists.
```python
# agent_framework_foundry/_feature_usage.py
from enum import IntEnum
from agent_framework._telemetry import mark_feature_used # pyright: ignore[reportAttributeAccessIssue]
class FeatureIndex(IntEnum):
FOUNDRY_CHAT_CLIENT = 48
class RawFoundryChatClient:
async def _send_request(self) -> None:
mark_feature_used(FeatureIndex.FOUNDRY_CHAT_CLIENT)
...
```
A repository validation test reads every package-local declaration and the
matching language/version table. It fails when an index is out of range, missing
from the registry, duplicated/overlapping across packages, or mapped to the wrong
id. For reference, in v1 `FoundryChatClient` → index 48,
`FoundryAgent` → index 49, Foundry memory → index 50.
### Usage activation points
- **Clients/embeddings/evals:** first outbound operation.
- **Tools/MCP:** first connection, discovery, or invocation that exercises the
tool surface.
- **Context/history providers:** first provider hook or load/save operation, not
constructor-only registration.
- **Agents/workflows/orchestrations:** first run/build/start operation that
activates the defined runtime.
- **Hosting:** first serve/start/route activation.
- **Constructor marking:** allowed only when construction itself performs one of
those activations or acquires/registers the runtime resource.
## Emission
**One path in v1: the User-Agent `feat=` token, stamped at request time on an
explicit allowlist of first-party Azure/Foundry client pipelines only.**
Marking (`mark_feature_used`) is **universal** — every feature sets its index
regardless of provider. Only **emission** is scoped. A user who never calls a
first-party endpoint emits no token; this is the honest, intended behaviour (no
third-party leakage, no signal we couldn't read anyway).
The existing base User-Agent behavior (`agent-framework-python/{version}` plus
any dynamically detected hosting prefix) is unchanged; packages continue using
their current `default_headers`, `user_agent`, suffix, or policy mechanisms.
`get_user_agent()` stays base-only (no `feat=`). The `feat=` token is
**separate**, added **only** by eligible Azure/Foundry clients, and
**re-evaluated on each request** so it reflects the mask accumulated so far. A
helper stamps it:
This request-time read does not make the signal request-scoped. The payload
remains the process-global Boolean history described above.
```python
# agent_framework/_telemetry.py
def apply_feature_token(user_agent: str) -> str:
"""Append/refresh the live ``(feat=v<ver>.<hex>)`` comment on a UA string.
Re-reads the current mask on every call, so newly accumulated bits are
reflected immediately. Idempotent: replaces an existing ``(feat=...)``
comment rather than appending a second.
"""
token = get_feature_token() # None when disabled or mask == 0
base = _strip_feature_comment(user_agent)
return f"{base} (feat={token})" if token else base
```
Emission requires **both**:
1. an explicitly approved framework client/pipeline family; and
2. the actual request's normalized HTTPS origin matching that family's reviewed
first-party origin allowlist.
Credentials, `use_azure`, or an Azure-named setting alone do not approve a
destination. Approval depends on the **resolved origin**: customer-specific
subdomains on reviewed Azure/Foundry suffixes remain eligible even when supplied
through `base_url` / `AZURE_OPENAI_BASE_URL`, while customer gateways and unknown
OpenAI-compatible origins are denied by default. The check runs on every actual
request, including redirect hops; a cross-origin or otherwise unapproved redirect
removes `(feat=...)` before sending.
Eligible first-party clients install a **request hook** that performs this
classification and calls `apply_feature_token()`:
- **OpenAI-SDK clients created by Agent Framework**: construct the underlying
client with
`http_client=DefaultAsyncHttpxClient(event_hooks={"request": [_stamp_feat_hook]})`.
Using OpenAI's `DefaultAsyncHttpxClient` preserves the SDK's redirect,
connection-limit, and timeout defaults; a plain `httpx.AsyncClient` must not
replace them. The hook adds or removes the token based on the approved pipeline
plus actual-origin classification. Caller-supplied clients/transports are not
replaced or patched.
- **azure-core pipeline clients**: start with `AIProjectClient` paths whose
telemetry is confirmed ingestible. When Agent Framework constructs/configures
an approved pipeline, add a separate per-call `SansIOHTTPPolicy` whose
`on_request` performs the same actual-origin check and calls
`apply_feature_token()` on
`request.http_request.headers["User-Agent"]`. Do not stamp `SearchClient`,
`CosmosClient`, or another Azure client merely because it is first-party; add
it to the allowlist only after confirming the data path. This mirrors .NET's
request-time `PipelinePolicy` exactly.
This fixes the frozen-at-construction problem: the token is materialised at
**send time**, not client-init time, so it carries features activated after the
client was created. It also confines the token to first-party endpoints. Caller-owned
clients are not patched, and toolkit-owned clients without a supported public
hook are outside v1 coverage.
Encoding uses the RFC 7231 **comment** form `(feat=v1.<hex>)` (metadata, not a
product token), placed after the agent-framework product token, e.g.:
```text
foundry-hosting/agent-framework-python/1.2.3 (feat=v1.2a)
```
### OpenTelemetry — not in v1
An OTel span attribute carrying the same value was considered but **deferred —
primarily for privacy, not complexity**. Unlike the first-party-only UA token, a
span attribute broadcasts the feature-combination fingerprint into the user's
**general** telemetry pipeline, which is commonly exported to third-party APM
vendors (Datadog, Honeycomb, …) — re-introducing exactly the leakage the
first-party scoping was chosen to avoid. (It also carries a cardinality footgun:
a monotonically-growing, combinatorial value must never become a metric
dimension.) The version prefix leaves the door open to add it later **if** the
User-Agent path cannot answer a concrete query and there is an acceptable
scoped/redacted variant; v1 ships the UA path only. See
[ADR-0033 → option C](../decisions/0033-feature-usage-bitmask-user-agent.md#considered-options).
## API Changes
New **internal cross-package** surface in
`agent_framework._telemetry` (not exported from `agent_framework`):
- `mark_feature_used(index: int) -> None`
- `get_feature_token() -> str | None` — returns `v<ver>.<hex>` or `None`.
- `apply_feature_token(user_agent: str) -> str` — live, idempotent UA stamper
used by first-party request hooks.
- `FEATURE_MASK_DISABLED_ENV_VAR` constant — the dedicated mask-only opt-out env
var name (`AGENT_FRAMEWORK_FEATURE_MASK_DISABLED`).
Each package also adds a private package-local `FeatureIndex` declaration for
the rows it owns. The dedicated mask-only opt-out and Python's existing
whole-User-Agent opt-out gate the Python mask; see [Opt-out](#opt-out).
Behavioural change to existing API:
- `get_user_agent()` / `prepend_agent_framework_to_user_agent()` are
**unchanged** — they keep returning the base UA with no `feat=` token. The
token is added only by first-party request hooks via
`apply_feature_token()`.
No breaking changes: when the mask is empty or disabled, for any non-first-party
client, or for an injected client outside the supported-hook set, output is
byte-for-byte identical to today.
## Opt-out
The dedicated mask-only opt-out is shared by both SDKs. Python also retains its
pre-existing whole-User-Agent opt-out:
| Env var | SDKs | Effect |
| --- | --- | --- |
| `AGENT_FRAMEWORK_FEATURE_MASK_DISABLED` | Python and .NET | disables **only** the feature mask; the base `agent-framework-<lang>/{version}` User-Agent is still sent |
| `AGENT_FRAMEWORK_USER_AGENT_DISABLED` | Python (existing behavior) | disables the **entire** Python AF User-Agent contribution, mask included |
The flags accept `true`/`1` (case-insensitive). The dedicated flag lets a
privacy-conscious user keep contributing the SDK identity/version (useful for
support and compat triage) while withholding the feature-usage signal. The mask
is also disabled implicitly whenever Python's whole User-Agent is disabled. A
new whole-User-Agent opt-out for .NET is outside this design.
## E2E example
```python
from agent_framework import Agent
from agent_framework_foundry import FoundryChatClient
from agent_framework_openai import OpenAIChatClient
# First-party (Foundry) client: request hook stamps the live feat token.
agent = Agent(client=FoundryChatClient(...), instructions="...")
# Agent use marks bit 0; FoundryChatClient marks bit 48
await agent.run("Hello")
# Outgoing request to Foundry carries:
# User-Agent: agent-framework-python/1.2.3 (feat=v1.<mask-at-send-time>)
# Third-party client: NO feat token is added (no first-party hook).
other = Agent(client=OpenAIChatClient(...), instructions="...")
await other.run("Hi")
# Outgoing request to OpenAI carries only:
# User-Agent: agent-framework-python/1.2.3
```
Drop only the feature mask (keep the base User-Agent):
```bash
AGENT_FRAMEWORK_FEATURE_MASK_DISABLED=true python app.py
# Foundry request User-Agent: agent-framework-python/1.2.3 (no (feat=...) comment)
```
Python only: use the existing flag to drop its entire User-Agent contribution
(mask included):
```bash
AGENT_FRAMEWORK_USER_AGENT_DISABLED=true python app.py
```
## .NET mapping
- Core owns `FeatureUsage.MarkUsed(int index)` plus the core package's private
index declaration. Each optional assembly owns a private `FeatureIndex` enum
containing only its allocated rows. These are index positions `0..127`, not
`[Flags]` values; `MarkUsed` performs the shift.
- Store the 128-bit mask as **two `long` lanes** (`low` for bits 063, `high`
for 64127). Marking touches one lane with `Interlocked.Or` where available
and a small `Interlocked.CompareExchange` loop on `netstandard2.0` / `net472`.
Read each lane atomically. Since bits only move from zero to one, a concurrent
two-lane snapshot may miss a just-added bit but can never invent or clear one;
the next request includes it.
- Format without depending on `UInt128`: if `high == 0`, emit `low` as lowercase
hex; otherwise emit `high` without leading zeros followed by `low:x16`. Cast
each signed lane to `ulong` before formatting so bits 63 and 127 are preserved.
Reject indexes outside `0..127`.
- **Emission is stamped at request time and first-party-scoped**, matching
Python. The
existing `AgentFrameworkUserAgentPolicy` / `HostedAgentUserAgentPolicy`
pipeline policies already run per request — extend them to apply the same
approved-pipeline + actual-origin classifier, append/refresh the `(feat=...)`
comment only for approved destinations, and remove it on unapproved redirect
hops. Do not register it on third-party `IChatClient`s.
- Same **wire format** (`v<version>.<hex>` comment, hex encoding) and the same
dedicated mask-only opt-out (`AGENT_FRAMEWORK_FEATURE_MASK_DISABLED`). The
**mask is decoded per language**: indexes are not shared, so a decoder must
read the language from the UA product token and select that language's table
before decoding. (.NET's policy was already request-time, so there is no
Python/.NET timing asymmetry.) Adding a .NET whole-User-Agent opt-out is
outside this design.
## Keeping the bitmap in sync
[feature-usage-bit-registry.md](feature-usage-bit-registry.md) is the published
allocation contract. Package-local `FeatureIndex` declarations are the runtime
implementation. There is deliberately **no shared numbering across languages**
and **no machine-readable registry file**.
One repository validation test gathers every package-local declaration for one
language/version and parses the matching Markdown table. It asserts:
1. every declared index is within `0..127`;
2. every `(index, id)` exactly matches one registry row;
3. the union of declarations has no duplicate/overlapping indexes;
4. every non-reserved registry row is declared exactly once.
Adding an optional-package feature therefore changes that package and the
registry, not core. If a programmatic decoder is built later, export the table
to JSON then.
### Decoding
```
UA: agent-framework-python/1.2.3 (feat=v1.2a)
│ │ └ hex mask
│ └ version
└ language → pick the Python table (version 1)
```
Read language → pick the table; read `vN` → pick that version; `AND` the hex mask
against each bit. Unknown bits (from a newer SDK than the decoder's copy of the
table) are ignored.
## Implementation plan (post-approval)
1. **Privacy approval** — confirm the first-party-only feature-combination
signal, retention, access, allowed queries, and opt-out behavior before code
ships.
2. **Core accumulator** — in `agent_framework/_telemetry.py` add the 128-bit
mask, lock, `mark_feature_used(index)`, `get_feature_token`, and
`apply_feature_token`; `get_user_agent()` stays base-only.
3. **Package-local indexes + validation** — add private `FeatureIndex`
declarations to packages and a repository test for exact registry parity,
complete coverage, range, and zero overlap.
4. **First-party request-time hooks** — use OpenAI's
`DefaultAsyncHttpxClient` for framework-created clients and the separate
azure-core `SansIOHTTPPolicy`. Require approved pipeline **and** approved
actual origin on every request/redirect hop. Verify custom origins and
cross-origin redirects never carry the token.
5. **Mark feature usage** — call `mark_feature_used(FeatureIndex.X)` at the
first meaningful activation. Operational clients/providers/tools mark on
their first real operation; build/start points mark compositional features.
Constructor-only marking requires construction itself to exercise the
capability.
6. **.NET parity** — package-local index enums plus the two atomic 64-bit lanes
with `Interlocked.Or` / compare-exchange fallback; extend existing request-time
Foundry UA policies through the shared destination classifier and formatter.
7. **Docs & tests** — update package `AGENTS.md`/skills; tests for **both**
Python opt-out paths (dedicated mask-only and existing whole-UA), the
dedicated .NET mask-only opt-out, first-party scoping, and the live
(non-frozen) UA.
## Limitations & open questions
The decision-level limitations and unresolved trade-offs — reach, per-process
(not per-call) attribution, v1 granularity, fingerprinting residue, and the OTel
question — are owned by the ADR (the dedicated mask-only opt-out is now decided
and included). See
**[ADR-0033 → Limitations](../decisions/0033-feature-usage-bitmask-user-agent.md#limitations)**
and **[Open Questions](../decisions/0033-feature-usage-bitmask-user-agent.md#open-questions-for-decider-discussion)**.
This spec is the implementation reference; it does not re-litigate those choices.
Implementation-only note:
- **Per-request hook overhead is negligible** (a flag check, one Python integer
snapshot or two atomic .NET lane reads, and a string concat per first-party
request), but benchmark the hot path once if a high-QPS Foundry scenario is in
scope.
@@ -1,631 +0,0 @@
---
status: proposed
contact: eavanvalkenburg
date: 2026-07-27
deciders: eavanvalkenburg
---
# Python function-calling loop contract and validation matrix
## Scope
This specification defines the required behavior and validation coverage for the Python function-calling loop.
It covers:
- normal local function execution;
- streaming and non-streaming response aggregation;
- tool approval request and resume;
- approved, rejected, mixed, and replayed approval rounds;
- reasoning content and opaque reasoning signatures bound to function calls;
- history persistence and service-side continuation;
- error, user-input, middleware-termination, middleware-failure, and loop-limit paths;
- provider and transport serialization of function calls and results.
The primary implementation is in `python/packages/core/agent_framework/_tools.py`. History replay behavior in
`python/packages/core/agent_framework/_sessions.py`, provider serializers, hosting packages, and UI transports are
part of the same contract when they carry function-call loop content.
## Change sensitivity
This code is high risk. Small changes can produce duplicate side effects, orphaned calls or results, invalid
provider histories, invisible streaming results, stale approval authority, or loops that never terminate.
Dropping reasoning content that a service binds to a tool call can also make an otherwise balanced call/result
transcript invalid.
Any change to the function-calling loop or its approval/history/serialization paths must:
1. identify every affected row in the scenario matrix below;
2. add or update the corresponding regression tests;
3. validate streaming updates, streaming finalization, and non-streaming output where applicable;
4. validate both model-bound history and caller-visible responses;
5. run the full core package tests plus every affected provider or transport package;
6. run source typing, test typing, and syntax checks for every affected package;
7. receive extra review focused on call/result pairing, exactly-once execution, and history replay.
A passing narrow regression test is not sufficient evidence for changes in this area.
### Contribution ownership
Issues involving this code must not be picked up by external contributors without first checking with the Agent
Framework core team. The core team must confirm the intended behavior, affected scenario-matrix rows, ownership
across core/providers/transports, and the required validation scope before implementation starts.
## Flow diagrams and code map
### Main function-calling flow
The main control flow deliberately has separate streaming and non-streaming methods. They share policy helpers, but
their output mechanics differ: one returns an aggregated `ChatResponse`; the other yields `ChatResponseUpdate`
items and is finalized by `ResponseStream`.
The diagrams use only the generic distinction between **local tools**, which Agent Framework executes, and
**hosted-service tools**, whose calls and approval decisions are owned by a remote service. Provider-specific wire
formats and regression tests appear later in the scenario matrix.
```mermaid
flowchart TD
Entry["FunctionInvocationLayer.get_response(...)"]
Setup["Prepare middleware, options, session, budget state,<br/>and execute_function_calls partial"]
Enabled{"Function invocation enabled?"}
Direct["Delegate directly to super().get_response(...)"]
Mode{"stream?"}
NonStream["_get_response_with_function_invocation(...)"]
Stream["_stream_response_with_function_invocation(...)"]
Resolve["_resolve_approval_responses(...)<br/>runs once before the model-iteration loop"]
ApprovalAction{"approval action"}
Immediate["Return/yield terminal result or user-input request<br/>without another model call"]
ApprovalPolicy["Record approval executions;<br/>apply stop/function-call-limit policy"]
Model["Call super_get_response(...)<br/>response may contain reasoning + function_call"]
Process["_process_model_function_calls(...)"]
FunctionAction{"function-processing action"}
Execute["_execute_function_calls(...)"]
Try["_try_execute_function_calls(...)"]
Single["_execute_single_function_call(...)"]
Handle["_handle_function_call_results(...)"]
PostCallPolicy["Record executions; apply error/function-call-limit policy;<br/>reset required tool choice"]
Advance["_prepare_messages_for_next_iteration(...)"]
More{"iteration budget remains?"}
Final["Final model call with tool_choice = none<br/>and deterministic fallback if needed"]
Output["Return ChatResponse or complete ResponseStream"]
Entry --> Setup --> Enabled
Enabled -- no --> Direct
Enabled -- yes --> Mode
Mode -- no --> NonStream
Mode -- yes --> Stream
NonStream --> Resolve
Stream --> Resolve
Resolve --> ApprovalAction
ApprovalAction -- return --> Immediate --> Output
ApprovalAction -- stop --> ApprovalPolicy
ApprovalAction -- continue --> ApprovalPolicy
ApprovalPolicy --> More
Model --> Process
Process --> Execute --> Try --> Single --> Handle --> FunctionAction
FunctionAction -- return --> Output
FunctionAction -- stop --> PostCallPolicy
FunctionAction -- continue --> PostCallPolicy
PostCallPolicy --> Advance
Advance --> More
More -- yes --> Model
More -- no --> Final --> Output
```
Code-reading landmarks:
- `get_response(...)` owns setup and selects the response mode.
- `_get_response_with_function_invocation(...)` owns non-streaming aggregation.
- `_stream_response_with_function_invocation(...)` owns streamed emission/finalization.
- `_resolve_approval_responses(...)` handles only inbound approval decisions.
- `_process_model_function_calls(...)` handles only calls from a completed model response.
- `_try_execute_function_calls(...)` decides approval/declaration/execution behavior for a batch.
- `_replace_approval_contents_with_results(...)` is the occurrence-aware approval transcript normalizer.
- `FunctionInvocationLayer._update_function_invocation_continuation_state(...)` updates continuation state after
every service response. Provider layers may override it to carry provider-specific continuation metadata into
the next service call, but must delegate to the base implementation so generic conversation continuation remains
synchronized with the active `AgentSession`.
### Approval pause and resume
```mermaid
sequenceDiagram
participant Caller
participant History as HistoryProvider
participant Layer as FunctionInvocationLayer
participant Tool
participant Model
Caller->>Layer: Initial user request
Layer->>Model: Messages + tools
Model-->>Layer: reasoning content + function_call
Layer->>Layer: Tool requires approval
Layer-->>Caller: function_call + function_approval_request
Caller->>Layer: function_approval_response
Layer->>Layer: Copy caller-owned messages
Layer->>Layer: _resolve_approval_responses(...)
alt approved
Layer->>Tool: Execute exactly once
Tool-->>Layer: result or exception
Layer->>Layer: Create terminal function_result
else rejected
Layer->>Layer: Create synthetic rejection function_result
end
Layer-->>Caller: Terminal result message/update
alt tool requests more user input
Layer-->>Caller: User-input request with assistant role
else middleware terminates
Layer-->>Caller: Termination result
else error limit reached
Layer->>Model: Normalized reasoning/call/result history, tools disabled
Model-->>Layer: Final assistant response
Layer-->>Caller: Final assistant response
else continue normally
Layer->>Model: Normalized reasoning/call/result history
Model-->>Layer: Final assistant response or another function_call
Layer-->>Caller: Final assistant response / continued loop
end
Layer-->>History: Persist caller input + returned response
Note over History: Later model replay filters approval request/response wrappers
```
The terminal result is caller-visible in both modes. The private normalized message copy is model-visible. The
original caller input and earlier response remain unchanged.
### Reasoning-bound function-call groups
Some hosted services bind reasoning content or an opaque reasoning signature to the function call that follows it.
For those services, reasoning is not optional decoration; it is part of the provider-valid function-call group.
```mermaid
flowchart TD
Response["Assistant response:<br/>reasoning content + function_call"]
Group["One logical reasoning/function-call group"]
Owner{"local or hosted-service tool?"}
Local["Local execution"]
Hosted["Hosted service owns tool execution/state"]
Result["Terminal function_result or hosted result"]
Continuation{"continuation mode"}
Stateless["Stateless or framework-history replay"]
Replayable{"reasoning payload/signature<br/>is replayable?"}
Replay["Replay reasoning + call + result atomically"]
Reject["Fail before the service call;<br/>do not send a lossy transcript"]
Service["Hosted-service continuation"]
Reference["Reference service-stored reasoning/call;<br/>send only the new result or approval decision"]
Compact{"compaction needed?"}
Atomic["Keep or exclude the complete<br/>reasoning/call/result group"]
Caller["Caller-visible response retains reasoning<br/>with the function-call turn"]
Response --> Group --> Owner
Group --> Caller
Owner -- local --> Local --> Result
Owner -- hosted service --> Hosted --> Result
Result --> Compact
Compact -- yes --> Atomic --> Continuation
Compact -- no --> Continuation
Continuation -- stateless / local history --> Stateless --> Replayable
Replayable -- yes --> Replay
Replayable -- no --> Reject
Continuation -- service-managed --> Service --> Reference
```
The generic contract is:
- reasoning content remains ordered immediately before or alongside the function call it explains;
- a terminal result does not replace or discard the reasoning/call portion of the active group;
- stateless replay includes the service-required reasoning payload or opaque signature;
- if required reasoning cannot be reconstructed, the adapter fails before sending invalid or lossy history;
- service-managed continuation may rely on the hosted service's stored reasoning/call items and send only new
outputs or approval decisions;
- compaction keeps or removes the entire reasoning/call/result group atomically.
In the code, core response aggregation preserves reasoning `Content` items, compaction annotations bind reasoning to
the tool-call group, and provider adapters serialize or reconstruct the provider-specific reasoning representation.
### Approval correlation, replay, and reused ids
`call_id` is not globally unique forever. The normalizer therefore tracks open logical occurrences in transcript
order instead of keeping one global result per id.
```mermaid
flowchart TD
Scan["Scan normalized messages in order"]
Kind{"content type"}
Call["function_call:<br/>open a call occurrence"]
Request["function_approval_request"]
Bind{"unbound call occurrence<br/>with same call_id?"}
BindExisting["Bind request id to existing occurrence<br/>and remove wrapper"]
Duplicate{"same request identity<br/>already restored?"}
DropDuplicate["Remove replayed duplicate wrapper"]
Restore["Restore embedded function_call<br/>as a new occurrence"]
Placeholder["function_result with APPROVAL_PENDING:<br/>attach placeholder to open occurrence"]
Completed["terminal function_result:<br/>close earliest open occurrence"]
Response["function_approval_response"]
Pending{"response still pending?"}
RemoveOld["Remove already-resolved historical response"]
Decision{"approved?"}
Approved["Pop next execution result for this call_id"]
Rejected["Create synthetic rejection result"]
HasPlaceholder{"occurrence has placeholder?"}
Replace["Replace placeholder and remove response wrapper"]
ReplaceResponse["Replace response wrapper with terminal content"]
Close["Close occurrence; append terminal content<br/>to resumed response"]
Next["Continue scan"]
Scan --> Kind
Kind -- function_call --> Call --> Next
Kind -- approval request --> Request --> Bind
Bind -- yes --> BindExisting --> Next
Bind -- no --> Duplicate
Duplicate -- yes --> DropDuplicate --> Next
Duplicate -- no --> Restore --> Next
Kind -- pending placeholder --> Placeholder --> Next
Kind -- terminal result --> Completed --> Next
Kind -- approval response --> Response --> Pending
Pending -- no --> RemoveOld --> Next
Pending -- yes --> Decision
Decision -- yes --> Approved --> HasPlaceholder
Decision -- no --> Rejected --> HasPlaceholder
HasPlaceholder -- yes --> Replace --> Close --> Next
HasPlaceholder -- no --> ReplaceResponse --> Close --> Next
Next --> Kind
```
This flow corresponds to `_ApprovalCallOccurrence`, `_collect_approval_responses(...)`, and
`_replace_approval_contents_with_results(...)`.
### History and service-side continuation
```mermaid
flowchart LR
Store["History backing store<br/>(may retain approval wrappers for audit)"]
Load{"HistoryProvider.load_messages?"}
Filter["_filter_approval_control_messages(...)"]
Context["SessionContext model history:<br/>function_call + terminal function_result"]
Current["Current caller input:<br/>new function_approval_response"]
Layer["FunctionInvocationLayer private copy"]
Local{"local or hosted-service approval?"}
LocalResult["Execute locally and normalize to function_result"]
Hosted["Hosted-service adapter"]
StoredRequest["Prior service-issued approval request"]
NewResponse["Current hosted approval decision"]
Skip["Do not replay the stored request inline"]
Send["Send the approval decision exactly once"]
Later["Later turn"]
Manual["Manual-history caller"]
Store --> Load
Load -- yes --> Filter --> Context --> Layer
Load -- no --> Layer
Current --> Layer
Layer --> Local
Local -- local --> LocalResult --> Later
Local -- hosted service --> Hosted
StoredRequest --> Hosted --> Skip
NewResponse --> Hosted --> Send --> Later
Later --> Store
Manual -. owns equivalent filtering .-> Layer
```
When `load_messages=False`, no history is replayed and the history filter is intentionally not invoked. Callers
that manually replay messages own the equivalent rule: do not resend an approval response after its terminal result.
## Normative contract
### Function calls and results
- Every actionable local `function_call` produces exactly one terminal `function_result`, unless execution pauses
for a new user-input request or the run is aborted by `MiddlewareFailure`.
- An ordinary exception raised by function middleware or a tool body becomes one terminal error `function_result`
and the loop continues; `MiddlewareFailure` is the loop's only fail-closed escape: it is never converted into a
tool result, the in-flight parallel batch is cancelled, no further tool call starts, no further model turn is
consumed, and the exception propagates to the caller (for streaming runs, when the stream is consumed). On a
service-managed conversation the loop first settles the aborted batch — one error `function_result` per dangling
call (approval-response wrappers unwrap to their underlying calls; hosted-tool approvals are left to their own
provider protocol), submitted with `tool_choice="none"` in a single extra request — so the hosted thread is not
left ending in unresolved function calls that the service would reject on the session's next request; the
persisted continuation then advances to the settlement response (for response-ID continuations the settled
endpoint is the new handle; for conversation-object ids the advance is a no-op) and the settlement response is
otherwise discarded. Settlement covers the approval-resolution phase too: a fatal abort while an approved tool is
replayed settles the original, already-persisted calls. Without a service-managed conversation no extra request
is made. Batch
cancellation is cooperative: an async sibling stops at its next suspension point, while a synchronous tool body
already executing in a worker thread cannot be interrupted and may complete its side effects — its result is
discarded either way and never reaches the transcript, the model, or history. Middleware must not catch
`MiddlewareFailure` — swallowing it converts a fail-closed abort back into a running, possibly unguarded loop.
- Parallel calls retain model order in the returned transcript.
- Reused `call_id` values are correlated by logical occurrence, not one global value per id.
- A completed function call/result pair is inert on later turns.
- Informational-only and declaration-only calls are not executed as local tools.
### Reasoning-bound calls
- Reasoning content or opaque reasoning metadata that a service binds to a function call is part of the same logical
group as that call and its terminal result.
- Active function loops preserve the reasoning content, function call, function result, and final assistant output
in caller-visible responses.
- Framework-managed/stateless replay includes the service-required reasoning representation before the paired call.
- Service-managed continuation may omit inline reasoning/call items only when the hosted service already owns them.
- Missing non-reconstructable reasoning fails explicitly before a provider request instead of silently dropping the
content.
- Foundry clients do not request `reasoning.encrypted_content` implicitly; callers may opt in explicitly when the
selected deployment supports encrypted reasoning.
- Compaction preserves or excludes the complete reasoning/call/result group atomically.
### Approval request and resume
- A tool that requires approval does not execute before an approved response.
- With an `AgentSession`, every surfaced local or hosted approval request is stored as an immutable snapshot in one
active model batch. A new surfaced batch replaces an abandoned batch instead of accumulating session state.
- Approval request IDs use the provider function `call_id`, whose conversation-level uniqueness is required for
function-call/result correlation. Duplicate request IDs within one batch are rejected as malformed.
- An inbound response is honored only when its request id matches the pending server-held snapshot.
- Approval requests replayed in inbound message history do not create, replace, or resurrect approval authority.
- The executable call id, tool name, arguments, and local or hosted tool metadata are sourced from the recorded
request, never from the response payload.
- A matched approval response consumes its pending entry once. Unmatched, duplicate, and replayed responses do not
reach local execution.
- Tool lookup uses the recorded name against the current registry. A same-name implementation upgrade is allowed;
removing the name prevents local execution.
- Only the strict boolean `True` grants approval. Missing decisions and non-boolean values are rejection, not consent.
- Direct chat-client invocation without an `AgentSession` preserves pass-through compatibility, matching .NET;
authorization sinks still require strict `True`.
- An approved tool executes exactly once.
- A rejected tool executes zero times and produces one synthetic rejection `function_result` using the original
function `call_id`.
- The resumed response contains the newly resolved approved and rejected terminal results before any final assistant
message.
- Streaming yields the same logical result content and ordering as non-streaming output and
`ResponseStream.get_final_response()`.
- The function invocation layer normalizes a private copy of caller messages. It must not mutate the caller's
approval `Message`, approval `Content`, or an earlier returned response.
- Approval-time `UserInputRequiredException` and `MiddlewareTermination` return immediately without another model
call.
### Approval control content
- `function_approval_request` and `function_approval_response` are control-plane contents, not durable model
transcript items.
- A current hosted approval response must be sent once on the immediate resume request.
- AG-UI removes a local approval response from its request and snapshot replay when a terminal result belongs to an
already-consumed occurrence, including result-before-response replay. A client-authored result in the occurrence
that is still registered as pending does not prove completion: AG-UI removes that result, keeps the validated
response for local execution, and leaves hosted approval responses as provider protocol data.
- Hosted AG-UI approval interrupts expose an accept/reject decision only; argument edits are rejected because the
hosted provider executes the server-owned request rather than client-edited arguments.
- AG-UI tool approval resumes accept the standard `approved` decision and full-replacement `editedArgs` payload.
Existing MAF clients remain compatible through the `accepted` decision alias and direct partial argument edits.
- An AG-UI `cancelled` resume is a valid terminal decision, not a run error. In a resume covering parallel open
interrupts, resolved siblings still execute and cancelled calls do not. An identical cancellation retry during
the retained terminal window also completes normally without restoring authority.
- AG-UI Approval State capacity is enforced independently for each trusted application scope. Abandoned pending
authority expires after its configured window, and indeterminate execution records remain non-retryable until
their separate safety window permits reclamation. Reclamation never recreates approval authority.
- A server-issued approval request must not be replayed inline during service-side continuation.
- History providers may retain approval control contents in their backing store for audit, but base history replay
filters them before later model calls.
- Callers that manually own and replay message history without a loading `HistoryProvider` must likewise omit a
previously submitted approval response from later continuation requests.
### History and continuation
- Model-bound history contains one function call/result pair per completed logical occurrence.
- Append-only history must not replay stale approval request/response wrappers to the model.
- Framework-managed and service-managed continuation must preserve the same logical call/result transcript.
- A streaming response rebuilt from updates by an intermediate middleware must carry over the inner response's
conversation id and its internal-conversation-id marker, so framework-managed continuation appends only the latest
message instead of replaying a transcript the provider already holds. The rebuilt response mirrors the inner
conversation id exactly, including clearing it, and never retains an id emitted by an earlier service call in the
same turn.
- A trusted terminal result consumes the corresponding approval authority in explicit stateless replay; a result in a
server-registered pending occurrence cannot consume that authority before local execution.
## Scenario-to-test matrix
### Normal function invocation
| Scenario | Required invariant | Primary regression test |
|---|---|---|
| Single non-streaming call | Call, result, and final assistant message are returned in order. | `packages/core/tests/core/test_function_invocation_logic.py::test_base_client_with_function_calling` |
| String input | Flexible string input follows the same loop behavior. | `test_base_client_with_function_calling_string_input` |
| Multiple sequential rounds | Each round retains one call/result pair. | `test_base_client_with_function_calling_resets` |
| Streaming call | Call chunks, one result update, and final text are emitted in order. | `test_base_client_with_streaming_function_calling` |
| Reasoning-bound call | Finalized output retains reasoning, function call, function result, and final text. | `test_streaming_function_calling_response_includes_reasoning_and_tool_results` |
| Calls across response messages | Every actionable call is executed once. | `test_base_client_executes_function_calls_across_multiple_response_messages` |
| Parallel calls | Results retain the corresponding call ids and execution count. | `test_max_function_calls_limits_parallel_invocations`, `test_streaming_multiple_function_calls_parallel_execution` |
| Informational-only call | The call is returned but not executed or approved. | `test_informational_only_function_call_is_not_invoked`, `test_informational_only_function_call_does_not_request_approval`, `test_streaming_informational_only_function_call_is_not_invoked` |
| Declaration-only call | The call is surfaced as user input and is not executed; streaming arguments appear once while finalized request metadata remains available. | `test_declaration_only_tool`, `test_streaming_declaration_only_tool_preserves_metadata_without_duplicate_arguments` |
| Function invocation disabled | The client bypasses the invocation loop without losing invocation kwargs. | `test_function_invocation_config_enabled_false`, `test_function_invocation_config_enabled_false_preserves_invocation_kwargs`, `test_streaming_function_invocation_config_enabled_false` |
| Runtime tool changes | Added tools become available on the next iteration and retain approval behavior. | `test_add_tools_available_next_iteration`, `test_add_tools_with_approval_required_tool` |
### Approval pause and resume
| Scenario | Required invariant | Primary regression test |
|---|---|---|
| Initial approval request | Assistant response contains the original call and approval request; tool does not execute. | `test_approval_requests_in_assistant_message`, `test_streaming_approval_request_generated`, `test_streaming_approval_requests_in_assistant_message` |
| Approved non-streaming resume | Result precedes final text; tool executes once; inputs remain unchanged. | `packages/core/tests/core/test_harness_tool_approval.py::test_approval_resume_returns_result_without_mutating_inputs[non-streaming-approved]` |
| Rejected non-streaming resume | Rejection result precedes final text; tool executes zero times; inputs remain unchanged. | `test_approval_resume_returns_result_without_mutating_inputs[non-streaming-rejected]` |
| Approved streaming resume | Result update precedes final text and final response matches non-streaming shape. | `test_approval_resume_returns_result_without_mutating_inputs[streaming-approved]`, `test_streaming_approval_resume_yields_terminal_result_before_model_text[approved]` |
| Rejected streaming resume | Rejection result update precedes final text and tool executes zero times. | `test_approval_resume_returns_result_without_mutating_inputs[streaming-rejected]`, `test_streaming_approval_resume_yields_terminal_result_before_model_text[rejected]` |
| Mixed approved/rejected batch | Every call gets one correctly correlated terminal result. | `packages/core/tests/core/test_function_invocation_logic.py::test_rejected_approval` |
| Persisted approval replay | Resume executes with the prior call available. | `test_persisted_approval_messages_replay_correctly` |
| Hosted approval pass-through | Hosted requests/responses are bound to the recorded provider request and are not processed as local calls. | `test_hosted_tool_approval_response`, `test_hosted_mcp_approval_response_passthrough`, `test_session_approval_binding_reconstructs_hosted_response`, `test_mixed_local_and_hosted_approval_flow` |
| Approval-time user input | Every user-input request from one approved execution returns in order with assistant role and no extra model call; the execution consumes one call-budget unit. | `packages/core/tests/core/test_harness_tool_approval.py::test_approval_resume_returns_all_user_input_requests_without_another_model_call`, `packages/core/tests/core/test_function_invocation_logic.py::test_approval_resume_user_input_counts_toward_function_call_budget` |
| Mixed terminal result and follow-up input | Completed siblings remain tool-role while only follow-up input requests use assistant-role messages/updates. | `packages/core/tests/core/test_function_invocation_logic.py::test_approval_resume_separates_terminal_results_from_follow_up_requests`, `packages/openai/tests/openai/test_openai_chat_completion_client.py::test_mixed_approval_resume_roles_serialize_function_result_as_tool` |
| Approval-time middleware termination | Terminal result returns with no extra model call in either response mode. | `packages/core/tests/core/test_function_invocation_logic.py::test_approval_resume_honors_middleware_termination` |
| Approval re-entry after iteration budget | Pending approved calls resolve once even when prior model calls consumed `max_iterations`. | `packages/core/tests/core/test_harness_tool_approval.py::test_auto_approval_resolves_after_iteration_budget_is_exhausted` |
| Approval resume with reasoning | Model-bound resume history retains reasoning before the call and terminal result in both modes. | `packages/core/tests/core/test_harness_tool_approval.py::test_approval_resume_replays_reasoning_with_function_call_group` |
| Session-bound substituted response | A response is rebound to the immutable recorded call and cannot replace its call id, tool name, or arguments. | `packages/core/tests/core/test_function_invocation_logic.py::test_session_approval_binding_rebinds_consumes_and_rejects_duplicates` |
| Truthy non-boolean decision | Strings, integers, null, and other non-booleans do not authorize execution. | `packages/core/tests/core/test_function_invocation_logic.py::test_session_approval_binding_treats_truthy_non_boolean_as_rejection`, `packages/core/tests/core/test_types.py::test_function_approval_response_deserialization_rejects_non_boolean_decisions`, `packages/ag-ui/tests/ag_ui/test_message_adapters.py::test_function_approval_requires_real_boolean`, `packages/ag-ui/tests/ag_ui/test_approval_result_event.py::test_resolve_approval_responses_treats_non_boolean_decision_as_rejection` |
| Active batch replacement | A newly surfaced model batch replaces abandoned approval authority instead of growing session state. | `packages/core/tests/core/test_function_invocation_logic.py::test_session_approval_binding_replaces_abandoned_batch` |
| Duplicate request id | Ambiguous request IDs within one active batch fail explicitly. | `packages/core/tests/core/test_function_invocation_logic.py::test_session_approval_batch_rejects_duplicate_request_ids` |
| Tool registry changes | Same-name upgrades may execute the recorded operation; removing the recorded name executes nothing. | `packages/core/tests/core/test_harness_tool_approval.py::test_approval_resume_allows_same_name_tool_upgrade`, `test_approval_resume_does_not_execute_when_recorded_tool_disappears` |
### Approval correlation and replay
| Scenario | Required invariant | Primary regression test |
|---|---|---|
| Result matching without placeholders | Results match calls by id even when the result list is reordered. | `test_replace_approval_contents_with_results_uses_result_call_ids_without_placeholders` |
| Reused id after completion | A later round with the same id creates a second valid pair. | `test_replace_approval_contents_with_results_allows_reused_call_id_after_completion` |
| Replayed approval wrapper | A duplicated wrapper does not restore another function call. | `test_replace_approval_contents_with_results_deduplicates_replayed_approval_request` |
| Historical resolved response plus new round | The old response is removed from normalized input and is not converted into a rejection result. | `test_replace_approval_contents_with_results_ignores_already_resolved_response` |
| Multiple reused-id rounds | Approved and rejected rounds retain separate call/result occurrences. | `test_replace_approval_contents_with_results_correlates_reused_call_id_occurrences` |
| Multi-content result with reused id | Every content produced by one execution stays with that approval occurrence and cannot bleed into the next reused-id round. | `test_replace_approval_contents_with_results_keeps_multi_content_group_with_reused_call_id` |
| Follow-up request closes one occurrence | A user-input follow-up consumes only the preceding approval authority and leaves a later reused-id response pending. | `test_collect_approval_responses_consumes_matching_follow_up_request_occurrence` |
| Reused-id placeholders | Placeholder results consume approved results by occurrence. | `test_replace_approval_contents_with_results_correlates_reused_call_id_placeholders` |
| Rejected placeholder | Rejection replaces the pending placeholder instead of adding a second result. | `test_replace_approval_contents_with_results_replaces_rejected_placeholder` |
| Results reordered with placeholders | Results still match the correct call ids. | `test_replace_approval_contents_with_results_uses_result_call_ids_for_placeholders` |
| Missing result call id | A malformed result does not steal another approval's result. | `test_replace_approval_contents_with_results_skips_results_without_call_id` |
| Empty approval message cleanup | Fully consumed approval messages are removed from normalized model input. | `test_replace_approval_contents_with_results_prunes_emptied_messages` |
| Later stateless turn | A prior terminal approval response cannot execute again. | `test_resolved_approval_response_is_inert_on_later_stateless_turn` |
| Unbound or duplicate response | A response with no pending session request is removed; one request authorizes at most one response. | `test_session_approval_binding_rebinds_consumes_and_rejects_duplicates` |
| Forged inbound request history | A caller-supplied request wrapper cannot replace the server snapshot or resurrect consumed authority. | `test_session_approval_binding_does_not_trust_inbound_request_history` |
| Pending history turn | An unresolved approval batch is omitted atomically from unrelated model input while a later decision can still resume it once. | `packages/core/tests/core/test_harness_tool_approval.py::test_pending_approval_from_file_history_stays_resumable_without_model_orphan` |
| Duplicate function-call prevention | Approval normalization does not create a second call for one round. | `test_no_duplicate_function_calls_after_approval_processing` |
| Rejection call id | Rejection result uses the function call id, not only the approval id. | `test_rejection_result_uses_function_call_id` |
### Mixed batches and approval middleware
| Scenario | Required invariant | Primary regression test |
|---|---|---|
| Safe and approval-required calls in one batch | Hidden safe calls replay only with the matching visible approval. | `packages/core/tests/core/test_harness_tool_approval.py::test_mixed_batch_hides_already_approved_request_until_approval_replay` |
| Restored approval state | Serialized `ToolApprovalState` restores mixed-batch behavior. | `test_mixed_batch_accepts_restored_tool_approval_state` |
| Unrelated turn before approval | Hidden calls do not execute on an unrelated turn. | `test_hidden_mixed_batch_requests_do_not_replay_on_unrelated_turn` |
| Multiple abandoned batches | Hidden calls replay only for the matching batch. | `test_hidden_mixed_batch_requests_replay_only_for_matching_visible_approval` |
| Queued approvals | One unresolved approval is surfaced per run without premature execution. | `test_tool_approval_middleware_queues_multiple_approval_requests`, `test_tool_approval_middleware_queues_streamed_approval_requests` |
| Middleware state plus hidden core state | State saves do not discard hidden mixed-batch calls. | `test_tool_approval_middleware_preserves_hidden_mixed_batch_requests` |
| Auto-approval callback | Callback receives the original function call and executes the approved set once. | `test_tool_approval_middleware_auto_approval_rule_receives_function_call` |
| Shared call budget | Auto-approved re-entry does not reset `max_function_calls`, and every executed approval group counts even when it pauses for input. | `test_tool_approval_middleware_auto_approved_loops_share_function_call_budget`, `test_approval_resume_user_input_counts_toward_function_call_budget` |
| Standing tool rule | Tool-level approval applies only to later matching tools. | `test_tool_approval_middleware_always_approve_tool_rule` |
| Forged standing rule | An unbound or substituted hosted response cannot create a standing middleware approval rule for caller-selected metadata. | `test_tool_approval_middleware_drops_forged_standing_approval`, `test_tool_approval_middleware_rebinds_hosted_standing_approval` |
| Hosted server boundary | Standing approval does not cross `server_label`. | `test_tool_approval_middleware_standing_rules_include_hosted_server_boundary` |
| Argument-scoped rule | Exact arguments are required; empty arguments are not tool-wide. | `test_tool_approval_middleware_always_approve_tool_with_arguments_rule`, `test_tool_approval_middleware_empty_arguments_rule_is_not_tool_wide` |
| Provider-injected approval tool | A tool added during `before_run` defers to in-run resolution, executes once, and emits one result. | `packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_agent_approval_deferred_provider_tool_executes` |
| AG-UI provider boundary | Completed local approval controls from AG-UI request and snapshot replay are absent from raw chat-client input while deferred and hosted approvals keep their respective in-run/provider paths. | `packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_does_not_forward_resolved_local_approval_control_to_chat_client`, `packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_agent_approval_deferred_provider_tool_executes`, `packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_canonical_resume_preserves_hosted_approval_for_provider`, `packages/ag-ui/tests/ag_ui/test_run.py::test_filter_local_approval_responses_for_provider_removes_duplicate_completed_controls`, `packages/ag-ui/tests/ag_ui/test_run.py::test_filter_local_approval_responses_for_provider_pairs_reused_call_ids_by_occurrence`, `packages/ag-ui/tests/ag_ui/test_run.py::test_canonical_hosted_approval_resume_rejects_edited_arguments_without_mutating_pending` |
| AG-UI standard approval payload | Agent and workflow tool approvals emit canonical `tool_call` interrupts. `approved` plus full-replacement `editedArgs` executes once and replays idempotently, while legacy `accepted` plus direct partial edits remains supported. Hosted approvals remain decision-only. | `packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_agent_approval_resume_entry_applies_standard_full_replacement_edited_args`, `test_endpoint_agent_approval_replayed_standard_edited_resume_is_idempotent`, `test_endpoint_agent_approval_resume_entry_applies_edited_arguments`, `test_workflow_endpoint_emits_canonical_tool_approval_interrupt`, `test_workflow_endpoint_accepts_canonical_tool_approval_resume`, `test_workflow_endpoint_applies_canonical_approval_edited_args`, `test_workflow_endpoint_accepts_legacy_partial_approval_edits`, `test_workflow_endpoint_hosted_approval_rejects_argument_edits` |
| AG-UI cancellation | A cancelled interrupt executes zero times and completes normally, including an identical retry during retained cancellation state; resolved siblings in the same complete resume still execute once. Workflow cancellation clears both runner correlation and the owning agent executor's pending request so later approvals remain resumable. | `packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_agent_approval_cancelled_resume_entry_completes_without_execution`, `test_endpoint_agent_approval_replayed_cancellation_completes_idempotently`, `test_endpoint_agent_approval_mixed_cancelled_and_resolved_resume_executes_resolved_tool`, `test_endpoint_workflow_request_info_cancelled_resume_completes_normally`, `test_workflow_endpoint_cancelled_agent_approval_does_not_block_next_approval` |
| AG-UI shared workflow interrupt ownership | A direct shared `Workflow` request-info interrupt can only be resolved or cancelled by the Snapshot Scope and AG-UI thread that created it. Ownership follows the authoritative pending request occurrence, and explicitly threaded cold checkpoint resumes fail closed when ownership is unavailable. | `packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_workflow_request_info_rejects_resume_from_different_thread`, `test_endpoint_workflow_request_info_rejects_resume_from_different_scope`, `test_endpoint_workflow_request_info_rejects_cancellation_from_different_thread`, `test_endpoint_workflow_request_info_remains_owned_after_client_disconnect`, `test_endpoint_workflow_request_info_rejects_unowned_pending_interrupt`, `test_endpoint_workflow_checkpoint_resume_rejects_threaded_resume_after_restart` |
| AG-UI approval retention and capacity | Pending authority expires automatically, indeterminate outcomes remain non-retryable until their safety window permits reclamation, and one trusted scope cannot consume another scope's occurrence quota. | `packages/ag-ui/tests/ag_ui/test_approval_lifecycle.py::test_abandoned_pending_occurrence_expires_and_releases_capacity`, `test_indeterminate_occurrence_is_reclaimed_after_its_safety_window`, `test_capacity_is_enforced_per_trusted_scope` |
| AG-UI local executor unavailable on resume | A claimed local occurrence whose executor disappeared releases its unstarted claim, reports temporary unavailability, and remains safely retryable. | `packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_agent_approval_resume_remains_retryable_when_local_tool_is_temporarily_unavailable` |
| AG-UI forwarded execution interruption | A provider failure, cancellation, or stream close after forwarding an approval recovers the open occurrence as indeterminate when no idempotency key proves retry safety. | `packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_hosted_approval_becomes_indeterminate_when_provider_stream_fails` |
### Errors, control flow, and limits
| Scenario | Required invariant | Primary regression test |
|---|---|---|
| Rejected execution | Rejection is a normal terminal result, not an exception to the caller. | `test_unapproved_tool_execution_raises_exception` |
| Approved tool exception | Generic and detailed error modes preserve one result and one execution. | `test_approved_function_call_with_error_without_detailed_errors`, `test_approved_function_call_with_error_with_detailed_errors` |
| Approved validation error | Validation failure returns one result without invoking the function body. | `test_approved_function_call_with_validation_error` |
| Approved success | Successful approved execution returns one result. | `test_approved_function_call_successful_execution` |
| Consecutive error cap | Error threshold stops repeated failures, submits collected results, and makes only the required final no-tool model call. | `test_function_invocation_config_max_consecutive_errors`, `test_streaming_function_invocation_config_max_consecutive_errors`, `test_approval_resume_error_limit_forces_final_no_tool_response` |
| Unknown call handling | Configured false returns an error result; configured true raises. | `test_function_invocation_config_terminate_on_unknown_calls_false`, `test_function_invocation_config_terminate_on_unknown_calls_true`, streaming equivalents |
| Middleware termination | Normal non-approval loop stops without a second model call. | `test_terminate_loop_single_function_call`, `test_terminate_loop_multiple_function_calls_one_terminates`, `test_terminate_loop_streaming_single_function_call` |
| Middleware failure (fatal) | `MiddlewareFailure` from function middleware or a tool body propagates to the caller without becoming a tool result; the tool does not execute (pre-invocation) or its result never feeds another model call (post-invocation); the cause chain is preserved; ordinary exceptions still become tool-error results and the loop continues. | `packages/core/tests/core/test_middleware_with_agent.py::TestMiddlewareFailure::test_failure_before_tool_aborts_run`, `test_failure_after_tool_aborts_run_before_next_model_turn`, `test_failure_cause_chain_reaches_caller`, `test_failure_from_tool_escapes_without_middleware`, `test_failure_streaming_reaches_stream_consumer`, `test_ordinary_exception_still_becomes_tool_error` |
| Middleware failure batch cancellation | A fatal signal fails the whole parallel batch: in-flight sibling tool invocations are cancelled and awaited before the failure propagates. Cancellation is cooperative — an async sibling stops at its next suspension point; a synchronous tool body already executing in a worker thread cannot be interrupted and may complete its side effects, but its result is discarded and never reaches the transcript, the model, or history, and failure propagation is not delayed behind it. | `TestMiddlewareFailure::test_failure_cancels_concurrent_sibling_tool`, `test_failure_with_sync_sibling_discards_late_result` |
| Middleware failure on a service-managed conversation | The continuation state is already persisted when the batch fails, so before propagating, the loop settles the hosted thread: one error `function_result` per dangling call, sent with `tool_choice="none"` in one extra request; the persisted continuation advances to the settlement response (required for response-ID continuations, a no-op for conversation-object ids) and the settlement response is otherwise discarded; a settlement failure never masks the abort. Without a service-managed conversation no extra request is made. | `TestMiddlewareFailure::test_failure_settles_dangling_calls_on_service_conversation`, `test_failure_settles_service_conversation_streaming`, `test_failure_settlement_advances_response_id_continuation`, `test_failure_without_service_conversation_makes_no_settlement_request` |
| Middleware failure during approved-tool replay | A fatal abort while the approval-resolution phase replays an approved tool escapes loudly (never absorbed into a rejection result), the tool's original — already service-persisted — call is settled the same way, and the continuation advances; both response modes. | `TestMiddlewareFailure::test_failure_during_approved_replay_settles_and_escapes`, `test_failure_during_approved_replay_streaming` |
| Maximum iterations | No orphan calls; a final no-tool response or deterministic fallback is returned. | `test_max_iterations_limit`, `test_max_iterations_no_orphaned_function_calls`, `test_max_iterations_makes_final_toolchoice_none_call`, `test_max_iterations_blank_final_fallback_synthesizes_message`, streaming equivalents |
| Maximum function calls | Parallel overshoot is bounded after the batch; every executed result group counts even without a `function_result`; blank final responses get fallback content. | `test_max_function_calls_limits_parallel_invocations`, `test_max_function_calls_single_calls_per_iteration`, `test_user_input_request_multiple_contents_propagate`, `test_approval_resume_user_input_counts_toward_function_call_budget`, `test_max_function_calls_blank_final_fallback_synthesizes_message`, streaming equivalent |
| Provider tool content after an active limit | Locally actionable calls and local approval requests returned despite `tool_choice="none"` are removed in both response modes. Provider-executed informational call/result pairs, hosted approval requests, and metadata-only streaming updates remain visible; fallback text never replaces retained transcript content. | `test_function_invocation_limit_drops_unexecutable_tool_content`, `test_streaming_function_invocation_limit_drops_unexecutable_tool_content`, `test_streaming_function_invocation_limit_preserves_metadata_after_tool_content_is_dropped`, `test_function_invocation_limit_preserves_provider_executed_tool_pair`, `test_streaming_function_invocation_limit_preserves_provider_executed_tool_pair`, `test_function_invocation_limit_appends_fallback_after_provider_executed_tool_pair`, `test_streaming_function_invocation_limit_appends_fallback_after_provider_executed_tool_pair`, `test_function_invocation_limit_preserves_hosted_approval_request`, `test_streaming_function_invocation_limit_preserves_hosted_approval_request` |
| Conversation continuation | Conversation id updates between iterations and is cleared on stop where required. | `test_conversation_id_updated_in_options_between_tool_iterations`, `test_function_invocation_stop_clears_conversation_id_non_stream`, `test_streaming_function_invocation_stop_clears_conversation_id` |
### History and provider serialization
| Scenario | Required invariant | Primary regression test |
|---|---|---|
| Append-only history replay | Resolved approval wrappers do not reach a later model call; one call/result pair remains. | `packages/core/tests/core/test_harness_tool_approval.py::test_approval_resume_filters_resolved_control_items_from_file_history` |
| Pending placeholder history | An approval response remains replayable while its only result is `[APPROVAL_PENDING]`. | `packages/core/tests/core/test_sessions.py::test_filter_approval_controls_keeps_response_for_pending_placeholder` |
| Pending hosted history replay | Stateless hosted approval requests remain replayable until a response is recorded, then both controls become inert. | `packages/openai/tests/openai/test_openai_chat_client.py::test_stateless_history_preserves_pending_hosted_approval_request_until_response` |
| Non-history provider plus session | Local history is still auto-injected for approval resume. | `packages/core/tests/core/test_agents.py::test_non_history_context_provider_still_injects_inmemory` |
| Hosted per-service-call persistence | A host-managed transcript remains available throughout a local function-call loop without being persisted into the framework session and replayed on the next hosted request. | `packages/foundry_hosting/tests/test_responses.py::TestAgentSessionPersistence::test_per_service_call_persistence_preserves_function_loop_history` |
| Streaming message injection with per-service-call persistence | A streaming response rebuilt from updates mirrors the inner conversation id exactly, including clearing it, and keeps its internal marker, so the next iteration appends only the latest message rather than replaying the whole turn on top of provider-held history, and never persists a conversation id from an earlier injected service call. | `packages/core/tests/core/test_middleware_with_chat.py::TestChatMiddleware::test_message_injection_middleware_streaming_preserves_inner_continuation_state`, `test_message_injection_middleware_streaming_keeps_service_conversation_id_external`, `test_message_injection_middleware_streaming_clears_conversation_id_when_final_call_has_none`, `test_message_injection_middleware_conversation_id_matches_across_streaming_modes`, `packages/core/tests/core/test_harness_agent.py::test_streaming_harness_tool_call_does_not_duplicate_transcript` |
| Service-side approval decision | Stored hosted request is skipped; the current approved or rejected hosted response is sent, while local approval controls are omitted from provider input. | `packages/openai/tests/openai/test_openai_chat_client.py::test_prepare_messages_strips_approval_request_but_keeps_response_under_storage`, `test_prepare_messages_drops_local_approval_controls` |
| OpenAI approval serialization | Hosted approval id and decision serialize to `mcp_approval_response`; local approvals remain in-process. | `test_prepare_message_for_openai_with_function_approval_response`, `test_prepare_content_for_opentool_approval_response`, `test_function_approval_response_with_mcp_tool_call` |
| OpenAI end-to-end hosted approval | Hosted request parses, response sends, and continuation completes. | `test_end_to_end_mcp_approval_flow` |
| Stored function call/result | Service-side storage drops server-issued calls but keeps new outputs. | `test_prepare_options_with_conversation_id_strips_server_issued_items`, `test_prepare_messages_for_openai_full_conversation_with_reasoning` |
| Stateless reasoning replay | Replay reconstructs reasoning, call, and result together; missing required reasoning fails before the request. | `test_tool_loop_store_false_replays_encrypted_reasoning_group`, `test_stateless_request_rejects_non_replayable_reasoning_bound_mcp_output`, `test_prepare_messages_for_openai_full_conversation_with_reasoning` |
| Foundry encrypted reasoning opt-in | Foundry clients omit `reasoning.encrypted_content` by default and preserve an explicit caller opt-in. | `packages/foundry/tests/foundry/test_foundry_chat_client.py::test_get_response_does_not_request_encrypted_reasoning_by_default`, `test_get_response_preserves_explicit_encrypted_reasoning_opt_in`, `packages/foundry/tests/foundry/test_foundry_agent.py::test_foundry_agent_basic_call_does_not_request_unsupported_encrypted_reasoning`, `test_foundry_agent_preserves_caller_requested_encrypted_reasoning`, `packages/foundry_hosting/tests/test_responses_int.py::TestReasoningHostedMcpReplay::test_second_turn_replays_mcp_call_with_encrypted_reasoning` |
| Opaque reasoning signature replay | Provider-specific opaque reasoning metadata is captured and restored on reconstructed calls. | `packages/gemini/tests/test_gemini_client.py::test_function_call_part_captures_thought_signature_as_reasoning_content`, `test_reconstructed_function_call_replays_thought_signature_from_reasoning_content` |
| Chat Completions approval wrappers | Framework approval wrappers are not sent as chat messages. | `packages/openai/tests/openai/test_openai_chat_completion_client.py` approval serialization tests |
| AG-UI approval result event | Approved result emits once with content and persists in snapshot. | `packages/ag-ui/tests/ag_ui/test_approval_result_event.py::test_approved_call_emits_one_live_result_under_original_identity`, `packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_agent_approval_resume_persists_replayable_tool_results`, `test_endpoint_agent_approval_replayed_resume_entry_reprojects_retained_result` |
| AG-UI rejection/mixed decision | Transport emits only the events defined for approved and rejected calls without duplicates. | `packages/ag-ui/tests/ag_ui/test_approval_result_event.py::test_rejected_call_does_not_execute_or_emit_live_result`, `test_mixed_batch_preserves_approved_result_identity_and_order`, `packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_agent_approval_rejection_releases_already_approved_sibling` |
| AG-UI approval-time follow-up | The full grouped user-input pause remains in message history and emits no synthetic `TOOL_CALL_RESULT`. | `packages/ag-ui/tests/ag_ui/test_approval_result_event.py::test_approval_follow_up_group_remains_in_history_without_live_tool_result` |
| AG-UI approval execution failure | A grouped executor failure becomes one deterministic terminal error result for the approved call. | `packages/ag-ui/tests/ag_ui/test_approval_result_event.py::test_approval_execution_failure_emits_one_terminal_error_result` |
| AG-UI no-approval path | Ordinary tool results do not gain an extra approval result event. | `packages/ag-ui/tests/ag_ui/test_approval_result_event.py::test_no_approval_path_emits_no_approval_specific_duplicate_result` |
| AG-UI client-tool request isolation | Client tool declarations are validated before use and remain request-scoped; a rejected collision or earlier successful request cannot change a later request's server-tool execution. | `packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_failed_client_tool_collision_does_not_affect_next_request`, `test_endpoint_client_tools_do_not_persist_into_next_request` |
| AG-UI `confirm_changes` snapshot | An accepted synthetic confirmation is replaced only when its original function call has a real result; rejection is cleaned explicitly, and missing accepted results remain inert. | `packages/ag-ui/tests/ag_ui/test_confirm_changes_snapshot.py` |
| AG-UI malformed `confirm_changes` metadata | Non-list tool-call metadata and malformed argument JSON are ignored without guessing a target call. | `test_confirm_changes_target_ignores_non_list_tool_calls`, `test_confirm_changes_target_rejects_malformed_arguments_json` |
| Compaction pair integrity | Adjacent and non-adjacent pairs, including assistant-embedded results and completed reused-id occurrences, remain atomic without pairing ambiguous or out-of-order ids. | `packages/core/tests/core/test_compaction.py::test_group_annotations_keep_tool_call_and_tool_result_atomic`, `test_group_annotations_include_reasoning_in_tool_call_group`, `test_group_annotations_pair_nonadjacent_function_result_by_call_id`, `test_group_annotations_pair_multiple_nonadjacent_results_with_declaration`, `test_group_annotations_pair_completed_reused_call_id_occurrences`, `test_group_annotations_close_assistant_embedded_result_before_reused_call_id`, `test_sliding_window_does_not_retain_orphan_result_after_assistant_embedded_result`, `test_sliding_window_keeps_reused_call_id_occurrences_atomic`, `test_group_annotations_do_not_pair_ambiguous_duplicate_call_ids` |
## Required coverage gaps
These scenarios are required but are not fully covered by merged tests on `main`:
| Gap | Tracking |
|---|---|
| Service-owned `previous_response_id` continuation cannot execute a terminal approval again on a later turn. | #6851 |
Do not mark these rows covered by nearby tests; each needs a dedicated regression at the owning layer.
## Minimum validation commands
Run from `python/` for any core function-loop change:
```bash
uv run poe test -P core
uv run poe syntax -P core
uv run poe pyright -P core
uv run poe test-typing -P core
```
Also run every affected package. Common approval-loop changes require:
```bash
uv run poe test -P openai
uv run poe syntax -P openai
uv run poe pyright -P openai
uv run poe test-typing -P openai
uv run poe test -P ag-ui
uv run poe test -P declarative
uv run --directory packages/foundry_hosting poe test
```
Run focused regression files first while iterating, but do not substitute them for the full package commands above.
## Review checklist
Before accepting an update, reviewers must confirm:
- the changed behavior is represented in this specification;
- the matrix names a regression test for every affected scenario;
- approved tools cannot execute twice;
- rejected tools cannot execute;
- no call or result becomes orphaned or duplicated;
- call/result matching does not assume `call_id` is globally unique forever;
- reasoning content or opaque signatures remain in the same logical group as the paired call/result, or replay fails
explicitly before sending a lossy provider request;
- caller messages and previous responses remain immutable;
- streaming updates and final response agree with non-streaming output;
- history replay does not reintroduce approval authority;
- full package, syntax, source typing, and test typing checks were run.
## Related issues
- #7241 — approval-resolution result streaming
- #7522 — first-class fatal signal (`MiddlewareFailure`) for function middleware
- #7267 / #7271 and #7304 — replayed calls and reused ids
- #7043 — provider-injected approval execution
- #6828 — AG-UI `confirm_changes` snapshot correlation
- #7212 — non-adjacent and reused-id compaction integrity
- #7125 — service-side approval response serialization
- #7045 — post-limit tool-content transcript integrity
- #6973 — declaration-only streaming metadata and argument integrity
- #6851 — duplicate side effects after approval continuation
- #7383 — bind approval responses to framework-issued requests after this foundation merges
- #6963 / #7095 — opaque reasoning-signature replay
- #6074 / #7233 — reasoning-paired tool-call replay
- #6450 / #6794 — provider message and tool-result serialization
-284
View File
@@ -1,284 +0,0 @@
# Feature-usage bit registry (per-language)
> **Status:** draft, accompanies [ADR-0033](../decisions/0033-feature-usage-bitmask-user-agent.md)
> and [SPEC-004](004-feature-usage-telemetry.md).
> **Version:** `1` per language · **Width:** 128-bit
This document is the proposed human-readable registry for the feature-usage
mask. Until ADR-0033 is accepted and the index declarations ship, these tables
are a **candidate mapping**, not a stable wire contract. The table is the
allocation authority and published decoder contract; package-local private
`FeatureIndex` declarations implement the rows they own. There is no generated
artifact.
This telemetry is intentionally **transparent**: this registry is public, the
emitted value is human-decodable, and a dedicated env var disables the mask
without removing the base User-Agent. Python's existing whole-User-Agent opt-out
also suppresses its mask; see [Opt-out](#opt-out).
## What is collected
A single 128-bit integer (the *feature mask*) describing **which Agent Framework
features were exercised** in a process — not which packages are installed. The
candidate below uses package-level bits plus selected major capabilities: core
agent/workflow/MCP features, stable skill source types, each orchestration
pattern, each individual built-in context/history provider, and distinct Foundry
surfaces. ADR-0033 still leaves the final v1 granularity open. A feature sets its
index at first meaningful activation; the SDK shifts that index, ORs the mask,
and emits the value.
No identifiers, arguments, prompts, payloads, or user data are encoded — only the
coarse Boolean \"this feature was observed at least once in this process\" per
registered bit. A repeated bit on later requests is the same observation, not
another use and not a count.
## Allocation tenet
**An index represents a stable, framework-owned capability whose adoption answers a
concrete product or support question.** It has a clear actual-use mark point in a
public entry path, and the privacy review covers the resulting distinction.
Keep imports, installation state, aliases, wrappers, internal helpers, and
implementation decorators such as caching/filtering/deduplication within their
own capability bit. Customer/runtime values — names, prompts, arguments, URLs,
identifiers, configuration choices — never become bits. A proposed distinction
without a concrete query and named decision owner waits.
Operational clients, tools, providers, and hosts mark on their first real public
operation/participation. Constructor marking is reserved for cases where
construction itself activates or registers the capability; DI instantiation
alone is not usage.
Ids use the package/integration name for a package-level signal and add a
capability suffix only when the row tracks a narrower surface. They describe the
registered feature, not an inheritance hierarchy: for example, Python
`hosting` is the base `agent-framework-hosting` package, while `hosting.a2a` is
the separate hosting-A2A integration.
## Per-language, not shared
The two tables below are **independent**. Feature indexes are **not** shared across
languages — Python bit 13 and .NET bit 13 do not mean the same thing. This is
deliberate: the User-Agent product token already names the language
(`agent-framework-python` vs `agent-framework-dotnet`), so a decoder selects the
right table from the UA and decodes against it. Each SDK numbers and evolves its
features independently — no cross-language synchronization, no null placeholders,
no \"same bit, same meaning\" rule.
## Encoding
- **Width:** 128-bit unsigned integer per language.
- **Versioning:** the emission carries the version so a decoder knows the bit
mapping in effect (version is per language).
- **User-Agent:** the mask is an RFC 7231 **comment** (metadata, not a product
token), placed after the agent-framework product token:
```text
agent-framework-python/1.2.3 (feat=v1.<hex_mask>)
```
where `<hex_mask>` is lowercase hex, no leading zeros, no `0x` prefix. Example
for bits 0, 1, 5 set (`0b100011 = 0x23`):
```text
agent-framework-python/1.2.3 (feat=v1.23)
```
- **Decoding:** read the **language** from the product token, pick that table;
read `vN`, pick that version; test `mask & (1 << index)` for each row. Unknown indexes
(newer SDK than the decoder's copy) are ignored.
## Emission scope (where the mask is sent)
- **Marking is universal:** every feature sets its index at first meaningful
activation, regardless of provider.
- **User-Agent `(feat=...)` comment — approved first-party clients only,
stamped at request time.** Added only when both the **Azure / Foundry**
client/pipeline family and the actual HTTPS origin are approved, re-evaluated
on every request and redirect hop. Custom origins are default-deny and an
unapproved redirect removes the token. It is
**never** sent to third-party providers — a feature fingerprint must not leak
into logs we cannot read. See [SPEC-004](004-feature-usage-telemetry.md#emission).
- **OpenTelemetry: not in v1.** Deferred primarily for privacy (a span attribute
would broadcast the fingerprint into the user's general telemetry / third-party
APM vendors). Left open behind the version prefix; see
[ADR-0033](../decisions/0033-feature-usage-bitmask-user-agent.md#considered-options).
## Index table — Python (`agent-framework-python`, version 1)
Layout: core features 031, orchestration patterns 3247, and
provider/integration packages from 48.
The provider/integration block is intentionally **not** partitioned by vendor
ownership. Some packages span first- and third-party services, ownership can
change, and protocols/storage integrations do not fit a stable first/third-party
taxonomy. Index ranges are allocation space, not privacy or emission policy;
the explicit destination allowlist independently ensures that the mask is sent
only to approved first-party endpoints.
| Index | Id | Feature | Activated at (representative) |
| --- | --- | --- | --- |
| 0 | `core.agent` | Agent | `agent_framework.Agent` |
| 1 | `core.harness_agent` | Harness agent | `agent_framework.create_harness_agent` |
| 2 | `core.workflow` | Workflow engine (custom graphs) | `agent_framework.WorkflowBuilder` |
| 3 | `core.mcp` | MCP tool (any transport) | `agent_framework.MCPStdioTool` |
| 4 | `core.tool_approval` | Tool-approval harness | `agent_framework.ToolApprovalMiddleware` |
| 5 | `core.memory_provider` | Memory context provider | `agent_framework.MemoryContextProvider` |
| 6 | `core.skills_provider` | Skills provider | `agent_framework.SkillsProvider` |
| 7 | `core.file_access_provider` | File-access provider | `agent_framework.FileAccessProvider` |
| 8 | `core.compaction_provider` | Context compaction provider | `agent_framework.CompactionProvider` |
| 9 | `core.todo_provider` | Todo provider | `agent_framework.TodoProvider` |
| 10 | `core.agent_mode_provider` | Agent-mode provider | `agent_framework.AgentModeProvider` |
| 11 | `core.background_agents_provider` | Background-agents provider | `agent_framework.BackgroundAgentsProvider` |
| 12 | `core.in_memory_history_provider` | In-memory history provider | `agent_framework.InMemoryHistoryProvider` |
| 13 | `core.file_history_provider` | File history provider | `agent_framework.FileHistoryProvider` |
| 14 | `core.file_skills_source` | File-backed skills | `agent_framework.FileSkillsSource` |
| 15 | `core.in_memory_skills_source` | In-memory / programmatic skills | `agent_framework.InMemorySkillsSource` |
| 16 | `core.mcp_skills_source` | MCP-backed skills | `agent_framework.MCPSkillsSource` |
| 17 | `core.session_store` | Agent session store | `agent_framework.SessionStore` / `FileSessionStore` |
| 18 | `core.agent_hooks` | Agent Hooks middleware | `agent_framework.create_agent_hooks_middleware` |
| 1931 | _reserved_ | core growth | — |
| 32 | `orchestration.sequential` | Sequential orchestration | `agent_framework_orchestrations.SequentialBuilder` |
| 33 | `orchestration.concurrent` | Concurrent orchestration | `agent_framework_orchestrations.ConcurrentBuilder` |
| 34 | `orchestration.group_chat` | Group-chat orchestration | `agent_framework_orchestrations.GroupChatBuilder` |
| 35 | `orchestration.magentic` | Magentic orchestration | `agent_framework_orchestrations.MagenticBuilder` |
| 36 | `orchestration.handoff` | Handoff orchestration | `agent_framework_orchestrations.HandoffBuilder` |
| 3747 | _reserved_ | orchestration growth | — |
| 48 | `foundry.chat_client` | Foundry chat client | `agent_framework_foundry.RawFoundryChatClient` |
| 49 | `foundry.agent` | Foundry agent | `agent_framework_foundry.FoundryAgent` |
| 50 | `foundry.memory` | Foundry memory provider | `agent_framework_foundry.FoundryMemoryProvider` |
| 51 | `foundry.embedding` | Foundry embedding client | `agent_framework_foundry.RawFoundryEmbeddingClient` |
| 52 | `foundry.evals` | Foundry evaluations | `agent_framework_foundry.FoundryEvals` |
| 53 | `foundry.toolbox` | Foundry Toolbox MCP tool | `agent_framework_foundry_hosting.FoundryToolbox` |
| 54 | `foundry_local` | Foundry Local client | `agent_framework_foundry_local.FoundryLocalClient` |
| 55 | `foundry_hosting` | Foundry hosting layer | `agent_framework_foundry_hosting.ResponsesHostServer` / `InvocationsHostServer` |
| 56 | `openai` | OpenAI clients | `agent_framework_openai` |
| 57 | `anthropic` | Anthropic clients | `agent_framework_anthropic` |
| 58 | `bedrock` | AWS Bedrock clients | `agent_framework_bedrock` |
| 59 | `gemini` | Gemini chat client | `agent_framework_gemini` |
| 60 | `mistral` | Mistral embedding client | `agent_framework_mistral` |
| 61 | `ollama` | Ollama clients | `agent_framework_ollama` |
| 62 | `claude` | Claude Agent SDK agent | `agent_framework_claude` |
| 63 | `copilotstudio` | Copilot Studio agent | `agent_framework_copilotstudio` |
| 64 | `github_copilot` | GitHub Copilot agent | `agent_framework_github_copilot` |
| 65 | `azure_ai_search` | Azure AI Search context provider | `agent_framework_azure_ai_search` |
| 66 | `azure_cosmos` | Azure Cosmos history / checkpoint store | `agent_framework_azure_cosmos` |
| 67 | `azure_contentunderstanding` | Azure Content Understanding context provider | `agent_framework_azure_contentunderstanding.ContentUnderstandingContextProvider` |
| 68 | `redis` | Redis context / history provider | `agent_framework_redis` |
| 69 | `mem0` | Mem0 memory provider | `agent_framework_mem0.Mem0ContextProvider` |
| 70 | `purview` | Purview client | `agent_framework_purview.PurviewClient` |
| 71 | `a2a` | A2A agent / executor | `agent_framework_a2a.A2AAgent` / `A2AExecutor` |
| 72 | `ag_ui` | AG-UI chat client / agent | `agent_framework_ag_ui` |
| 73 | `chatkit` | ChatKit integration | `agent_framework_chatkit` |
| 74 | `devui` | DevUI served | `agent_framework_devui.serve` |
| 75 | `declarative.agent` | Declarative agent definitions | `agent_framework_declarative.AgentFactory` |
| 76 | `declarative.workflow` | Declarative workflow definitions | `agent_framework_declarative.WorkflowFactory` |
| 77 | `durabletask` | Durable task runtime | `agent_framework_durabletask` |
| 78 | `azurefunctions` | Azure Functions agent host | `agent_framework_azurefunctions` |
| 79 | `tools.shell` | Shell tools | `agent_framework_tools.shell.LocalShellTool` / `DockerShellTool` |
| 80 | `monty` | Monty CodeAct provider | `agent_framework_monty.MontyCodeActProvider` |
| 81 | `hyperlight` | Hyperlight CodeAct provider | `agent_framework_hyperlight.HyperlightCodeActProvider` |
| 82 | `azure_cosmos_memory` | Azure Cosmos DB semantic-memory provider | `agent_framework_azure_cosmos_memory.CosmosMemoryContextProvider` |
| 83 | `hosting` | App-owned agent/workflow hosting state | `agent_framework_hosting.AgentState` / `WorkflowState` |
| 84 | `hosting.a2a` | A2A hosting converters | `agent_framework_hosting_a2a.a2a_to_run` / `a2a_from_run` |
| 85 | `hosting.mcp` | MCP hosting adapters | `agent_framework_hosting_mcp.AgentMCPTool` / `WorkflowMCPTool` |
| 86 | `hosting.responses` | OpenAI Responses hosting converters | `agent_framework_hosting_responses.responses_to_run` |
| 87 | `hosting.telegram` | Telegram hosting converters | `agent_framework_hosting_telegram.telegram_to_run` |
| 88 | `lab` | Experimental Agent Framework Lab features | `agent_framework.lab` feature entry points |
| 89127 | _reserved_ | future packages | — |
## Index table — .NET (`agent-framework-dotnet`, version 1)
| Index | Id | Feature | Activated at (representative) |
| --- | --- | --- | --- |
| 0 | `core.agent` | Agent | `Microsoft.Agents.AI.ChatClientAgent` |
| 1 | `core.harness_agent` | Harness agent | `Microsoft.Agents.AI.HarnessAgent` |
| 2 | `core.workflow` | Workflow engine (custom graphs) | `Microsoft.Agents.AI.Workflows.WorkflowBuilder` |
| 3 | `core.tool_approval` | Tool-approval agent | `Microsoft.Agents.AI.ToolApprovalAgent` |
| 4 | `core.chat_history_memory_provider` | Chat-history memory provider | `Microsoft.Agents.AI.ChatHistoryMemoryProvider` |
| 5 | `core.file_memory_provider` | File memory provider | `Microsoft.Agents.AI.FileMemoryProvider` |
| 6 | `core.text_search_provider` | Text-search provider | `Microsoft.Agents.AI.TextSearchProvider` |
| 7 | `core.file_access_provider` | File-access provider | `Microsoft.Agents.AI.FileAccessProvider` |
| 8 | `core.skills_provider` | Skills provider | `Microsoft.Agents.AI.AgentSkillsProviderBuilder` |
| 9 | `core.compaction_provider` | Context compaction provider | `Microsoft.Agents.AI.Compaction.CompactionProvider` |
| 10 | `core.todo_provider` | Todo provider | `Microsoft.Agents.AI.TodoProvider` |
| 11 | `core.agent_mode_provider` | Agent-mode provider | `Microsoft.Agents.AI.AgentModeProvider` |
| 12 | `core.background_agents_provider` | Background-agents provider | `Microsoft.Agents.AI.BackgroundAgentsProvider` |
| 13 | `core.in_memory_history_provider` | In-memory history provider | `Microsoft.Agents.AI.InMemoryChatHistoryProvider` |
| 14 | `core.mcp` | MCP tasks / skills integration | `Microsoft.Agents.AI.Mcp.McpClientTaskExtensions` |
| 15 | `core.file_skills_source` | File-backed skills | `Microsoft.Agents.AI.AgentFileSkillsSource` |
| 16 | `core.in_memory_skills_source` | In-memory skills | `Microsoft.Agents.AI.AgentInMemorySkillsSource` |
| 17 | `core.inline_skill` | Inline programmatic skill | `Microsoft.Agents.AI.AgentInlineSkill` |
| 18 | `core.class_skill` | Class-based programmatic skill | `Microsoft.Agents.AI.AgentClassSkill` |
| 19 | `core.mcp_skills_source` | MCP-backed skills | `Microsoft.Agents.AI.AgentSkillsProviderBuilderMcpExtensions.UseMcpSkills` |
| 2031 | _reserved_ | core growth | — |
| 32 | `orchestration.sequential` | Sequential orchestration | `Microsoft.Agents.AI.Workflows.SequentialWorkflowBuilder` |
| 33 | `orchestration.concurrent` | Concurrent orchestration | `Microsoft.Agents.AI.Workflows.ConcurrentWorkflowBuilder` |
| 34 | `orchestration.group_chat` | Group-chat orchestration | `Microsoft.Agents.AI.Workflows.GroupChatWorkflowBuilder` |
| 35 | `orchestration.magentic` | Magentic orchestration | `Microsoft.Agents.AI.Workflows.MagenticWorkflowBuilder` |
| 36 | `orchestration.handoff` | Handoff orchestration | `Microsoft.Agents.AI.Workflows.HandoffWorkflowBuilder` |
| 3747 | _reserved_ | orchestration growth | — |
| 48 | `foundry.chat_client` | Foundry chat client | `Microsoft.Agents.AI.Foundry.FoundryChatClient` |
| 49 | `foundry.agent` | Foundry agent | `Microsoft.Agents.AI.Foundry.FoundryAgent` |
| 50 | `foundry.memory` | Foundry memory provider | `Microsoft.Agents.AI.Foundry.FoundryMemoryProvider` |
| 51 | `foundry.evals` | Foundry evaluations | `Microsoft.Agents.AI.Foundry.FoundryEvals` |
| 52 | `foundry.toolbox` | Foundry Toolbox MCP tool | `Microsoft.Agents.AI.Foundry.HostedMcpToolboxAITool` |
| 53 | `foundry_hosting` | Foundry hosting layer | `Microsoft.Agents.AI.Foundry.Hosting.FoundryHostingExtensions.AddFoundryResponses` |
| 54 | `openai` | OpenAI integration | `Microsoft.Agents.AI.OpenAI` |
| 55 | `anthropic` | Anthropic integration | `Microsoft.Agents.AI.Anthropic` |
| 56 | `copilotstudio` | Copilot Studio agent | `Microsoft.Agents.AI.CopilotStudio.CopilotStudioAgent` |
| 57 | `github_copilot` | GitHub Copilot agent | `Microsoft.Agents.AI.GitHub.Copilot.GitHubCopilotAgent` |
| 58 | `azure_cosmos` | Cosmos history / checkpoint store | `Microsoft.Agents.AI.CosmosChatHistoryProvider` |
| 59 | `valkey` | Valkey chat-history provider | `Microsoft.Agents.AI.Valkey.ValkeyChatHistoryProvider` |
| 60 | `mem0` | Mem0 memory provider | `Microsoft.Agents.AI.Mem0.Mem0Provider` |
| 61 | `purview` | Purview integration | `Microsoft.Agents.AI.Purview` |
| 62 | `a2a` | A2A agent | `Microsoft.Agents.AI.A2A.A2AAgent` |
| 63 | `hosting.ag_ui` | AG-UI hosting endpoint | `Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.AGUIEndpointRouteBuilderExtensions.MapAGUIServer` |
| 64 | `devui` | DevUI served | `Microsoft.Agents.AI.DevUI` |
| 65 | `declarative.agent` | Declarative agent definitions | `Microsoft.Agents.AI.PromptAgentFactory.CreateAsync` |
| 66 | `declarative.workflow` | Declarative workflow definitions | `Microsoft.Agents.AI.Workflows.Declarative.DeclarativeWorkflowBuilder.Build` |
| 67 | `durabletask` | Durable task runtime | `Microsoft.Agents.AI.DurableTask` |
| 68 | `azurefunctions` | Azure Functions agent host | `Microsoft.Agents.AI.Hosting.AzureFunctions` |
| 69 | `tools.shell` | Shell tools | `Microsoft.Agents.AI.Tools.Shell.ShellExecutor` |
| 70 | `hyperlight` | Hyperlight CodeAct provider | `Microsoft.Agents.AI.Hyperlight.HyperlightCodeActProvider` |
| 71 | `hosting.agent` | Hosted AF agent wrapper | `Microsoft.Agents.AI.Hosting.AIHostAgent` |
| 72 | `local_codeact` | Local Python CodeAct provider | `Microsoft.Agents.AI.LocalCodeAct.LocalCodeActProvider` |
| 73 | `hosting.a2a` | A2A hosting endpoints | `Microsoft.AspNetCore.Builder.A2AEndpointRouteBuilderExtensions.MapA2AJsonRpc` |
| 74 | `hosting.openai` | OpenAI-compatible hosting endpoints | `Microsoft.AspNetCore.Builder.MicrosoftAgentAIHostingOpenAIEndpointRouteBuilderExtensions.MapOpenAIResponses` |
| 75127 | _reserved_ | future packages | — |
## Opt-out
The dedicated mask-only environment variable is shared by both SDKs:
- `AGENT_FRAMEWORK_FEATURE_MASK_DISABLED=true|1` — drops **only** the feature
mask; the base `agent-framework-<lang>/{version}` User-Agent is still sent.
The dedicated flag lets a privacy-conscious user keep contributing SDK
identity/version (useful for support and compatibility triage) while withholding
the feature-usage signal. Python's existing
`AGENT_FRAMEWORK_USER_AGENT_DISABLED=true|1` also suppresses its entire Agent
Framework User-Agent contribution, mask included. Adding a matching .NET
whole-User-Agent opt-out is outside this design.
## Governance
1. One index per package/feature, **numbered independently per language**, in the
table for that language. New indexes are added by editing this file in a reviewed
PR; indexes are never reused within a `(language, version)`.
2. Each package owns a private `FeatureIndex` declaration containing only its
rows. Core owns the accumulator API and core indexes, but never imports
optional packages. Adding a new optional-package index therefore does not
require a core release once the marker API exists.
3. Adding a feature: apply the [allocation tenet](#allocation-tenet), name the
concrete query/decision owner, add the package-local index and table row, and mark the
stable public entry point where actual use begins.
4. Widening beyond 128-bit or re-partitioning bumps that language's version; old
decoders keep working because the version prefix disambiguates the mapping.
5. A repository validation test gathers all package-local declarations for each
`(language, version)` and asserts exact table parity, complete non-reserved
coverage, `0..127` range, and **no duplicate/overlapping indexes**.
> **No machine-readable registry file ships today.** Nothing consumes one at
> runtime (packages own private declarations). If/when a programmatic decoder is built, this
> table is the contract to export to JSON for it then.
-119
View File
@@ -1,119 +0,0 @@
---
name: pull-requests
description: >
Guidance for creating pull requests and handling PR review comments in the
Agent Framework repository. Use this when writing a PR description (filling out
the PR template) or when responding to and resolving review comments on an
existing PR.
---
# Pull Request Workflow
This skill covers two tasks: (1) writing a high-quality PR description, and
(2) handling review comments on an existing PR.
## 1. Writing the PR description
Always follow the repository PR template at
[`.github/pull_request_template.md`](../../../../.github/pull_request_template.md). Keep its
exact structure and headings. Fill every section:
### `### Motivation & Context`
Explain *why* the change is needed: the problem it solves and the scenario it
contributes to. Describe the net change relative to `main` — this is implied, so
do **not** spell out "vs main" explicitly.
### `### Description & Review Guide`
Describe the changes, the overall approach, and the design. Answer the three
prompts:
- **What are the major changes?**
- **What is the impact of these changes?**
- **What do you want reviewers to focus on?** — This item is for **human
reviewers only**. Automated/AI reviewers must ignore it and review the entire
change rather than narrowing scope to it.
### `### Related Issue`
Link the issue the PR fixes using a GitHub closing keyword (`Fixes #123` /
`Closes #123`) so it closes automatically on merge. A PR with no linked issue may
be closed regardless of how valid the change is. Before opening, confirm there is
no other open PR for the same issue; if there is, explain how this PR differs.
### `### Contribution Checklist`
Check every item that applies. For the breaking-change item:
- Leave **"This is not a breaking change."** checked for the common case.
- If the change **is** breaking, add the `breaking change` label **or** put
`[BREAKING]` in the title prefix, before or after a language prefix such as
`Python:` or `.NET:` — workflows keep the label and the title prefix in sync
automatically (see `.github/workflows/label-title-prefix.yml` and
`.github/workflows/label-pr.yml`).
### Do not
- Do **not** add ad-hoc sections such as "Validation" or "Tests run"; CI/CD and
the checklist already cover validation status.
- Do **not** remove or reorder the template's headings.
### Creating the PR
Open new PRs as **drafts** until they are ready for review. Example:
```bash
gh pr create --repo microsoft/agent-framework --base main \
--head <your-fork-owner>:<branch> --draft \
--title "<concise title>" --body "<body following the template>"
```
## 2. Handling review comments
When a PR receives review comments, follow this sequence — **do not start editing
code before the user has reviewed the plan**:
1. **Review the comments.** Read every review comment and thread on the PR,
including inline code comments and general review summaries.
2. **Make a plan.** Produce a concrete plan describing how each comment will be
addressed (or why it should not be, with reasoning).
3. **Let the user review the plan.** Present the plan and wait for the user's
approval or adjustments before implementing anything.
4. **Implement.** Make the agreed changes.
5. **Reply to every comment.** Add a reply to **all** comments explaining how it
was addressed, preferably citing the commit containing the change. If the
feedback was not addressed, explain why. Leave no comment unanswered.
6. **Resolve completed threads yourself.** After replying and completing any
necessary discussion, resolve the review thread. Do not wait for the reviewer
or a maintainer to resolve it. Leave a thread open only while it has an
unanswered question or active discussion.
### Useful commands
List review comments and threads:
```bash
# Inline review comments
gh api repos/{owner}/{repo}/pulls/{pr}/comments
# Review threads with resolution state (GraphQL)
gh api graphql -f query='
query($owner:String!,$repo:String!,$pr:Int!){
repository(owner:$owner,name:$repo){
pullRequest(number:$pr){
reviewThreads(first:100){
nodes{ id isResolved comments(first:50){ nodes{ id body author{login} } } }
}
}
}
}' -F owner={owner} -F repo={repo} -F pr={pr}
```
Reply to an inline review comment:
```bash
gh api repos/{owner}/{repo}/pulls/{pr}/comments/{comment_id}/replies \
-f body="Addressed in <commit>: <explanation>"
```
Resolve a review thread (needs the thread node id from the GraphQL query above):
```bash
gh api graphql -f query='
mutation($threadId:ID!){
resolveReviewThread(input:{threadId:$threadId}){ thread{ isResolved } }
}' -F threadId={thread_id}
```
+2 -4
View File
@@ -1,4 +1,4 @@
---
---
name: verify-samples-tool
description: How to use the verify-samples tool to run, verify, and manage sample definitions in the Agent Framework repository. Use this when adding, updating, or running sample verification.
---
@@ -157,7 +157,7 @@ new SampleDefinition
new SampleDefinition
{
Name = "Agent_With_AzureOpenAIChatCompletion",
ProjectPath = "samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIChatCompletion",
ProjectPath = "samples/02-agents/AgentProviders/Agent_With_AzureOpenAIChatCompletion",
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
ExpectedOutputDescription =
@@ -223,5 +223,3 @@ new SampleDefinition
SkipReason = "Runs as an MCP stdio server that does not exit on its own.",
},
```
-4
View File
@@ -10,10 +10,6 @@ See `./.github/skills/build-and-test/SKILL.md` for detailed instructions on buil
See `./.github/skills/project-structure/SKILL.md` for an overview of the project structure.
## Pull Requests
See `./.github/skills/pull-requests/SKILL.md` for guidance on writing PR descriptions and handling/resolving PR review comments.
### Core types
- `AIAgent`: The abstract base class that all agents derive from, providing common methods for interacting with an agent.
+46 -48
View File
@@ -11,8 +11,8 @@
</PropertyGroup>
<ItemGroup>
<!-- Aspire.* -->
<PackageVersion Include="Anthropic" Version="12.42.0" />
<PackageVersion Include="Anthropic.Foundry" Version="0.7.1" />
<PackageVersion Include="Anthropic" Version="12.20.0" />
<PackageVersion Include="Anthropic.Foundry" Version="0.5.0" />
<PackageVersion Include="Aspire.Hosting" Version="$(AspireAppHostSdkVersion)" />
<PackageVersion Include="Aspire.Azure.AI.OpenAI" Version="13.0.0-preview.1.25560.3" />
<PackageVersion Include="Aspire.Azure.AI.Inference" Version="13.1.0-preview.1.25616.3" />
@@ -21,47 +21,39 @@
<PackageVersion Include="Aspire.Hosting.Azure.CognitiveServices" Version="$(AspireAppHostSdkVersion)" />
<PackageVersion Include="Aspire.Microsoft.Azure.Cosmos" Version="$(AspireAppHostSdkVersion)" />
<PackageVersion Include="CommunityToolkit.Aspire.OllamaSharp" Version="13.0.0" />
<PackageVersion Include="MessagePack" Version="3.1.7" /> <!-- Transitive dependency of Aspire pinned to newer version due to vulnerability in 2.5.192 -->
<!-- Azure.* -->
<PackageVersion Include="Azure.AI.AgentServer.Core" Version="1.0.0-beta.28" />
<PackageVersion Include="Azure.AI.AgentServer.Invocations" Version="1.0.0-beta.6" />
<PackageVersion Include="Azure.AI.AgentServer.Responses" Version="1.0.0-beta.8" />
<PackageVersion Include="Azure.AI.AgentServer.Core" Version="1.0.0-beta.25" />
<PackageVersion Include="Azure.AI.AgentServer.Invocations" Version="1.0.0-beta.4" />
<PackageVersion Include="Azure.AI.AgentServer.Responses" Version="1.0.0-beta.5" />
<PackageVersion Include="Azure.Search.Documents" Version="12.0.0" />
<PackageVersion Include="Azure.AI.Projects" Version="2.1.0-beta.4" />
<PackageVersion Include="Azure.AI.Projects" Version="2.1.0-beta.2" />
<PackageVersion Include="Azure.AI.Agents.Persistent" Version="1.2.0-beta.10" />
<PackageVersion Include="Azure.AI.OpenAI" Version="2.9.0-beta.1" />
<PackageVersion Include="Azure.Core" Version="1.61.0" />
<PackageVersion Include="Azure.Core" Version="1.56.0" />
<PackageVersion Include="Azure.Identity" Version="1.21.0" />
<PackageVersion Include="Azure.Storage.Blobs" Version="12.29.1" />
<PackageVersion Include="DotNetEnv" Version="3.1.1" />
<PackageVersion Include="Azure.Monitor.OpenTelemetry.Exporter" Version="1.5.0" />
<!-- Google Gemini -->
<PackageVersion Include="Google.GenAI" Version="1.6.0" />
<PackageVersion Include="Mscc.GenerativeAI.Microsoft" Version="2.9.3" />
<!-- Microsoft.Azure.* -->
<PackageVersion Include="Microsoft.Azure.Cosmos" Version="3.61.0" />
<PackageVersion Include="Microsoft.Azure.Cosmos" Version="3.54.0" />
<!-- Newtonsoft.Json -->
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
<!-- System.* -->
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.11" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.6" />
<PackageVersion Include="Microsoft.Bcl.HashCode" Version="6.0.0" />
<PackageVersion Include="Microsoft.Bcl.Memory" Version="10.0.10" />
<PackageVersion Include="System.ClientModel" Version="1.15.0" />
<PackageVersion Include="Microsoft.Bcl.Memory" Version="10.0.5" />
<PackageVersion Include="System.ClientModel" Version="1.12.0" />
<PackageVersion Include="System.CodeDom" Version="10.0.0" />
<PackageVersion Include="System.Collections.Immutable" Version="10.0.10" />
<PackageVersion Include="System.Collections.Immutable" Version="10.0.1" />
<PackageVersion Include="System.CommandLine" Version="2.0.0-rc.2.25502.107" />
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="10.0.11" />
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="10.0.6" />
<PackageVersion Include="System.Linq.AsyncEnumerable" Version="10.0.5" />
<PackageVersion Include="System.Net.Http.Json" Version="10.0.0" />
<PackageVersion Include="System.Net.ServerSentEvents" Version="10.0.10" />
<!-- AG-UI .NET SDK packages (published by the AG-UI team). -->
<PackageVersion Include="AGUI.Abstractions" Version="0.0.5" />
<PackageVersion Include="AGUI.Formatting" Version="0.0.5" />
<PackageVersion Include="AGUI.Protobuf" Version="0.0.5" />
<PackageVersion Include="AGUI.Client" Version="0.0.5" />
<PackageVersion Include="AGUI.Server" Version="0.0.5" />
<PackageVersion Include="System.Text.Json" Version="10.0.11" />
<PackageVersion Include="System.Threading.Channels" Version="10.0.11" />
<PackageVersion Include="System.Net.ServerSentEvents" Version="10.0.5" />
<PackageVersion Include="System.Text.Json" Version="10.0.6" />
<PackageVersion Include="System.Threading.Channels" Version="10.0.6" />
<PackageVersion Include="System.Threading.Tasks.Extensions" Version="4.6.3" />
<PackageVersion Include="System.Net.Security" Version="4.3.2" />
<!-- OpenTelemetry -->
@@ -77,16 +69,15 @@
<!-- Microsoft.AspNetCore.* -->
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.9" />
<PackageVersion Include="Microsoft.OpenApi" Version="2.7.5" /> <!-- Pin patched OpenAPI.NET to remediate GHSA-v5pm-xwqc-g5wc -->
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.0.0" />
<!-- Microsoft.Extensions.* -->
<PackageVersion Include="Microsoft.Extensions.AI" Version="10.9.0" />
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="10.9.0" />
<PackageVersion Include="Microsoft.Extensions.AI.Evaluation" Version="10.9.0" />
<PackageVersion Include="Microsoft.Extensions.AI.Evaluation.Quality" Version="10.9.0" />
<PackageVersion Include="Microsoft.Extensions.AI.Evaluation.Safety" Version="10.9.0" />
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="10.6.0" />
<PackageVersion Include="Microsoft.Extensions.AI" Version="10.5.1" />
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="10.5.1" />
<PackageVersion Include="Microsoft.Extensions.AI.Evaluation" Version="10.4.0" />
<PackageVersion Include="Microsoft.Extensions.AI.Evaluation.Quality" Version="10.4.0" />
<PackageVersion Include="Microsoft.Extensions.AI.Evaluation.Safety" Version="10.3.0-preview.1.26109.11" />
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="10.5.1" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Compliance.Abstractions" Version="10.5.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="10.0.1" />
@@ -95,22 +86,20 @@
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Configuration.UserSecrets" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.11" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.FileSystemGlobbing" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.11" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.ServiceDiscovery" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.VectorData.Abstractions" Version="10.7.0" />
<PackageVersion Include="Microsoft.Extensions.VectorData.Abstractions" Version="9.7.0" />
<!-- Vector Stores -->
<PackageVersion Include="CommunityToolkit.VectorData.CosmosNoSql" Version="1.0.0" />
<PackageVersion Include="CommunityToolkit.VectorData.InMemory" Version="1.0.0" />
<PackageVersion Include="CommunityToolkit.VectorData.Qdrant" Version="1.0.0" />
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.InMemory" Version="1.67.0-preview" />
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.Qdrant" Version="1.67.0-preview" />
<!-- Agent SDKs -->
<PackageVersion Include="GitHub.Copilot.SDK" Version="1.0.5" />
<PackageVersion Include="ResponsibleAI.AgentHooks" Version="0.1.0-alpha.4" />
<PackageVersion Include="GitHub.Copilot.SDK" Version="1.0.0" />
<PackageVersion Include="Microsoft.Agents.CopilotStudio.Client" Version="1.3.171-beta" />
<!-- M365 Agents SDK -->
<PackageVersion Include="AdaptiveCards" Version="3.1.0" />
@@ -120,30 +109,39 @@
<PackageVersion Include="A2A" Version="1.0.0-preview2" />
<PackageVersion Include="A2A.AspNetCore" Version="1.0.0-preview2" />
<!-- MCP -->
<PackageVersion Include="ModelContextProtocol" Version="2.1.0" />
<PackageVersion Include="ModelContextProtocol.Extensions.Tasks" Version="2.1.0" />
<PackageVersion Include="ModelContextProtocol" Version="1.2.0" />
<!-- Hyperlight -->
<PackageVersion Include="Hyperlight.HyperlightSandbox.Api" Version="0.4.0" />
<PackageVersion Include="Hyperlight.HyperlightSandbox.Guest.Python" Version="0.4.0" />
<!-- Inference SDKs -->
<PackageVersion Include="Dapr.AI.Microsoft.Extensions" Version="1.18.4" />
<PackageVersion Include="Microsoft.ML.OnnxRuntimeGenAI" Version="0.10.0" />
<PackageVersion Include="Microsoft.ML.Tokenizers" Version="2.0.0" />
<PackageVersion Include="OllamaSharp" Version="5.4.8" />
<PackageVersion Include="OpenAI" Version="2.10.0" />
<!-- Identity -->
<PackageVersion Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.84.2" />
<PackageVersion Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.83.1" />
<!-- Workflows -->
<PackageVersion Include="Microsoft.Agents.ObjectModel" Version="2026.2.4.1" />
<PackageVersion Include="Microsoft.Agents.ObjectModel.Json" Version="2026.2.4.1" />
<PackageVersion Include="Microsoft.Agents.ObjectModel.PowerFx" Version="2026.2.4.1" />
<PackageVersion Include="Microsoft.PowerFx.Interpreter" Version="1.8.1" />
<!-- Valkey -->
<PackageVersion Include="Valkey.Glide" Version="1.1.0" />
<!-- Durable Task -->
<PackageVersion Include="Microsoft.DurableTask.Client" Version="1.18.0" />
<PackageVersion Include="Microsoft.DurableTask.Client.AzureManaged" Version="1.18.0" />
<PackageVersion Include="Microsoft.DurableTask.Worker" Version="1.18.0" />
<PackageVersion Include="Microsoft.DurableTask.Worker.AzureManaged" Version="1.18.0" />
<!-- Azure Functions -->
<PackageVersion Include="Microsoft.Azure.Functions.Worker" Version="2.50.0" />
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" Version="1.12.1" />
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged" Version="1.0.1" />
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.3.0" />
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" Version="2.1.0" />
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.Mcp" Version="1.0.0" />
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Sdk" Version="2.0.7" />
<!-- Redis -->
<PackageVersion Include="StackExchange.Redis" Version="2.10.1" />
<!-- Console UX -->
<PackageVersion Include="Spectre.Console" Version="0.49.1" />
<!-- AWS -->
<PackageVersion Include="AWSSDK.Extensions.Bedrock.MEAI" Version="4.0.6.10" />
<!-- Test -->
<PackageVersion Include="FluentAssertions" Version="8.8.0" />
<PackageVersion Include="Microsoft.AspNetCore.TestHost" Condition="'$(TargetFramework)' == 'net8.0'" Version="8.0.22" />
+1
View File
@@ -33,3 +33,4 @@ Console.WriteLine(await agent.RunAsync("Write a haiku about Microsoft Agent Fram
- [Design Documents](../docs/design)
- [Architectural Decision Records](../docs/decisions)
- [MSFT Learn Docs](https://learn.microsoft.com/agent-framework/overview/agent-framework-overview)
+112 -130
View File
@@ -15,27 +15,27 @@
<Project Path="samples/01-get-started/03_multi_turn/03_multi_turn.csproj" />
<Project Path="samples/01-get-started/04_memory/04_memory.csproj" />
<Project Path="samples/01-get-started/05_first_workflow/05_first_workflow.csproj" />
<Project Path="samples/01-get-started/06_host_your_agent/06_host_your_agent.csproj" />
</Folder>
<Folder Name="/Samples/02-agents/">
<File Path="samples/02-agents/README.md" />
</Folder>
<Folder Name="/Samples/02-agents/AgentProviders/">
<File Path="samples/02-agents/AgentProviders/README.md" />
<Project Path="samples/02-agents/AgentProviders/a2a/Agent_With_A2A/Agent_With_A2A.csproj" />
<Project Path="samples/02-agents/AgentProviders/anthropic/Agent_With_Anthropic/Agent_With_Anthropic.csproj" />
<Project Path="samples/02-agents/AgentProviders/azure/Agent_With_AzureAIProject/Agent_With_AzureAIProject.csproj" />
<Project Path="samples/02-agents/AgentProviders/azure/Agent_With_AzureFoundryModel/Agent_With_AzureFoundryModel.csproj" />
<Project Path="samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIChatCompletion/Agent_With_AzureOpenAIChatCompletion.csproj" />
<Project Path="samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIResponses/Agent_With_AzureOpenAIResponses.csproj" />
<Project Path="samples/02-agents/AgentProviders/custom/Agent_With_CustomImplementation/Agent_With_CustomImplementation.csproj" />
<Project Path="samples/02-agents/AgentProviders/dapr/Agent_With_Dapr/Agent_With_Dapr.csproj" />
<Project Path="samples/02-agents/AgentProviders/github-copilot/Agent_With_GitHubCopilot/Agent_With_GitHubCopilot.csproj" />
<Project Path="samples/02-agents/AgentProviders/github-copilot/Agent_With_GitHubCopilot_BYOK/Agent_With_GitHubCopilot_BYOK.csproj" />
<Project Path="samples/02-agents/AgentProviders/google-gemini/Agent_With_GoogleGemini/Agent_With_GoogleGemini.csproj" />
<Project Path="samples/02-agents/AgentProviders/ollama/Agent_With_Ollama/Agent_With_Ollama.csproj" />
<Project Path="samples/02-agents/AgentProviders/onnx/Agent_With_ONNX/Agent_With_ONNX.csproj" />
<Project Path="samples/02-agents/AgentProviders/openai/Agent_With_OpenAIChatCompletion/Agent_With_OpenAIChatCompletion.csproj" />
<Project Path="samples/02-agents/AgentProviders/openai/Agent_With_OpenAIResponses/Agent_With_OpenAIResponses.csproj" />
<Project Path="samples/02-agents/AgentProviders/Agent_With_A2A/Agent_With_A2A.csproj" />
<Project Path="samples/02-agents/AgentProviders/Agent_With_Anthropic/Agent_With_Anthropic.csproj" />
<Project Path="samples/02-agents/AgentProviders/Agent_With_AzureAIAgentsPersistent/Agent_With_AzureAIAgentsPersistent.csproj" />
<Project Path="samples/02-agents/AgentProviders/Agent_With_AzureAIProject/Agent_With_AzureAIProject.csproj" />
<Project Path="samples/02-agents/AgentProviders/Agent_With_AzureFoundryModel/Agent_With_AzureFoundryModel.csproj" />
<Project Path="samples/02-agents/AgentProviders/Agent_With_AzureOpenAIChatCompletion/Agent_With_AzureOpenAIChatCompletion.csproj" />
<Project Path="samples/02-agents/AgentProviders/Agent_With_AzureOpenAIResponses/Agent_With_AzureOpenAIResponses.csproj" />
<Project Path="samples/02-agents/AgentProviders/Agent_With_CustomImplementation/Agent_With_CustomImplementation.csproj" />
<Project Path="samples/02-agents/AgentProviders/Agent_With_GitHubCopilot/Agent_With_GitHubCopilot.csproj" />
<Project Path="samples/02-agents/AgentProviders/Agent_With_GoogleGemini/Agent_With_GoogleGemini.csproj" />
<Project Path="samples/02-agents/AgentProviders/Agent_With_Ollama/Agent_With_Ollama.csproj" />
<Project Path="samples/02-agents/AgentProviders/Agent_With_ONNX/Agent_With_ONNX.csproj" />
<Project Path="samples/02-agents/AgentProviders/Agent_With_OpenAIChatCompletion/Agent_With_OpenAIChatCompletion.csproj" />
<Project Path="samples/02-agents/AgentProviders/Agent_With_OpenAIResponses/Agent_With_OpenAIResponses.csproj" />
</Folder>
<Folder Name="/Samples/05-end-to-end/DevUIAspireIntegration/">
<Project Path="samples/05-end-to-end/DevUIAspireIntegration/DevUIIntegration.AppHost/DevUIIntegration.AppHost.csproj" />
@@ -66,13 +66,28 @@
<Project Path="samples/02-agents/Agents/Agent_Step19_InFunctionLoopCheckpointing/Agent_Step19_InFunctionLoopCheckpointing.csproj" />
<Project Path="samples/02-agents/Agents/Agent_Step20_DynamicFunctionTools/Agent_Step20_DynamicFunctionTools.csproj" />
<Project Path="samples/02-agents/Agents/Agent_Step21_ShellWithEnvironment/Agent_Step21_ShellWithEnvironment.csproj" />
<Project Path="samples/02-agents/Agents/Agent_Step22_AgentMode/Agent_Step22_AgentMode.csproj" />
<Project Path="samples/02-agents/Agents/Agent_Step23_TodoList/Agent_Step23_TodoList.csproj" />
<Project Path="samples/02-agents/Agents/Agent_Step24_MultiModelRouting/Agent_Step24_MultiModelRouting.csproj" />
</Folder>
<Folder Name="/Samples/02-agents/DeclarativeAgents/">
<Project Path="samples/02-agents/DeclarativeAgents/ChatClient/DeclarativeChatClientAgents.csproj" />
</Folder>
<Folder Name="/Samples/04-hosting/DurableWorkflows/" />
<Folder Name="/Samples/04-hosting/DurableWorkflows/ConsoleApps/">
<Project Path="samples/04-hosting/DurableWorkflows/ConsoleApps/01_SequentialWorkflow/01_SequentialWorkflow.csproj" />
<Project Path="samples/04-hosting/DurableWorkflows/ConsoleApps/02_ConcurrentWorkflow/02_ConcurrentWorkflow.csproj" />
<Project Path="samples/04-hosting/DurableWorkflows/ConsoleApps/03_ConditionalEdges/03_ConditionalEdges.csproj" />
<Project Path="samples/04-hosting/DurableWorkflows/ConsoleApps/04_WorkflowAndAgents/04_WorkflowAndAgents.csproj" />
<Project Path="samples/04-hosting/DurableWorkflows/ConsoleApps/05_WorkflowEvents/05_WorkflowEvents.csproj" />
<Project Path="samples/04-hosting/DurableWorkflows/ConsoleApps/06_WorkflowSharedState/06_WorkflowSharedState.csproj" />
<Project Path="samples/04-hosting/DurableWorkflows/ConsoleApps/07_SubWorkflows/07_SubWorkflows.csproj" />
<Project Path="samples/04-hosting/DurableWorkflows/ConsoleApps/08_WorkflowHITL/08_WorkflowHITL.csproj" />
</Folder>
<Folder Name="/Samples/04-hosting/DurableWorkflows/AzureFunctions/">
<Project Path="samples/04-hosting/DurableWorkflows/AzureFunctions/01_SequentialWorkflow/01_SequentialWorkflow.csproj" />
<Project Path="samples/04-hosting/DurableWorkflows/AzureFunctions/02_ConcurrentWorkflow/02_ConcurrentWorkflow.csproj" />
<Project Path="samples/04-hosting/DurableWorkflows/AzureFunctions/03_WorkflowHITL/03_WorkflowHITL.csproj" />
<Project Path="samples/04-hosting/DurableWorkflows/AzureFunctions/04_WorkflowMcpTool/04_WorkflowMcpTool.csproj" />
<Project Path="samples/04-hosting/DurableWorkflows/AzureFunctions/05_WorkflowAndAgents/05_WorkflowAndAgents.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/">
<File Path="samples/GettingStarted/README.md" />
</Folder>
@@ -103,17 +118,9 @@
<Project Path="samples/02-agents/AgentSkills/Agent_Step04_MixedSkills/Agent_Step04_MixedSkills.csproj" />
<Project Path="samples/02-agents/AgentSkills/Agent_Step05_SkillsWithDI/Agent_Step05_SkillsWithDI.csproj" />
<Project Path="samples/02-agents/AgentSkills/Agent_Step06_McpBasedSkills/Agent_Step06_McpBasedSkills.csproj" />
<Project Path="samples/02-agents/AgentSkills/Agent_Step07_SkillsAutoApproval/Agent_Step07_SkillsAutoApproval.csproj" />
</Folder>
<Folder Name="/Samples/02-agents/Harness/">
<File Path="samples/02-agents/Harness/README.md" />
<Project Path="samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step01_MeetYourClaw/Claw_Step01_MeetYourClaw.csproj" />
<Project Path="samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step02_WorkingWithData/Claw_Step02_WorkingWithData.csproj" />
<Project Path="samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step03_ScalingCapabilities/Claw_Step03_ScalingCapabilities.csproj" />
<Project Path="samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step04_ProductionReady/ClawAgent/ClawAgent.csproj" />
<Project Path="samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step04_ProductionReady/ClawAgent.Console/ClawAgent.Console.csproj" />
<Project Path="samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step04_ProductionReady/ClawAgent.Hosted/ClawAgent.Hosted.csproj" />
<Project Path="samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step04_ProductionReady/ClawAgent.Evals/ClawAgent.Evals.csproj" />
<Project Path="samples/02-agents/Harness/ConsoleReactiveComponents/ConsoleReactiveComponents.csproj" />
<Project Path="samples/02-agents/Harness/ConsoleReactiveFramework/ConsoleReactiveFramework.csproj" />
<Project Path="samples/02-agents/Harness/Harness_Shared_Console/Harness_Shared_Console.csproj" />
@@ -122,7 +129,6 @@
<Project Path="samples/02-agents/Harness/Harness_Step02_Research_WithBackgroundAgents/Harness_Step02_Research_WithBackgroundAgents.csproj" />
<Project Path="samples/02-agents/Harness/Harness_Step03_DataProcessing/Harness_Step03_DataProcessing.csproj" />
<Project Path="samples/02-agents/Harness/Harness_Step04_CodeExecution/Harness_Step04_CodeExecution.csproj" />
<Project Path="samples/02-agents/Harness/Harness_Step05_Loop/Harness_Step05_Loop.csproj" />
</Folder>
<Folder Name="/Samples/02-agents/AGUI/Step05_StateManagement/">
<Project Path="samples/02-agents/AGUI/Step05_StateManagement/Client/Client.csproj" />
@@ -132,43 +138,43 @@
<File Path="samples/02-agents/DevUI/README.md" />
<Project Path="samples/02-agents/DevUI/DevUI_Step01_BasicUsage/DevUI_Step01_BasicUsage.csproj" />
</Folder>
<Folder Name="/Samples/02-agents/AgentProviders/anthropic/">
<File Path="samples/02-agents/AgentProviders/anthropic/README.md" />
<Project Path="samples/02-agents/AgentProviders/anthropic/Agent_Anthropic_Step01_Running/Agent_Anthropic_Step01_Running.csproj" />
<Project Path="samples/02-agents/AgentProviders/anthropic/Agent_Anthropic_Step02_Reasoning/Agent_Anthropic_Step02_Reasoning.csproj" />
<Project Path="samples/02-agents/AgentProviders/anthropic/Agent_Anthropic_Step03_UsingFunctionTools/Agent_Anthropic_Step03_UsingFunctionTools.csproj" />
<Project Path="samples/02-agents/AgentProviders/anthropic/Agent_Anthropic_Step04_UsingSkills/Agent_Anthropic_Step04_UsingSkills.csproj" />
<Folder Name="/Samples/02-agents/AgentWithAnthropic/">
<File Path="samples/02-agents/AgentWithAnthropic/README.md" />
<Project Path="samples/02-agents/AgentWithAnthropic/Agent_Anthropic_Step01_Running/Agent_Anthropic_Step01_Running.csproj" />
<Project Path="samples/02-agents/AgentWithAnthropic/Agent_Anthropic_Step02_Reasoning/Agent_Anthropic_Step02_Reasoning.csproj" />
<Project Path="samples/02-agents/AgentWithAnthropic/Agent_Anthropic_Step03_UsingFunctionTools/Agent_Anthropic_Step03_UsingFunctionTools.csproj" />
<Project Path="samples/02-agents/AgentWithAnthropic/Agent_Anthropic_Step04_UsingSkills/Agent_Anthropic_Step04_UsingSkills.csproj" />
</Folder>
<Folder Name="/Samples/02-agents/AgentProviders/foundry/">
<File Path="samples/02-agents/AgentProviders/foundry/README.md" />
<Project Path="samples/02-agents/AgentProviders/foundry/Agent_Step00_FoundryAgentLifecycle/Agent_Step00_FoundryAgentLifecycle.csproj" />
<Project Path="samples/02-agents/AgentProviders/foundry/Agent_Step01_Basics/Agent_Step01_Basics.csproj" />
<Project Path="samples/02-agents/AgentProviders/foundry/Agent_Step02.1_MultiturnConversation/Agent_Step02.1_MultiturnConversation.csproj" />
<Project Path="samples/02-agents/AgentProviders/foundry/Agent_Step02.2_MultiturnWithServerConversations/Agent_Step02.2_MultiturnWithServerConversations.csproj" />
<Project Path="samples/02-agents/AgentProviders/foundry/Agent_Step03_UsingFunctionTools/Agent_Step03_UsingFunctionTools.csproj" />
<Project Path="samples/02-agents/AgentProviders/foundry/Agent_Step04_UsingFunctionToolsWithApprovals/Agent_Step04_UsingFunctionToolsWithApprovals.csproj" />
<Project Path="samples/02-agents/AgentProviders/foundry/Agent_Step05_StructuredOutput/Agent_Step05_StructuredOutput.csproj" />
<Project Path="samples/02-agents/AgentProviders/foundry/Agent_Step06_PersistedConversations/Agent_Step06_PersistedConversations.csproj" />
<Project Path="samples/02-agents/AgentProviders/foundry/Agent_Step07_Observability/Agent_Step07_Observability.csproj" />
<Project Path="samples/02-agents/AgentProviders/foundry/Agent_Step08_DependencyInjection/Agent_Step08_DependencyInjection.csproj" />
<Project Path="samples/02-agents/AgentProviders/foundry/Agent_Step09_UsingMcpClientAsTools/Agent_Step09_UsingMcpClientAsTools.csproj" />
<Project Path="samples/02-agents/AgentProviders/foundry/Agent_Step10_UsingImages/Agent_Step10_UsingImages.csproj" />
<Project Path="samples/02-agents/AgentProviders/foundry/Agent_Step11_AsFunctionTool/Agent_Step11_AsFunctionTool.csproj" />
<Project Path="samples/02-agents/AgentProviders/foundry/Agent_Step12_Middleware/Agent_Step12_Middleware.csproj" />
<Project Path="samples/02-agents/AgentProviders/foundry/Agent_Step13_Plugins/Agent_Step13_Plugins.csproj" />
<Project Path="samples/02-agents/AgentProviders/foundry/Agent_Step14_CodeInterpreter/Agent_Step14_CodeInterpreter.csproj" />
<Project Path="samples/02-agents/AgentProviders/foundry/Agent_Step15_ComputerUse/Agent_Step15_ComputerUse.csproj" />
<Project Path="samples/02-agents/AgentProviders/foundry/Agent_Step16_FileSearch/Agent_Step16_FileSearch.csproj" />
<Project Path="samples/02-agents/AgentProviders/foundry/Agent_Step17_OpenAPITools/Agent_Step17_OpenAPITools.csproj" />
<Project Path="samples/02-agents/AgentProviders/foundry/Agent_Step18_BingCustomSearch/Agent_Step18_BingCustomSearch.csproj" />
<Project Path="samples/02-agents/AgentProviders/foundry/Agent_Step19_SharePoint/Agent_Step19_SharePoint.csproj" />
<Project Path="samples/02-agents/AgentProviders/foundry/Agent_Step20_MicrosoftFabric/Agent_Step20_MicrosoftFabric.csproj" />
<Project Path="samples/02-agents/AgentProviders/foundry/Agent_Step21_WebSearch/Agent_Step21_WebSearch.csproj" />
<Project Path="samples/02-agents/AgentProviders/foundry/Agent_Step22_MemorySearch/Agent_Step22_MemorySearch.csproj" />
<Project Path="samples/02-agents/AgentProviders/foundry/Agent_Step23_LocalMCP/Agent_Step23_LocalMCP.csproj" />
<Project Path="samples/02-agents/AgentProviders/foundry/Agent_Step24_CodeInterpreterFileDownload/Agent_Step24_CodeInterpreterFileDownload.csproj" />
<Project Path="samples/02-agents/AgentProviders/foundry/Agent_Step25_FoundryToolboxMcp/Agent_Step25_FoundryToolboxMcp.csproj" />
<Project Path="samples/02-agents/AgentProviders/foundry/Agent_Step26_FoundryToolboxMcpSkills/Agent_Step26_FoundryToolboxMcpSkills.csproj" />
<Folder Name="/Samples/02-agents/AgentsWithFoundry/">
<File Path="samples/02-agents/AgentsWithFoundry/README.md" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step00_FoundryAgentLifecycle/Agent_Step00_FoundryAgentLifecycle.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step01_Basics/Agent_Step01_Basics.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step02.1_MultiturnConversation/Agent_Step02.1_MultiturnConversation.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step02.2_MultiturnWithServerConversations/Agent_Step02.2_MultiturnWithServerConversations.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step03_UsingFunctionTools/Agent_Step03_UsingFunctionTools.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step04_UsingFunctionToolsWithApprovals/Agent_Step04_UsingFunctionToolsWithApprovals.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step05_StructuredOutput/Agent_Step05_StructuredOutput.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step06_PersistedConversations/Agent_Step06_PersistedConversations.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step07_Observability/Agent_Step07_Observability.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step08_DependencyInjection/Agent_Step08_DependencyInjection.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step09_UsingMcpClientAsTools/Agent_Step09_UsingMcpClientAsTools.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step10_UsingImages/Agent_Step10_UsingImages.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step11_AsFunctionTool/Agent_Step11_AsFunctionTool.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step12_Middleware/Agent_Step12_Middleware.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step13_Plugins/Agent_Step13_Plugins.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step14_CodeInterpreter/Agent_Step14_CodeInterpreter.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step15_ComputerUse/Agent_Step15_ComputerUse.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step16_FileSearch/Agent_Step16_FileSearch.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step17_OpenAPITools/Agent_Step17_OpenAPITools.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step18_BingCustomSearch/Agent_Step18_BingCustomSearch.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step19_SharePoint/Agent_Step19_SharePoint.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step20_MicrosoftFabric/Agent_Step20_MicrosoftFabric.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step21_WebSearch/Agent_Step21_WebSearch.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step22_MemorySearch/Agent_Step22_MemorySearch.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step23_LocalMCP/Agent_Step23_LocalMCP.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step24_CodeInterpreterFileDownload/Agent_Step24_CodeInterpreterFileDownload.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step25_FoundryToolboxMcp/Agent_Step25_FoundryToolboxMcp.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step26_FoundryToolboxMcpSkills/Agent_Step26_FoundryToolboxMcpSkills.csproj" />
</Folder>
<Folder Name="/Samples/02-agents/Evaluation/">
<Project Path="samples/02-agents/Evaluation/Evaluation_CustomEvals/Evaluation_CustomEvals.csproj" />
@@ -186,22 +192,17 @@
<File Path="samples/02-agents/AgentWithMemory/README.md" />
<Project Path="samples/02-agents/AgentWithMemory/AgentWithMemory_Step01_ChatHistoryMemory/AgentWithMemory_Step01_ChatHistoryMemory.csproj" />
<Project Path="samples/02-agents/AgentWithMemory/AgentWithMemory_Step02_MemoryUsingMem0/AgentWithMemory_Step02_MemoryUsingMem0.csproj" />
<Project Path="samples/02-agents/AgentWithMemory/AgentWithMemory_Step03_MemoryUsingValkey/AgentWithMemory_Step03_MemoryUsingValkey.csproj" />
<Project Path="samples/02-agents/AgentWithMemory/AgentWithMemory_Step03_MemoryUsingValkey_Bedrock/AgentWithMemory_Step03_MemoryUsingValkey_Bedrock.csproj" />
<Project Path="samples/02-agents/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/AgentWithMemory_Step04_MemoryUsingFoundry.csproj" />
<Project Path="samples/02-agents/AgentWithMemory/AgentWithMemory_Step05_BoundedChatHistory/AgentWithMemory_Step05_BoundedChatHistory.csproj" />
<Project Path="samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory.csproj" />
<Project Path="samples/02-agents/AgentWithMemory/AgentWithMemory_Step07_FileMemoryProvider/AgentWithMemory_Step07_FileMemoryProvider.csproj" />
<Project Path="samples/02-agents/AgentWithMemory/AgentWithMemory_Step08_MemoryUsingCosmosNoSql/AgentWithMemory_Step08_MemoryUsingCosmosNoSql.csproj" />
</Folder>
<Folder Name="/Samples/02-agents/AgentProviders/openai/">
<File Path="samples/02-agents/AgentProviders/openai/README.md" />
<Project Path="samples/02-agents/AgentProviders/openai/Agent_OpenAI_Step01_Running/Agent_OpenAI_Step01_Running.csproj" />
<Project Path="samples/02-agents/AgentProviders/openai/Agent_OpenAI_Step02_Reasoning/Agent_OpenAI_Step02_Reasoning.csproj" />
<Project Path="samples/02-agents/AgentProviders/openai/Agent_OpenAI_Step03_CreateFromChatClient/Agent_OpenAI_Step03_CreateFromChatClient.csproj" />
<Project Path="samples/02-agents/AgentProviders/openai/Agent_OpenAI_Step04_CreateFromOpenAIResponseClient/Agent_OpenAI_Step04_CreateFromOpenAIResponseClient.csproj" />
<Project Path="samples/02-agents/AgentProviders/openai/Agent_OpenAI_Step05_Conversation/Agent_OpenAI_Step05_Conversation.csproj" />
<Project Path="samples/02-agents/AgentProviders/openai/Agent_OpenAI_Step06_CodeInterpreterFileDownload/Agent_OpenAI_Step06_CodeInterpreterFileDownload.csproj" />
<Folder Name="/Samples/02-agents/AgentWithOpenAI/">
<File Path="samples/02-agents/AgentWithOpenAI/README.md" />
<Project Path="samples/02-agents/AgentWithOpenAI/Agent_OpenAI_Step01_Running/Agent_OpenAI_Step01_Running.csproj" />
<Project Path="samples/02-agents/AgentWithOpenAI/Agent_OpenAI_Step02_Reasoning/Agent_OpenAI_Step02_Reasoning.csproj" />
<Project Path="samples/02-agents/AgentWithOpenAI/Agent_OpenAI_Step03_CreateFromChatClient/Agent_OpenAI_Step03_CreateFromChatClient.csproj" />
<Project Path="samples/02-agents/AgentWithOpenAI/Agent_OpenAI_Step04_CreateFromOpenAIResponseClient/Agent_OpenAI_Step04_CreateFromOpenAIResponseClient.csproj" />
<Project Path="samples/02-agents/AgentWithOpenAI/Agent_OpenAI_Step05_Conversation/Agent_OpenAI_Step05_Conversation.csproj" />
<Project Path="samples/02-agents/AgentWithOpenAI/Agent_OpenAI_Step06_CodeInterpreterFileDownload/Agent_OpenAI_Step06_CodeInterpreterFileDownload.csproj" />
</Folder>
<Folder Name="/Samples/02-agents/AgentWithRAG/">
<File Path="samples/02-agents/AgentWithRAG/README.md" />
@@ -214,7 +215,6 @@
<Folder Name="/Samples/02-agents/ModelContextProtocol/">
<File Path="samples/02-agents/ModelContextProtocol/README.md" />
<Project Path="samples/02-agents/ModelContextProtocol/Agent_MCP_LongRunningTask_Client/Agent_MCP_LongRunningTask_Client.csproj" />
<Project Path="samples/02-agents/ModelContextProtocol/Agent_MCP_PerRun_AuthHeaders/Agent_MCP_PerRun_AuthHeaders.csproj" />
<Project Path="samples/02-agents/ModelContextProtocol/Agent_MCP_Server/Agent_MCP_Server.csproj" />
<Project Path="samples/02-agents/ModelContextProtocol/Agent_MCP_Server_Auth/Agent_MCP_Server_Auth.csproj" />
<Project Path="samples/02-agents/ModelContextProtocol/FoundryAgent_Hosted_MCP/FoundryAgent_Hosted_MCP.csproj" />
@@ -237,10 +237,10 @@
</Folder>
<Folder Name="/Samples/03-workflows/Declarative/">
<File Path="samples/03-workflows/Declarative/README.md" />
<Project Path="samples/03-workflows/Declarative/AotCheckpointing/AotCheckpointing.csproj" />
<Project Path="samples/03-workflows/Declarative/ConfirmInput/ConfirmInput.csproj" />
<Project Path="samples/03-workflows/Declarative/CustomerSupport/CustomerSupport.csproj" />
<Project Path="samples/03-workflows/Declarative/DeepResearch/DeepResearch.csproj" />
<Project Path="samples/03-workflows/Declarative/ExecuteCode/ExecuteCode.csproj" />
<Project Path="samples/03-workflows/Declarative/ExecuteWorkflow/ExecuteWorkflow.csproj" />
<Project Path="samples/03-workflows/Declarative/FunctionTools/FunctionTools.csproj" />
<Project Path="samples/03-workflows/Declarative/HostedWorkflow/HostedWorkflow.csproj" />
@@ -306,19 +306,7 @@
<Project Path="samples/03-workflows/Evaluation/Evaluation_WorkflowEval/Evaluation_WorkflowEval.csproj" />
<Project Path="samples/03-workflows/Evaluation/Evaluation_WorkflowExpectedOutputs/Evaluation_WorkflowExpectedOutputs.csproj" />
</Folder>
<Folder Name="/Samples/04-hosting/" />
<Folder Name="/Samples/04-hosting/af-hosting/">
<File Path="samples/04-hosting/af-hosting/README.md" />
</Folder>
<Folder Name="/Samples/04-hosting/af-hosting/local_responses/">
<File Path="samples/04-hosting/af-hosting/local_responses/README.md" />
<Project Path="samples/04-hosting/af-hosting/local_responses/Server/Server.csproj" />
<Project Path="samples/04-hosting/af-hosting/local_responses/Client/Client.csproj" />
</Folder>
<Folder Name="/Samples/04-hosting/af-hosting/local_responses_workflow/">
<File Path="samples/04-hosting/af-hosting/local_responses_workflow/README.md" />
<Project Path="samples/04-hosting/af-hosting/local_responses_workflow/Server/Server.csproj" />
<Project Path="samples/04-hosting/af-hosting/local_responses_workflow/Client/Client.csproj" />
<Folder Name="/Samples/04-hosting/">
</Folder>
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/" />
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/invocations/" />
@@ -332,9 +320,6 @@
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/">
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/HostedChatClientAgent.csproj" />
</Folder>
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent-Dockerfile/">
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent-Dockerfile/HostedChatClientAgentDocker.csproj" />
</Folder>
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/">
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/HostedFoundryAgent.csproj" />
</Folder>
@@ -344,9 +329,6 @@
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/">
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/HostedLocalTools.csproj" />
</Folder>
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/">
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/HostedLocalCodeAct.csproj" />
</Folder>
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/">
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/HostedMcpTools.csproj" />
</Folder>
@@ -362,9 +344,6 @@
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/">
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/HostedToolbox.csproj" />
</Folder>
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/">
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/Hosted-Toolbox-AuthPaths.csproj" />
</Folder>
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/">
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/HostedToolboxMcpSkills.csproj" />
</Folder>
@@ -377,29 +356,39 @@
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/">
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/HostedWorkflowSimple.csproj" />
</Folder>
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Resilient/">
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Resilient/HostedWorkflowResilient.csproj" />
</Folder>
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Resilient-Long-Running/">
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Resilient-Long-Running/HostedWorkflowResilientLongRunning.csproj" />
</Folder>
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-Steering/">
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-Steering/HostedSteering.csproj" />
</Folder>
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/">
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/Hosted-Toolbox-AuthPaths-Client/Hosted-Toolbox-AuthPaths-Client.csproj" />
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SessionFilesClient/SessionFilesClient.csproj" />
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent/SimpleAgent.csproj" />
</Folder>
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Using-E2E-Resilience/">
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Using-E2E-Resilience/Using-E2E-Resilience.csproj" />
</Folder>
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/">
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/HostedWorkflowHandoff.csproj" />
</Folder>
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/">
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/HostedAgentSkills.csproj" />
</Folder>
<Folder Name="/Samples/04-hosting/DurableAgents/" />
<Folder Name="/Samples/04-hosting/DurableAgents/AzureFunctions/">
<File Path="samples/04-hosting/DurableAgents/AzureFunctions/.editorconfig" />
<File Path="samples/04-hosting/DurableAgents/AzureFunctions/README.md" />
<Project Path="samples/04-hosting/DurableAgents/AzureFunctions/01_SingleAgent/01_SingleAgent.csproj" />
<Project Path="samples/04-hosting/DurableAgents/AzureFunctions/02_AgentOrchestration_Chaining/02_AgentOrchestration_Chaining.csproj" />
<Project Path="samples/04-hosting/DurableAgents/AzureFunctions/03_AgentOrchestration_Concurrency/03_AgentOrchestration_Concurrency.csproj" />
<Project Path="samples/04-hosting/DurableAgents/AzureFunctions/04_AgentOrchestration_Conditionals/04_AgentOrchestration_Conditionals.csproj" />
<Project Path="samples/04-hosting/DurableAgents/AzureFunctions/05_AgentOrchestration_HITL/05_AgentOrchestration_HITL.csproj" />
<Project Path="samples/04-hosting/DurableAgents/AzureFunctions/06_LongRunningTools/06_LongRunningTools.csproj" />
<Project Path="samples/04-hosting/DurableAgents/AzureFunctions/07_AgentAsMcpTool/07_AgentAsMcpTool.csproj" />
<Project Path="samples/04-hosting/DurableAgents/AzureFunctions/08_ReliableStreaming/08_ReliableStreaming.csproj" />
</Folder>
<Folder Name="/Samples/04-hosting/DurableAgents/ConsoleApps/">
<File Path="samples/04-hosting/DurableAgents/ConsoleApps/README.md" />
<Project Path="samples/04-hosting/DurableAgents/ConsoleApps/01_SingleAgent/01_SingleAgent.csproj" />
<Project Path="samples/04-hosting/DurableAgents/ConsoleApps/02_AgentOrchestration_Chaining/02_AgentOrchestration_Chaining.csproj" />
<Project Path="samples/04-hosting/DurableAgents/ConsoleApps/03_AgentOrchestration_Concurrency/03_AgentOrchestration_Concurrency.csproj" />
<Project Path="samples/04-hosting/DurableAgents/ConsoleApps/04_AgentOrchestration_Conditionals/04_AgentOrchestration_Conditionals.csproj" />
<Project Path="samples/04-hosting/DurableAgents/ConsoleApps/05_AgentOrchestration_HITL/05_AgentOrchestration_HITL.csproj" />
<Project Path="samples/04-hosting/DurableAgents/ConsoleApps/06_LongRunningTools/06_LongRunningTools.csproj" />
<Project Path="samples/04-hosting/DurableAgents/ConsoleApps/07_ReliableStreaming/07_ReliableStreaming.csproj" />
</Folder>
<Folder Name="/Samples/02-agents/A2A/">
<File Path="samples/02-agents/A2A/README.md" />
<Project Path="samples/02-agents/A2A/A2AAgent_AsFunctionTools/A2AAgent_AsFunctionTools.csproj" />
@@ -414,7 +403,6 @@
<Folder Name="/Samples/05-end-to-end/Evaluation/">
<Project Path="samples/05-end-to-end/Evaluation/Evaluation_ConversationSplits/Evaluation_ConversationSplits.csproj" />
<Project Path="samples/05-end-to-end/Evaluation/Evaluation_FoundryQuality/Evaluation_FoundryQuality.csproj" />
<Project Path="samples/05-end-to-end/Evaluation/Evaluation_FoundryRubric/Evaluation_FoundryRubric.csproj" />
<Project Path="samples/05-end-to-end/Evaluation/Evaluation_MixedProviders/Evaluation_MixedProviders.csproj" />
</Folder>
<Folder Name="/Samples/05-end-to-end/A2AClientServer/">
@@ -603,12 +591,14 @@
<Project Path="src/Aspire.Hosting.AgentFramework.DevUI/Aspire.Hosting.AgentFramework.DevUI.csproj" />
<Project Path="src/Microsoft.Agents.AI.A2A/Microsoft.Agents.AI.A2A.csproj" />
<Project Path="src/Microsoft.Agents.AI.Abstractions/Microsoft.Agents.AI.Abstractions.csproj" />
<Project Path="src/Microsoft.Agents.AI.AGUI/Microsoft.Agents.AI.AGUI.csproj" />
<Project Path="src/Microsoft.Agents.AI.Anthropic/Microsoft.Agents.AI.Anthropic.csproj" />
<Project Path="src/Microsoft.Agents.AI.AzureAI.Persistent/Microsoft.Agents.AI.AzureAI.Persistent.csproj" />
<Project Path="src/Microsoft.Agents.AI.CopilotStudio/Microsoft.Agents.AI.CopilotStudio.csproj" />
<Project Path="src/Microsoft.Agents.AI.CosmosNoSql/Microsoft.Agents.AI.CosmosNoSql.csproj" />
<Project Path="src/Microsoft.Agents.AI.Declarative/Microsoft.Agents.AI.Declarative.csproj" />
<Project Path="src/Microsoft.Agents.AI.DevUI/Microsoft.Agents.AI.DevUI.csproj" />
<Project Path="src/Microsoft.Agents.AI.DurableTask/Microsoft.Agents.AI.DurableTask.csproj" />
<Project Path="src/Microsoft.Agents.AI.Foundry.Hosting/Microsoft.Agents.AI.Foundry.Hosting.csproj" />
<Project Path="src/Microsoft.Agents.AI.Foundry/Microsoft.Agents.AI.Foundry.csproj" />
<Project Path="src/Microsoft.Agents.AI.GitHub.Copilot/Microsoft.Agents.AI.GitHub.Copilot.csproj" />
@@ -616,19 +606,16 @@
<Project Path="src/Microsoft.Agents.AI.Hosting.A2A.AspNetCore/Microsoft.Agents.AI.Hosting.A2A.AspNetCore.csproj" />
<Project Path="src/Microsoft.Agents.AI.Hosting.A2A/Microsoft.Agents.AI.Hosting.A2A.csproj" />
<Project Path="src/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.csproj" />
<Project Path="src/Microsoft.Agents.AI.Hosting.AspNetCore/Microsoft.Agents.AI.Hosting.AspNetCore.csproj" />
<Project Path="src/Microsoft.Agents.AI.Hosting.AzureStorage/Microsoft.Agents.AI.Hosting.AzureStorage.csproj" />
<Project Path="src/Microsoft.Agents.AI.Hosting.AzureFunctions/Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
<Project Path="src/Microsoft.Agents.AI.Hosting.OpenAI/Microsoft.Agents.AI.Hosting.OpenAI.csproj" />
<Project Path="src/Microsoft.Agents.AI.Hosting.AspNetCore/Microsoft.Agents.AI.Hosting.AspNetCore.csproj" />
<Project Path="src/Microsoft.Agents.AI.Hosting/Microsoft.Agents.AI.Hosting.csproj" />
<Project Path="src/Microsoft.Agents.AI.Hyperlight/Microsoft.Agents.AI.Hyperlight.csproj" />
<Project Path="src/Microsoft.Agents.AI.LocalCodeAct/Microsoft.Agents.AI.LocalCodeAct.csproj" />
<Project Path="src/Microsoft.Agents.AI.Mcp/Microsoft.Agents.AI.Mcp.csproj" />
<Project Path="src/Microsoft.Agents.AI.AgentHooks/Microsoft.Agents.AI.AgentHooks.csproj" />
<Project Path="src/Microsoft.Agents.AI.Mem0/Microsoft.Agents.AI.Mem0.csproj" />
<Project Path="src/Microsoft.Agents.AI.OpenAI/Microsoft.Agents.AI.OpenAI.csproj" />
<Project Path="src/Microsoft.Agents.AI.Purview/Microsoft.Agents.AI.Purview.csproj" />
<Project Path="src/Microsoft.Agents.AI.Tools.Shell/Microsoft.Agents.AI.Tools.Shell.csproj" />
<Project Path="src/Microsoft.Agents.AI.Valkey/Microsoft.Agents.AI.Valkey.csproj" />
<Project Path="src/Microsoft.Agents.AI.Workflows.Declarative.Foundry/Microsoft.Agents.AI.Workflows.Declarative.Foundry.csproj" />
<Project Path="src/Microsoft.Agents.AI.Workflows.Declarative.Mcp/Microsoft.Agents.AI.Workflows.Declarative.Mcp.csproj" />
<Project Path="src/Microsoft.Agents.AI.Workflows.Declarative/Microsoft.Agents.AI.Workflows.Declarative.csproj" />
@@ -636,9 +623,7 @@
<Project Path="src/Microsoft.Agents.AI.Workflows/Microsoft.Agents.AI.Workflows.csproj" />
<Project Path="src/Microsoft.Agents.AI/Microsoft.Agents.AI.csproj" />
</Folder>
<Folder Name="/Tests/">
<Project Path="tests/Microsoft.Agents.AI.Hosting.OpenAI.IntegrationTests/Microsoft.Agents.AI.Hosting.OpenAI.IntegrationTests.csproj" />
</Folder>
<Folder Name="/Tests/" />
<Folder Name="/Tests/IntegrationTests/">
<Project Path="tests/AgentConformance.IntegrationTests/AgentConformance.IntegrationTests.csproj" />
<Project Path="tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletion.IntegrationTests.csproj" />
@@ -647,9 +632,10 @@
<Project Path="tests/Foundry.Hosting.IntegrationTests.TestContainer/Foundry.Hosting.IntegrationTests.TestContainer.csproj" />
<Project Path="tests/Foundry.Hosting.IntegrationTests/Foundry.Hosting.IntegrationTests.csproj" />
<Project Path="tests/Foundry.IntegrationTests/Foundry.IntegrationTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/Microsoft.Agents.AI.DurableTask.IntegrationTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.GitHub.Copilot.IntegrationTests/Microsoft.Agents.AI.GitHub.Copilot.IntegrationTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Hosting.AzureStorage.IntegrationTests/Microsoft.Agents.AI.Hosting.AzureStorage.IntegrationTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Hyperlight.IntegrationTests/Microsoft.Agents.AI.Hyperlight.IntegrationTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Mem0.IntegrationTests/Microsoft.Agents.AI.Mem0.IntegrationTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Tools.Shell.IntegrationTests/Microsoft.Agents.AI.Tools.Shell.IntegrationTests.csproj" />
@@ -662,33 +648,29 @@
<Project Path="tests/Aspire.Hosting.AgentFramework.DevUI.UnitTests/Aspire.Hosting.AgentFramework.DevUI.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.A2A.UnitTests/Microsoft.Agents.AI.A2A.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Abstractions.UnitTests/Microsoft.Agents.AI.Abstractions.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.AgentHooks.UnitTests/Microsoft.Agents.AI.AgentHooks.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.AGUI.UnitTests/Microsoft.Agents.AI.AGUI.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Anthropic.UnitTests/Microsoft.Agents.AI.Anthropic.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.AzureAI.Persistent.UnitTests/Microsoft.Agents.AI.AzureAI.Persistent.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.CosmosNoSql.UnitTests/Microsoft.Agents.AI.CosmosNoSql.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Declarative.UnitTests/Microsoft.Agents.AI.Declarative.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.DevUI.UnitTests/Microsoft.Agents.AI.DevUI.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.FeatureRegistry.UnitTests/Microsoft.Agents.AI.FeatureRegistry.UnitTests.csproj">
<Build Solution="Debug|*" Project="false" />
</Project>
<Project Path="tests/Microsoft.Agents.AI.DurableTask.UnitTests/Microsoft.Agents.AI.DurableTask.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Foundry.UnitTests/Microsoft.Agents.AI.Foundry.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.GitHub.Copilot.UnitTests/Microsoft.Agents.AI.GitHub.Copilot.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Harness.UnitTests/Microsoft.Agents.AI.Harness.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/Microsoft.Agents.AI.Hosting.A2A.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Hosting.AzureStorage.UnitTests/Microsoft.Agents.AI.Hosting.AzureStorage.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Hosting.AzureFunctions.UnitTests/Microsoft.Agents.AI.Hosting.AzureFunctions.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Hosting.UnitTests/Microsoft.Agents.AI.Hosting.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Hyperlight.UnitTests/Microsoft.Agents.AI.Hyperlight.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.LocalCodeAct.UnitTests/Microsoft.Agents.AI.LocalCodeAct.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Mcp.UnitTests/Microsoft.Agents.AI.Mcp.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Mem0.UnitTests/Microsoft.Agents.AI.Mem0.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.OpenAI.UnitTests/Microsoft.Agents.AI.OpenAI.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Purview.UnitTests/Microsoft.Agents.AI.Purview.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Tools.Shell.UnitTests/Microsoft.Agents.AI.Tools.Shell.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.UnitTests/Microsoft.Agents.AI.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Valkey.UnitTests/Microsoft.Agents.AI.Valkey.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Workflows.Declarative.Mcp.UnitTests/Microsoft.Agents.AI.Workflows.Declarative.Mcp.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Workflows.Generators.UnitTests/Microsoft.Agents.AI.Workflows.Generators.UnitTests.csproj" />
+3 -4
View File
@@ -4,7 +4,7 @@
"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.AgentHooks\\Microsoft.Agents.AI.AgentHooks.csproj",
"src\\Microsoft.Agents.AI.AGUI\\Microsoft.Agents.AI.AGUI.csproj",
"src\\Microsoft.Agents.AI.Anthropic\\Microsoft.Agents.AI.Anthropic.csproj",
"src\\Microsoft.Agents.AI.GitHub.Copilot\\Microsoft.Agents.AI.GitHub.Copilot.csproj",
"src\\Microsoft.Agents.AI.Harness\\Microsoft.Agents.AI.Harness.csproj",
@@ -15,21 +15,20 @@
"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.AspNetCore\\Microsoft.Agents.AI.Hosting.AspNetCore.csproj",
"src\\Microsoft.Agents.AI.Hosting.AzureStorage\\Microsoft.Agents.AI.Hosting.AzureStorage.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.LocalCodeAct\\Microsoft.Agents.AI.LocalCodeAct.csproj",
"src\\Microsoft.Agents.AI.Mcp\\Microsoft.Agents.AI.Mcp.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.Tools.Shell\\Microsoft.Agents.AI.Tools.Shell.csproj",
"src\\Microsoft.Agents.AI.Valkey\\Microsoft.Agents.AI.Valkey.csproj",
"src\\Microsoft.Agents.AI.Workflows.Declarative.Foundry\\Microsoft.Agents.AI.Workflows.Declarative.Foundry.csproj",
"src\\Microsoft.Agents.AI.Workflows.Declarative.Mcp\\Microsoft.Agents.AI.Workflows.Declarative.Mcp.csproj",
"src\\Microsoft.Agents.AI.Workflows.Declarative\\Microsoft.Agents.AI.Workflows.Declarative.csproj",
-6
View File
@@ -26,13 +26,7 @@
<ItemGroup Condition="'$(InjectSharedDiagnosticIds)' == 'true'">
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\DiagnosticIds\*.cs" LinkBase="Shared\DiagnosticIds" />
</ItemGroup>
<ItemGroup Condition="'$(InjectSharedUsage)' == 'true'">
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\Usage\*.cs" LinkBase="Shared\Usage" />
</ItemGroup>
<ItemGroup Condition="'$(InjectSharedRedaction)' == 'true'">
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\Redaction\*.cs" LinkBase="Shared\Redaction" />
</ItemGroup>
<ItemGroup Condition="'$(InjectSharedFeatureUsageUserAgent)' == 'true'">
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\FeatureUsage\*.cs" LinkBase="Shared\FeatureUsage" />
</ItemGroup>
</Project>

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