Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cd5d282827 | |||
| 535690cd1d | |||
| 85fde62a76 | |||
| 85eb53d412 | |||
| 2d7c8da6b0 | |||
| e0b0b79d9e | |||
| a4f6c26990 | |||
| 1389f304f2 | |||
| 93719f4a34 | |||
| a486374fd8 | |||
| e78604103d |
@@ -2,7 +2,6 @@ dirs:
|
||||
- .
|
||||
excludedFiles:
|
||||
- ./python/CHANGELOG.md
|
||||
- "**/SKILL.md"
|
||||
ignorePatterns:
|
||||
- pattern: "/github/"
|
||||
- pattern: "./actions"
|
||||
@@ -27,7 +26,7 @@ ignorePatterns:
|
||||
- pattern: "https:\/\/dotnet.microsoft.com"
|
||||
- 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
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -59,7 +59,7 @@ runs:
|
||||
id: azure-login
|
||||
if: ${{ (inputs.mode || 'app-with-fallback') != 'pat' }}
|
||||
continue-on-error: true
|
||||
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
|
||||
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2
|
||||
with:
|
||||
client-id: ${{ inputs.azure-client-id }}
|
||||
tenant-id: ${{ inputs.azure-tenant-id }}
|
||||
|
||||
@@ -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@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
||||
with:
|
||||
version-file: "python/pyproject.toml"
|
||||
enable-cache: true
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -58,5 +58,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]".
|
||||
@@ -9,14 +9,12 @@
|
||||
* @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
|
||||
* @returns {Promise<{author: string|null, isTeamMember: boolean}>}
|
||||
*/
|
||||
async function checkTeamMembership({ github, context, core, teamSlug, issueNumber, username = '' }) {
|
||||
let author = username.trim() || (
|
||||
async function checkTeamMembership({ github, context, core, teamSlug, issueNumber }) {
|
||||
let author =
|
||||
context.payload.issue?.user?.login ??
|
||||
context.payload.pull_request?.user?.login
|
||||
);
|
||||
context.payload.pull_request?.user?.login;
|
||||
|
||||
if (!author) {
|
||||
const number = Number(issueNumber);
|
||||
|
||||
@@ -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;
|
||||
@@ -74,12 +74,9 @@ code before the user has reviewed the plan**:
|
||||
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.
|
||||
was addressed (or the agreed outcome) — leave none unanswered.
|
||||
6. **Resolve resolved threads.** Mark a review thread as resolved only when the
|
||||
comment has actually been addressed.
|
||||
|
||||
### Useful commands
|
||||
|
||||
|
||||
@@ -74,28 +74,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' } },
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # 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@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # 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@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
@@ -6,9 +6,6 @@ on:
|
||||
- opened
|
||||
- reopened
|
||||
- ready_for_review
|
||||
issue_comment:
|
||||
types:
|
||||
- created
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
@@ -23,7 +20,7 @@ permissions:
|
||||
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,44 +28,9 @@ 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:
|
||||
@@ -82,7 +44,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 +52,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}"
|
||||
@@ -109,7 +67,7 @@ jobs:
|
||||
echo "repo=${GITHUB_REPOSITORY}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Checkout GitHub automation
|
||||
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 }}
|
||||
sparse-checkout: |
|
||||
@@ -133,11 +91,10 @@ jobs:
|
||||
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:
|
||||
@@ -150,33 +107,19 @@ jobs:
|
||||
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.`);
|
||||
core.info(`Author ${author} is a team member; proceeding with review.`);
|
||||
} else {
|
||||
core.info(`User ${author} is not a member of ${process.env.TEAM_NAME}; skipping review.`);
|
||||
core.info(`Author ${author} is not a member of ${process.env.TEAM_NAME}; skipping review.`);
|
||||
}
|
||||
|
||||
- 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',
|
||||
});
|
||||
|
||||
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
|
||||
@@ -189,7 +132,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 +141,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 +151,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@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
||||
with:
|
||||
version: "0.11.x"
|
||||
enable-cache: true
|
||||
@@ -226,8 +169,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 +178,4 @@ jobs:
|
||||
uv run python scripts/trigger_pr_review.py \
|
||||
--pr-url "$PR_URL" \
|
||||
--github-username "$GITHUB_ACTOR" \
|
||||
--review-compare \
|
||||
--no-require-comment-selection
|
||||
|
||||
@@ -37,11 +37,11 @@ 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: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
|
||||
id: filter
|
||||
with:
|
||||
@@ -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: |
|
||||
@@ -185,14 +185,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 +208,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 +242,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 +278,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 }}
|
||||
@@ -355,7 +334,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 +346,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 +364,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 +392,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 +440,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 }}
|
||||
# Microsoft 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 +574,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 +593,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: |
|
||||
@@ -558,7 +639,7 @@ jobs:
|
||||
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: |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -21,6 +21,8 @@ on:
|
||||
required: true
|
||||
AZUREAI__ENDPOINT:
|
||||
required: true
|
||||
COPILOT_GITHUB_TOKEN:
|
||||
required: true
|
||||
OPENAI__APIKEY:
|
||||
required: true
|
||||
|
||||
@@ -30,7 +32,6 @@ permissions:
|
||||
jobs:
|
||||
dotnet-integration-tests:
|
||||
permissions:
|
||||
copilot-requests: write
|
||||
contents: read
|
||||
id-token: write
|
||||
strategy:
|
||||
@@ -43,7 +44,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 +78,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 +103,7 @@ jobs:
|
||||
env:
|
||||
COSMOSDB_ENDPOINT: https://localhost:8081
|
||||
COSMOSDB_KEY: C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
|
||||
OpenAI__ApiKey: ${{ secrets.OPENAI__APIKEY }}
|
||||
OpenAI__ChatModelId: ${{ vars.OPENAI__CHATMODELID }}
|
||||
OpenAI__ChatReasoningModelId: ${{ vars.OPENAI__CHATREASONINGMODELID }}
|
||||
|
||||
@@ -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 }}
|
||||
@@ -126,7 +126,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: |
|
||||
|
||||
@@ -25,13 +25,13 @@ jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
node-version: "22"
|
||||
|
||||
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
python-changes: ${{ steps.detect-changes.outputs.python }}
|
||||
steps:
|
||||
- name: Check out trusted workflow helpers
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
ref: ${{ github.sha }}
|
||||
persist-credentials: false
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
|
||||
- name: Resolve and authorize checkout ref
|
||||
id: resolve
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
@@ -102,7 +102,6 @@ jobs:
|
||||
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
|
||||
@@ -113,6 +112,7 @@ jobs:
|
||||
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
|
||||
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
AZUREAI__ENDPOINT: ${{ secrets.AZUREAI__ENDPOINT }}
|
||||
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
|
||||
OPENAI__APIKEY: ${{ secrets.OPENAI__APIKEY }}
|
||||
|
||||
python-integration-tests:
|
||||
@@ -120,7 +120,6 @@ jobs:
|
||||
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
|
||||
@@ -131,5 +130,6 @@ jobs:
|
||||
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
|
||||
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
|
||||
FOUNDRY_MODELS_API_KEY: ${{ secrets.FOUNDRY_MODELS_API_KEY }}
|
||||
OPENAI__APIKEY: ${{ secrets.OPENAI__APIKEY }}
|
||||
|
||||
@@ -29,7 +29,6 @@ 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:
|
||||
@@ -68,7 +67,7 @@ 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
|
||||
@@ -94,7 +93,7 @@ jobs:
|
||||
- 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 }}
|
||||
@@ -126,7 +125,6 @@ jobs:
|
||||
}}
|
||||
environment: integration
|
||||
permissions:
|
||||
copilot-requests: write
|
||||
contents: read
|
||||
id-token: write
|
||||
issues: write
|
||||
@@ -135,7 +133,7 @@ jobs:
|
||||
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 +141,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 +151,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@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
||||
with:
|
||||
version: "0.11.x"
|
||||
enable-cache: true
|
||||
@@ -168,7 +166,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 +176,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 +201,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 }}
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
issues: write
|
||||
steps:
|
||||
- name: Checkout GitHub automation
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
sparse-checkout: |
|
||||
.github/actions/github-app-token
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
repository: ${{ github.repository }}
|
||||
fallback-token: ${{ secrets.GH_ACTIONS_PR_WRITE }}
|
||||
|
||||
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
with:
|
||||
github-token: ${{ steps.github-auth.outputs.token }}
|
||||
script: |
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
|
||||
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: |
|
||||
@@ -46,12 +46,12 @@ jobs:
|
||||
repository: ${{ github.repository }}
|
||||
fallback-token: ${{ secrets.GH_ACTIONS_PR_WRITE }}
|
||||
|
||||
- uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213 # v6.1.0
|
||||
- uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213 # v6
|
||||
with:
|
||||
repo-token: ${{ steps.github-auth.outputs.token }}
|
||||
|
||||
- name: "PR: add breaking change label from title"
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
with:
|
||||
github-token: ${{ steps.github-auth.outputs.token }}
|
||||
script: |
|
||||
|
||||
@@ -16,13 +16,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout scripts
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
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 }}
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
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: |
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
|
||||
- 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 }}
|
||||
@@ -82,7 +82,7 @@ jobs:
|
||||
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: |
|
||||
@@ -107,7 +107,7 @@ jobs:
|
||||
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 }}
|
||||
script: |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -31,7 +31,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,7 +42,7 @@ 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') }}
|
||||
@@ -68,7 +68,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 +97,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
|
||||
@@ -128,7 +128,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
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
# installability starts differing across supported Python versions.
|
||||
UV_PYTHON: "3.13"
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -78,7 +78,7 @@ jobs:
|
||||
|
||||
- name: Upload dependency validation reports
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: dependency-maintenance-results
|
||||
path: |
|
||||
@@ -88,7 +88,7 @@ jobs:
|
||||
|
||||
- name: Create issue for failed dependency bounds test
|
||||
if: steps.validate_bounds_test.outcome != 'success'
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
@@ -158,7 +158,7 @@ jobs:
|
||||
|
||||
- name: Create issues for failed dependency candidates
|
||||
if: always()
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
@@ -321,7 +321,7 @@ jobs:
|
||||
|
||||
- name: Create or update dependency maintenance tracking issue
|
||||
if: steps.commit_updates.outputs.has_changes == 'true'
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
|
||||
@@ -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@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
||||
with:
|
||||
version-file: "python/pyproject.toml"
|
||||
enable-cache: true
|
||||
|
||||
@@ -25,6 +25,8 @@ on:
|
||||
required: true
|
||||
AZURE_SUBSCRIPTION_ID:
|
||||
required: true
|
||||
COPILOT_GITHUB_TOKEN:
|
||||
required: true
|
||||
FOUNDRY_MODELS_API_KEY:
|
||||
required: false
|
||||
OPENAI__APIKEY:
|
||||
@@ -48,7 +50,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 +83,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 +104,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
|
||||
@@ -127,7 +129,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 +140,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 +158,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 +180,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 +194,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@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: ~/.ollama/models
|
||||
key: ollama-models-qwen2.5-1.5b-nomic-embed-text-v1
|
||||
@@ -232,12 +234,11 @@ 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
|
||||
@@ -248,7 +249,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,6 +275,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
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
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
|
||||
@@ -297,7 +365,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 +376,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 +393,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
|
||||
@@ -347,7 +415,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 +426,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 +443,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 +470,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 +495,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 +506,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 +534,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 +552,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 +563,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
|
||||
@@ -535,7 +601,7 @@ jobs:
|
||||
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 +617,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 +626,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!')
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
outputs:
|
||||
pythonChanges: ${{ steps.filter.outputs.python}}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
|
||||
id: filter
|
||||
with:
|
||||
@@ -63,7 +63,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
|
||||
|
||||
@@ -36,12 +36,13 @@ 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: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
|
||||
id: filter
|
||||
with:
|
||||
@@ -67,7 +68,6 @@ 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'
|
||||
@@ -76,6 +76,9 @@ jobs:
|
||||
- '.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 +110,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 +157,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 +190,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 +218,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 +227,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 +260,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 +288,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 +299,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@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: ~/.ollama/models
|
||||
key: ollama-models-qwen2.5-1.5b-nomic-embed-text-v1
|
||||
@@ -336,12 +339,11 @@ 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
|
||||
@@ -382,12 +384,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 +493,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 +502,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 +529,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 +554,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 +563,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 +590,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 +625,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 +657,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 +675,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 +709,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 +727,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,7 +738,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
|
||||
uses: ./.github/actions/python-setup
|
||||
with:
|
||||
@@ -695,7 +773,7 @@ jobs:
|
||||
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 +789,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 +799,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!')
|
||||
|
||||
@@ -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,97 +33,28 @@ 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
|
||||
with:
|
||||
|
||||
@@ -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,13 +685,12 @@ 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
|
||||
@@ -939,7 +725,7 @@ jobs:
|
||||
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 }}
|
||||
|
||||
@@ -25,7 +25,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
|
||||
@@ -45,7 +45,7 @@ jobs:
|
||||
- name: Check coverage threshold
|
||||
run: python ${{ github.workspace }}/.github/scripts/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
|
||||
|
||||
@@ -31,7 +31,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,7 +33,7 @@ jobs:
|
||||
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
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
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'
|
||||
|
||||
|
||||
+1
-20
@@ -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:
|
||||
|
||||
@@ -11,10 +11,7 @@
|
||||
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 1–2 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.
|
||||
@@ -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 Task–compatible host. Show host-agnostic patterns (plain orchestration functions, `IServiceCollection` registration) before Azure Functions–specific 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.
|
||||
@@ -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 Task–compatible 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.
|
||||
@@ -172,7 +172,7 @@ parsing a structured payload into a typed record), without coupling the holder t
|
||||
- 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.
|
||||
`UseClaimsBasedSessionIsolation(...)`), so the session namespace is scoped per principal.
|
||||
- Persist session/checkpoint state only after the run or stream has completed.
|
||||
|
||||
## E2E Code Samples
|
||||
|
||||
@@ -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 0–63, `high`
|
||||
for 64–127). 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
|
||||
@@ -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 0–31, orchestration patterns 32–47, 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` |
|
||||
| 19–31 | _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` |
|
||||
| 37–47 | _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 |
|
||||
| 89–127 | _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` |
|
||||
| 20–31 | _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` |
|
||||
| 37–47 | _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` |
|
||||
| 75–127 | _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.
|
||||
+3
-6
@@ -74,12 +74,9 @@ code before the user has reviewed the plan**:
|
||||
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.
|
||||
was addressed (or the agreed outcome) — leave none unanswered.
|
||||
6. **Resolve resolved threads.** Mark a review thread as resolved only when the
|
||||
comment has actually been addressed.
|
||||
|
||||
### Useful commands
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<!-- Aspire.* -->
|
||||
<PackageVersion Include="Anthropic" Version="12.42.0" />
|
||||
<PackageVersion Include="Anthropic" Version="12.35.1" />
|
||||
<PackageVersion Include="Anthropic.Foundry" Version="0.7.1" />
|
||||
<PackageVersion Include="Aspire.Hosting" Version="$(AspireAppHostSdkVersion)" />
|
||||
<PackageVersion Include="Aspire.Azure.AI.OpenAI" Version="13.0.0-preview.1.25560.3" />
|
||||
@@ -23,45 +23,44 @@
|
||||
<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.26" />
|
||||
<PackageVersion Include="Azure.AI.AgentServer.Invocations" Version="1.0.0-beta.5" />
|
||||
<PackageVersion Include="Azure.AI.AgentServer.Responses" Version="1.0.0-beta.6" />
|
||||
<PackageVersion Include="Azure.Search.Documents" Version="12.0.0" />
|
||||
<PackageVersion Include="Azure.AI.Projects" Version="2.1.0-beta.4" />
|
||||
<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.60.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.9" />
|
||||
<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.14.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.9" />
|
||||
<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" />
|
||||
<PackageVersion Include="System.Net.ServerSentEvents" Version="10.0.8" />
|
||||
<!-- 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="AGUI.Abstractions" Version="0.0.3" />
|
||||
<PackageVersion Include="AGUI.Formatting" Version="0.0.3" />
|
||||
<PackageVersion Include="AGUI.Protobuf" Version="0.0.3" />
|
||||
<PackageVersion Include="AGUI.Client" Version="0.0.3" />
|
||||
<PackageVersion Include="AGUI.Server" Version="0.0.3" />
|
||||
<PackageVersion Include="System.Text.Json" Version="10.0.9" />
|
||||
<PackageVersion Include="System.Threading.Channels" Version="10.0.9" />
|
||||
<PackageVersion Include="System.Threading.Tasks.Extensions" Version="4.6.3" />
|
||||
<PackageVersion Include="System.Net.Security" Version="4.3.2" />
|
||||
<!-- OpenTelemetry -->
|
||||
@@ -81,11 +80,11 @@
|
||||
<PackageVersion Include="Microsoft.OpenApi" Version="2.7.5" /> <!-- Pin patched OpenAPI.NET to remediate GHSA-v5pm-xwqc-g5wc -->
|
||||
<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" Version="10.7.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="10.7.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.Evaluation" Version="10.7.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.Evaluation.Quality" Version="10.7.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.Evaluation.Safety" Version="10.7.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="10.6.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Compliance.Abstractions" Version="10.5.0" />
|
||||
@@ -95,22 +94,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.9" />
|
||||
<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.9" />
|
||||
<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" />
|
||||
<!-- 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" />
|
||||
<!-- Agent SDKs -->
|
||||
<PackageVersion Include="GitHub.Copilot.SDK" Version="1.0.5" />
|
||||
<PackageVersion Include="ResponsibleAI.AgentHooks" Version="0.1.0-alpha.4" />
|
||||
<PackageVersion Include="Microsoft.Agents.CopilotStudio.Client" Version="1.3.171-beta" />
|
||||
<!-- M365 Agents SDK -->
|
||||
<PackageVersion Include="AdaptiveCards" Version="3.1.0" />
|
||||
@@ -120,8 +117,7 @@
|
||||
<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" />
|
||||
@@ -138,6 +134,22 @@
|
||||
<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" />
|
||||
<!-- 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" />
|
||||
<!-- Valkey -->
|
||||
<!-- Redis -->
|
||||
<PackageVersion Include="StackExchange.Redis" Version="2.10.1" />
|
||||
<!-- Valkey -->
|
||||
<PackageVersion Include="Valkey.Glide" Version="1.1.0" />
|
||||
<!-- Console UX -->
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
<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" />
|
||||
@@ -30,7 +31,6 @@
|
||||
<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" />
|
||||
@@ -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>
|
||||
@@ -110,10 +125,6 @@
|
||||
<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" />
|
||||
@@ -191,8 +202,6 @@
|
||||
<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" />
|
||||
@@ -332,9 +341,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>
|
||||
@@ -388,6 +394,29 @@
|
||||
<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" />
|
||||
@@ -597,6 +626,7 @@
|
||||
<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" />
|
||||
@@ -605,13 +635,12 @@
|
||||
<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/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" />
|
||||
@@ -635,9 +664,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" />
|
||||
@@ -650,22 +680,19 @@
|
||||
<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.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" />
|
||||
@@ -683,3 +710,5 @@
|
||||
<Project Path="tests/Microsoft.Agents.AI.Workflows.UnitTests/Microsoft.Agents.AI.Workflows.UnitTests.csproj" />
|
||||
</Folder>
|
||||
</Solution>
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"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.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,15 +14,15 @@
|
||||
"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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
When specified, only test projects whose filename matches this pattern are kept.
|
||||
|
||||
.PARAMETER TestProjectNameExcludeFilter
|
||||
Optional wildcard pattern(s) to exclude test projects by name (e.g., *Slow.IntegrationTests*).
|
||||
Optional wildcard pattern(s) to exclude test projects by name (e.g., *DurableTask.IntegrationTests*).
|
||||
When specified, test projects whose filename matches any of these patterns are removed.
|
||||
Applied after TestProjectNameIncludeFilter. Can be a single string or an array of strings.
|
||||
|
||||
@@ -50,8 +50,8 @@
|
||||
dotnet test --solution (./dotnet/eng/scripts/New-FilteredSolution.ps1 -Solution dotnet/agent-framework-dotnet.slnx -TargetFramework net472) --no-build -f net472
|
||||
|
||||
.EXAMPLE
|
||||
# Generate integration tests while excluding a long-running test project
|
||||
./dotnet/eng/scripts/New-FilteredSolution.ps1 -Solution dotnet/agent-framework-dotnet.slnx -TargetFramework net10.0 -TestProjectNameIncludeFilter "*IntegrationTests*" -TestProjectNameExcludeFilter "*Slow.IntegrationTests*" -OutputPath filtered-integration.slnx
|
||||
# Generate integration tests excluding DurableTask and AzureFunctions
|
||||
./dotnet/eng/scripts/New-FilteredSolution.ps1 -Solution dotnet/agent-framework-dotnet.slnx -TargetFramework net10.0 -TestProjectNameIncludeFilter "*IntegrationTests*" -TestProjectNameExcludeFilter "*DurableTask.IntegrationTests*","*AzureFunctions.IntegrationTests*" -OutputPath filtered-other-integration.slnx
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
|
||||
@@ -329,80 +329,6 @@ internal static class AgentsSamples
|
||||
],
|
||||
},
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Agent_Step20_DynamicFunctionTools",
|
||||
ProjectPath = "samples/02-agents/Agents/Agent_Step20_DynamicFunctionTools",
|
||||
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
|
||||
MustContain =
|
||||
[
|
||||
"=== Dynamic Function Tools Sample ===",
|
||||
"=== Non-Streaming Mode ===",
|
||||
"=== Streaming Mode ===",
|
||||
"[User]",
|
||||
"[Agent]",
|
||||
],
|
||||
ExpectedOutputDescription =
|
||||
[
|
||||
"The output should show the agent starting with only a RequestTools function and dynamically loading additional tools (weather, time, temperature) as needed.",
|
||||
"The output should contain weather information for Seattle and London, the current time in New York, and a Fahrenheit-to-Celsius temperature conversion.",
|
||||
"The output should demonstrate both non-streaming and streaming modes.",
|
||||
"The output should not contain error messages or stack traces.",
|
||||
],
|
||||
},
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Agent_Step21_ShellWithEnvironment",
|
||||
ProjectPath = "samples/02-agents/Agents/Agent_Step21_ShellWithEnvironment",
|
||||
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
|
||||
MustContain =
|
||||
[
|
||||
"### Stateless mode",
|
||||
"### Persistent mode",
|
||||
"--- Captured environment snapshot ---",
|
||||
],
|
||||
ExpectedOutputDescription =
|
||||
[
|
||||
"The output should show an agent using a shell tool to print the current working directory.",
|
||||
"The output should demonstrate that in stateless mode side effects (such as changing directory) do not carry between calls, while in persistent mode the working directory and an environment variable (DEMO_TOKEN set to 'hello-world') carry across calls.",
|
||||
"The output should include a captured environment snapshot describing the OS, shell, and working directory.",
|
||||
"The output should not contain error messages or stack traces.",
|
||||
],
|
||||
},
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Agent_Step22_AgentMode",
|
||||
ProjectPath = "samples/02-agents/Agents/Agent_Step22_AgentMode",
|
||||
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
|
||||
SkipReason = "Interactive sample that reads console input in a loop and does not exit on its own.",
|
||||
},
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Agent_Step23_TodoList",
|
||||
ProjectPath = "samples/02-agents/Agents/Agent_Step23_TodoList",
|
||||
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
|
||||
MustContain =
|
||||
[
|
||||
"User:",
|
||||
"Agent:",
|
||||
"--- Current todo list ---",
|
||||
],
|
||||
ExpectedOutputDescription =
|
||||
[
|
||||
"The output should show an agent planning a team offsite by breaking the work into a todo list.",
|
||||
"The output should show the todo list being updated as progress is reported (for example marking items complete after the venue is booked and invites are sent) and adjusted when the plan changes to skip catering and add a group hike.",
|
||||
"The current todo list should be printed after each turn, showing item status.",
|
||||
"The output should not contain error messages or stack traces.",
|
||||
],
|
||||
},
|
||||
|
||||
// ── AgentSkills ─────────────────────────────────────────────────────
|
||||
|
||||
new SampleDefinition
|
||||
@@ -510,48 +436,6 @@ internal static class AgentsSamples
|
||||
SkipReason = "Requires a running Neo4j instance; standalone sample outside the repo's CPM build.",
|
||||
},
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "AgentWithMemory_Step07_FileMemoryProvider",
|
||||
ProjectPath = "samples/02-agents/AgentWithMemory/AgentWithMemory_Step07_FileMemoryProvider",
|
||||
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
|
||||
MustContain =
|
||||
[
|
||||
"Memory files will be written to:",
|
||||
"=== First conversation ===",
|
||||
"=== Memory files on disk ===",
|
||||
"=== Second conversation (new session) ===",
|
||||
],
|
||||
ExpectedOutputDescription =
|
||||
[
|
||||
"The output should acknowledge that the user is vegetarian and travels with a dog, indicating the agent stored these preferences.",
|
||||
"The memory files section should list at least one memory file written by the agent, such as a file about the user's preferences.",
|
||||
"The second conversation should recommend a hotel and a restaurant in Paris that are consistent with the remembered preferences, for example a pet-friendly hotel and a restaurant with vegetarian options, even though it is a new session.",
|
||||
"The output should not contain error messages or stack traces.",
|
||||
],
|
||||
},
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "AgentWithMemory_Step08_MemoryUsingCosmosNoSql",
|
||||
ProjectPath = "samples/02-agents/AgentWithMemory/AgentWithMemory_Step08_MemoryUsingCosmosNoSql",
|
||||
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT", "COSMOS_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["FOUNDRY_MODEL", "FOUNDRY_EMBEDDING_MODEL", "COSMOS_DATABASE_NAME"],
|
||||
MustContain =
|
||||
[
|
||||
"First session:",
|
||||
"Second session (recalling prior chat history from Cosmos DB):",
|
||||
],
|
||||
ExpectedOutputDescription =
|
||||
[
|
||||
"The output should contain two joke responses.",
|
||||
"The first joke should be about a pirate (as explicitly requested).",
|
||||
"The second joke should also be pirate-themed or similar to what the user likes, since chat history from the first session should be recalled from Cosmos DB.",
|
||||
"The output should not contain error messages or stack traces.",
|
||||
],
|
||||
},
|
||||
|
||||
// ── AgentWithRAG ────────────────────────────────────────────────────
|
||||
|
||||
new SampleDefinition
|
||||
@@ -878,19 +762,6 @@ internal static class AgentsSamples
|
||||
],
|
||||
},
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Agent_With_GitHubCopilot_BYOK",
|
||||
ProjectPath = "samples/02-agents/AgentProviders/github-copilot/Agent_With_GitHubCopilot_BYOK",
|
||||
RequiredEnvironmentVariables = ["BYOK_BASE_URL", "BYOK_API_KEY"],
|
||||
OptionalEnvironmentVariables = ["BYOK_PROVIDER_TYPE", "BYOK_MODEL_ID"],
|
||||
ExpectedOutputDescription =
|
||||
[
|
||||
"The output should contain a user prompt and a response about the benefits of BYOK.",
|
||||
"The output should not contain error messages or stack traces.",
|
||||
],
|
||||
},
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "Agent_With_GoogleGemini",
|
||||
@@ -1303,7 +1174,6 @@ internal static class AgentsSamples
|
||||
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
|
||||
MustContain =
|
||||
[
|
||||
"MCP 2026-07-28 Tasks extension enabled.",
|
||||
"=== Transparent long-running MCP task (RunAsync) ===",
|
||||
"=== Transparent long-running MCP task (RunStreamingAsync) ===",
|
||||
],
|
||||
|
||||
@@ -92,5 +92,14 @@ internal static class GetStartedSamples
|
||||
"The output should not contain error messages or stack traces.",
|
||||
],
|
||||
},
|
||||
|
||||
new SampleDefinition
|
||||
{
|
||||
Name = "06_host_your_agent",
|
||||
ProjectPath = "samples/01-get-started/06_host_your_agent",
|
||||
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
|
||||
SkipReason = "Requires Azure Functions Core Tools runtime and starts a web server.",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"sdk": {
|
||||
"version": "10.0.303",
|
||||
"version": "10.0.301",
|
||||
"rollForward": "minor",
|
||||
"allowPrerelease": false
|
||||
},
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<clear />
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||
</packageSources>
|
||||
<packageSourceMapping>
|
||||
<packageSource key="nuget.org">
|
||||
<package pattern="*" />
|
||||
</packageSource>
|
||||
</packageSourceMapping>
|
||||
</configuration>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<!-- Central version prefix - applies to all nuget packages. -->
|
||||
<VersionPrefix>1.18.0</VersionPrefix>
|
||||
<VersionPrefix>1.15.0</VersionPrefix>
|
||||
<RCNumber>1</RCNumber>
|
||||
<DateSuffix>260818</DateSuffix>
|
||||
<DateSuffix>260722</DateSuffix>
|
||||
<PackageVersion Condition="'$(IsReleaseCandidate)' == 'true'">$(VersionPrefix)-rc$(RCNumber)</PackageVersion>
|
||||
<PackageVersion Condition="'$(IsReleaseCandidate)' != 'true' AND '$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).$(DateSuffix).1</PackageVersion>
|
||||
<PackageVersion Condition="'$(IsReleaseCandidate)' != 'true' AND '$(VersionSuffix)' == ''">$(VersionPrefix)-preview.$(DateSuffix).1</PackageVersion>
|
||||
<PackageVersion Condition="'$(IsReleased)' == 'true'">$(VersionPrefix)</PackageVersion>
|
||||
<GitTag>1.18.0</GitTag>
|
||||
<GitTag>1.15.0</GitTag>
|
||||
|
||||
<Configurations>Debug;Release;Publish</Configurations>
|
||||
<IsPackable>true</IsPackable>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<!-- The Functions build tools don't like namespaces that start with a number -->
|
||||
<AssemblyName>HostedAgent</AssemblyName>
|
||||
<RootNamespace>HostedAgent</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
<!-- Azure Functions packages -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample shows how to host an AI agent with Azure Functions (DurableAgents).
|
||||
//
|
||||
// Prerequisites:
|
||||
// - Azure Functions Core Tools
|
||||
// - Foundry project endpoint and credentials
|
||||
//
|
||||
// Environment variables:
|
||||
// FOUNDRY_PROJECT_ENDPOINT
|
||||
// FOUNDRY_MODEL (defaults to "gpt-5.4-mini")
|
||||
//
|
||||
// Run with: func start
|
||||
// Then call: POST http://localhost:7071/api/agents/HostedAgent/run
|
||||
|
||||
using Azure.AI.Projects;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.Hosting.AzureFunctions;
|
||||
using Microsoft.Azure.Functions.Worker.Builder;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
|
||||
?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
|
||||
var model = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
|
||||
|
||||
// Set up an AI agent following the standard Microsoft Agent Framework pattern.
|
||||
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
||||
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
AIAgent agent = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential())
|
||||
.AsAIAgent(model: model, instructions: "You are a helpful assistant hosted in Azure Functions.", name: "HostedAgent");
|
||||
|
||||
// Configure the function app to host the AI agent.
|
||||
// This will automatically generate HTTP API endpoints for the agent.
|
||||
using IHost app = FunctionsApplication
|
||||
.CreateBuilder(args)
|
||||
.ConfigureFunctionsWebApplication()
|
||||
.ConfigureDurableAgents(options => options.AddAIAgent(agent, timeToLive: TimeSpan.FromHours(1)))
|
||||
.Build();
|
||||
app.Run();
|
||||
@@ -1,3 +0,0 @@
|
||||
# Azure Functions Hosting Sample Has Moved
|
||||
|
||||
The Azure Functions hosting tutorial is now maintained as the [single-agent Durable Agent sample](https://github.com/microsoft/agent-framework-durable-extension/tree/main/dotnet/samples/DurableAgents/AzureFunctions/01_SingleAgent).
|
||||
@@ -51,7 +51,7 @@ dotnet run --urls http://localhost:8888
|
||||
An interactive console client that connects to an AG-UI server. Demonstrates:
|
||||
|
||||
- Creating an AG-UI client with `AGUIChatClient`
|
||||
- Managing multi-turn conversations with an `AgentSession`
|
||||
- Managing conversation threads
|
||||
- Streaming responses with `RunStreamingAsync`
|
||||
- Displaying colored console output for different content types
|
||||
- Supporting both interactive and automated modes
|
||||
@@ -133,24 +133,28 @@ Demonstrates human-in-the-loop approval workflows for sensitive operations. This
|
||||
|
||||
An AG-UI server that implements approval workflows. Demonstrates:
|
||||
|
||||
- Wrapping a tool with `ApprovalRequiredAIFunction` so it requires approval before running
|
||||
- Mapping a plain agent with `MapAGUIServer`, which natively emits an approval interrupt when the model calls the approval-required tool and resumes the run once the client sends the decision back
|
||||
- Wrapping tools with `ApprovalRequiredAIFunction`
|
||||
- Converting `FunctionApprovalRequestContent` to approval requests
|
||||
- Middleware pattern with `ServerFunctionApprovalServerAgent`
|
||||
- Complete function call capture and restoration
|
||||
|
||||
**Run the server:**
|
||||
|
||||
```bash
|
||||
cd Step04_HumanInLoop/Server
|
||||
dotnet run --urls http://localhost:5100
|
||||
dotnet run --urls http://localhost:8888
|
||||
```
|
||||
|
||||
#### Client (`Step04_HumanInLoop/Client`)
|
||||
|
||||
An interactive client that handles approval requests from the server. Demonstrates:
|
||||
|
||||
- Detecting `ToolApprovalRequestContent` in the streamed response
|
||||
- Displaying approval details to the user and prompting for approval or rejection
|
||||
- Sending the decision back as a `ToolApprovalResponseContent` created with `approvalRequest.CreateResponse(approved)`
|
||||
- Resuming the run so the server continues after the decision is received
|
||||
- Using `ServerFunctionApprovalClientAgent` middleware
|
||||
- Detecting `FunctionApprovalRequestContent`
|
||||
- Displaying approval details to users
|
||||
- Prompting for approval/rejection
|
||||
- Sending approval responses with `FunctionApprovalResponseContent`
|
||||
- Resuming conversation after approval
|
||||
|
||||
**Run the client:**
|
||||
|
||||
@@ -163,15 +167,15 @@ Try asking the agent to perform sensitive operations like "Approve expense repor
|
||||
|
||||
### Step05_StateManagement
|
||||
|
||||
An AG-UI server and client that demonstrate shared state management.
|
||||
An AG-UI server and client that demonstrate state management with predictive updates.
|
||||
|
||||
#### Server (`Step05_StateManagement/Server`)
|
||||
|
||||
Demonstrates:
|
||||
|
||||
- Exposing a `generate_recipe` tool that returns the complete recipe
|
||||
- Mapping the tool result to a `STATE_SNAPSHOT` event with `AGUIStreamOptions.MapResultAsStateSnapshot`
|
||||
- Reading the client's current recipe from `RunAgentInput.State`
|
||||
- Defining state schemas using C# records
|
||||
- Using `SharedStateAgent` middleware for state management
|
||||
- Streaming predictive state updates with `AgentState` content
|
||||
- Managing shared state between client and server
|
||||
- Using JSON serialization contexts for state types
|
||||
|
||||
@@ -206,7 +210,7 @@ dotnet run
|
||||
|
||||
### Client-Side
|
||||
|
||||
1. `AGUIChatClient` sends HTTP POST request to server
|
||||
1. `AGUIAgent` sends HTTP POST request to server
|
||||
2. Server responds with SSE stream
|
||||
3. Client parses events into `AgentResponseUpdate` objects
|
||||
4. Updates are displayed based on content type
|
||||
@@ -224,7 +228,7 @@ dotnet run
|
||||
|
||||
`ConversationId` keeps request/response continuity. It is not proof that the caller owns that conversation. In multi-user deployments, authenticate each AG-UI request and authorize conversation access using your application's real boundary, such as the authenticated user, tenant, or workspace.
|
||||
|
||||
If your ASP.NET Core host shares session storage across users, pair `MapAGUIServer` with an isolation strategy such as `UseClaimsBasedAgentIsolation(...)` so the storage key includes a principal-specific dimension instead of relying on the conversation identifier alone.
|
||||
If your ASP.NET Core host shares session storage across users, pair `MapAGUI` with an isolation strategy such as `UseClaimsBasedSessionIsolation(...)` so the storage key includes a principal-specific dimension instead of relying on the conversation identifier alone.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using AGUI.Abstractions;
|
||||
using AGUI.Client;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
@@ -49,6 +50,7 @@ try
|
||||
|
||||
// Stream the response
|
||||
bool isFirstUpdate = true;
|
||||
string? threadId = null;
|
||||
|
||||
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(messages, session))
|
||||
{
|
||||
@@ -57,8 +59,11 @@ try
|
||||
// First update indicates run started
|
||||
if (isFirstUpdate)
|
||||
{
|
||||
// AGUIChatClient is stateless and never surfaces a ConversationId; the thread
|
||||
// id is carried on the AG-UI RUN_STARTED event's raw representation.
|
||||
threadId = (chatUpdate.RawRepresentation as RunStartedEvent)?.ThreadId;
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.WriteLine($"\n[Run Started - Run: {chatUpdate.ResponseId}]");
|
||||
Console.WriteLine($"\n[Run Started - Thread: {threadId}, Run: {chatUpdate.ResponseId}]");
|
||||
Console.ResetColor();
|
||||
isFirstUpdate = false;
|
||||
}
|
||||
@@ -81,11 +86,8 @@ try
|
||||
}
|
||||
}
|
||||
|
||||
// The session owns prior history, so the next run sends only the new user message.
|
||||
messages.Clear();
|
||||
|
||||
Console.ForegroundColor = ConsoleColor.Green;
|
||||
Console.WriteLine("\n[Run Finished]");
|
||||
Console.WriteLine($"\n[Run Finished - Thread: {threadId}]");
|
||||
Console.ResetColor();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,13 @@ using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore;
|
||||
using OpenAI.Chat;
|
||||
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
|
||||
builder.Services.AddHttpClient().AddLogging();
|
||||
builder.Services.AddAGUIServer();
|
||||
|
||||
// WARNING: When adding session persistence (e.g., WithInMemorySessionStore), or running in production,
|
||||
// make sure to also register an AgentIsolationKeyProvider to scope sessions by principal in multi-user
|
||||
// make sure to also register a SessionIsolationKeyProvider to scope sessions by principal in multi-user
|
||||
// deployments, e.g.:
|
||||
// builder.Services.UseClaimsBasedAgentIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });
|
||||
// builder.Services.UseClaimsBasedSessionIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });
|
||||
|
||||
WebApplication app = builder.Build();
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using AGUI.Abstractions;
|
||||
using AGUI.Client;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
@@ -49,6 +50,7 @@ try
|
||||
|
||||
// Stream the response
|
||||
bool isFirstUpdate = true;
|
||||
string? threadId = null;
|
||||
|
||||
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(messages, session))
|
||||
{
|
||||
@@ -57,8 +59,11 @@ try
|
||||
// First update indicates run started
|
||||
if (isFirstUpdate)
|
||||
{
|
||||
// AGUIChatClient is stateless and never surfaces a ConversationId; the thread
|
||||
// id is carried on the AG-UI RUN_STARTED event's raw representation.
|
||||
threadId = (chatUpdate.RawRepresentation as RunStartedEvent)?.ThreadId;
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.WriteLine($"\n[Run Started - Run: {chatUpdate.ResponseId}]");
|
||||
Console.WriteLine($"\n[Run Started - Thread: {threadId}, Run: {chatUpdate.ResponseId}]");
|
||||
Console.ResetColor();
|
||||
isFirstUpdate = false;
|
||||
}
|
||||
@@ -113,11 +118,8 @@ try
|
||||
}
|
||||
}
|
||||
|
||||
// The session owns prior history, so the next run sends only the new user message.
|
||||
messages.Clear();
|
||||
|
||||
Console.ForegroundColor = ConsoleColor.Green;
|
||||
Console.WriteLine("\n[Run Finished]");
|
||||
Console.WriteLine($"\n[Run Finished - Thread: {threadId}]");
|
||||
Console.ResetColor();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,14 +11,15 @@ using Microsoft.Extensions.Options;
|
||||
using OpenAI.Chat;
|
||||
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
|
||||
builder.Services.AddHttpClient().AddLogging();
|
||||
builder.Services.ConfigureHttpJsonOptions(options =>
|
||||
options.SerializerOptions.TypeInfoResolverChain.Add(SampleJsonSerializerContext.Default));
|
||||
builder.Services.AddAGUIServer();
|
||||
|
||||
// WARNING: When adding session persistence (e.g., WithInMemorySessionStore), or running in production,
|
||||
// make sure to also register an AgentIsolationKeyProvider to scope sessions by principal in multi-user
|
||||
// make sure to also register a SessionIsolationKeyProvider to scope sessions by principal in multi-user
|
||||
// deployments, e.g.:
|
||||
// builder.Services.UseClaimsBasedAgentIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });
|
||||
// builder.Services.UseClaimsBasedSessionIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });
|
||||
|
||||
WebApplication app = builder.Build();
|
||||
|
||||
@@ -74,7 +75,6 @@ AITool[] tools =
|
||||
[
|
||||
AIFunctionFactory.Create(
|
||||
SearchRestaurants,
|
||||
name: "search_restaurants",
|
||||
serializerOptions: jsonOptions.SerializerOptions)
|
||||
];
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.ComponentModel;
|
||||
using AGUI.Abstractions;
|
||||
using AGUI.Client;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
@@ -18,7 +19,7 @@ static string GetUserLocation()
|
||||
}
|
||||
|
||||
// Create frontend tools
|
||||
AITool[] frontendTools = [AIFunctionFactory.Create(GetUserLocation, name: "get_user_location")];
|
||||
AITool[] frontendTools = [AIFunctionFactory.Create(GetUserLocation)];
|
||||
|
||||
// Create the AG-UI client agent with tools
|
||||
using HttpClient httpClient = new()
|
||||
@@ -62,6 +63,7 @@ try
|
||||
|
||||
// Stream the response
|
||||
bool isFirstUpdate = true;
|
||||
string? threadId = null;
|
||||
|
||||
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(messages, session))
|
||||
{
|
||||
@@ -70,8 +72,11 @@ try
|
||||
// First update indicates run started
|
||||
if (isFirstUpdate)
|
||||
{
|
||||
// AGUIChatClient is stateless and never surfaces a ConversationId; the thread
|
||||
// id is carried on the AG-UI RUN_STARTED event's raw representation.
|
||||
threadId = (chatUpdate.RawRepresentation as RunStartedEvent)?.ThreadId;
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.WriteLine($"\n[Run Started - Run: {chatUpdate.ResponseId}]");
|
||||
Console.WriteLine($"\n[Run Started - Thread: {threadId}, Run: {chatUpdate.ResponseId}]");
|
||||
Console.ResetColor();
|
||||
isFirstUpdate = false;
|
||||
}
|
||||
@@ -106,11 +111,8 @@ try
|
||||
}
|
||||
}
|
||||
|
||||
// The session owns prior history, so the next run sends only the new user message.
|
||||
messages.Clear();
|
||||
|
||||
Console.ForegroundColor = ConsoleColor.Green;
|
||||
Console.WriteLine("\n[Run Finished]");
|
||||
Console.WriteLine($"\n[Run Finished - Thread: {threadId}]");
|
||||
Console.ResetColor();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,13 @@ using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore;
|
||||
using OpenAI.Chat;
|
||||
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
|
||||
builder.Services.AddHttpClient().AddLogging();
|
||||
builder.Services.AddAGUIServer();
|
||||
|
||||
// WARNING: When adding session persistence (e.g., WithInMemorySessionStore), or running in production,
|
||||
// make sure to also register an AgentIsolationKeyProvider to scope sessions by principal in multi-user
|
||||
// make sure to also register a SessionIsolationKeyProvider to scope sessions by principal in multi-user
|
||||
// deployments, e.g.:
|
||||
// builder.Services.UseClaimsBasedAgentIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });
|
||||
// builder.Services.UseClaimsBasedSessionIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });
|
||||
|
||||
WebApplication app = builder.Build();
|
||||
|
||||
|
||||
@@ -15,12 +15,17 @@ using HttpClient httpClient = new()
|
||||
|
||||
AGUIChatClient chatClient = new(new(httpClient, serverUrl));
|
||||
|
||||
// Create agent. No custom approval agent is required: the loop below handles the approval interrupt
|
||||
// directly, and AGUIChatClient transports the decision back to the server via the AG-UI resume mechanism.
|
||||
AIAgent agent = chatClient.AsAIAgent(
|
||||
// Create agent
|
||||
ChatClientAgent baseAgent = chatClient.AsAIAgent(
|
||||
name: "AGUIAssistant",
|
||||
instructions: "You are a helpful assistant.");
|
||||
|
||||
// Use default JSON serializer options
|
||||
JsonSerializerOptions jsonSerializerOptions = JsonSerializerOptions.Default;
|
||||
|
||||
// Wrap the agent with ServerFunctionApprovalClientAgent
|
||||
ServerFunctionApprovalClientAgent agent = new(baseAgent, jsonSerializerOptions);
|
||||
|
||||
List<ChatMessage> messages = [];
|
||||
AgentSession? session = null;
|
||||
|
||||
@@ -39,6 +44,7 @@ while ((input = Console.ReadLine()) != null && !input.Equals("exit", StringCompa
|
||||
messages.Add(new ChatMessage(ChatRole.User, input));
|
||||
Console.WriteLine();
|
||||
|
||||
#pragma warning disable MEAI001
|
||||
List<AIContent> approvalResponses = [];
|
||||
|
||||
do
|
||||
@@ -62,6 +68,15 @@ while ((input = Console.ReadLine()) != null && !input.Equals("exit", StringCompa
|
||||
|
||||
ToolApprovalResponseContent approvalResponse = approvalRequest.CreateResponse(approved);
|
||||
|
||||
if (approvalRequest.AdditionalProperties != null)
|
||||
{
|
||||
approvalResponse.AdditionalProperties = [];
|
||||
foreach (var kvp in approvalRequest.AdditionalProperties)
|
||||
{
|
||||
approvalResponse.AdditionalProperties[kvp.Key] = kvp.Value;
|
||||
}
|
||||
}
|
||||
|
||||
approvalResponses.Add(approvalResponse);
|
||||
break;
|
||||
|
||||
@@ -100,10 +115,11 @@ while ((input = Console.ReadLine()) != null && !input.Equals("exit", StringCompa
|
||||
messages.AddRange(response.Messages);
|
||||
foreach (AIContent approvalResponse in approvalResponses)
|
||||
{
|
||||
messages.Add(new ChatMessage(ChatRole.User, [approvalResponse]));
|
||||
messages.Add(new ChatMessage(ChatRole.Tool, [approvalResponse]));
|
||||
}
|
||||
}
|
||||
while (approvalResponses.Count > 0);
|
||||
#pragma warning restore MEAI001
|
||||
|
||||
Console.WriteLine("\n");
|
||||
Console.ForegroundColor = ConsoleColor.White;
|
||||
@@ -111,6 +127,7 @@ while ((input = Console.ReadLine()) != null && !input.Equals("exit", StringCompa
|
||||
Console.ResetColor();
|
||||
}
|
||||
|
||||
#pragma warning disable MEAI001
|
||||
static void DisplayApprovalRequest(ToolApprovalRequestContent approvalRequest, FunctionCallContent fcc)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
@@ -132,3 +149,4 @@ static void DisplayApprovalRequest(ToolApprovalRequestContent approvalRequest, F
|
||||
Console.WriteLine("============================================================");
|
||||
Console.ResetColor();
|
||||
}
|
||||
#pragma warning restore MEAI001
|
||||
|
||||
+265
@@ -0,0 +1,265 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
using ServerFunctionApproval;
|
||||
|
||||
/// <summary>
|
||||
/// A delegating agent that handles server function approval requests and responses.
|
||||
/// Transforms between ToolApprovalRequestContent/ToolApprovalResponseContent
|
||||
/// and the server's request_approval tool call pattern.
|
||||
/// </summary>
|
||||
internal sealed class ServerFunctionApprovalClientAgent : DelegatingAIAgent
|
||||
{
|
||||
private readonly JsonSerializerOptions _jsonSerializerOptions;
|
||||
|
||||
public ServerFunctionApprovalClientAgent(AIAgent innerAgent, JsonSerializerOptions jsonSerializerOptions)
|
||||
: base(innerAgent)
|
||||
{
|
||||
this._jsonSerializerOptions = jsonSerializerOptions;
|
||||
}
|
||||
|
||||
protected override Task<AgentResponse> RunCoreAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentSession? session = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return this.RunCoreStreamingAsync(messages, session, options, cancellationToken)
|
||||
.ToAgentResponseAsync(cancellationToken);
|
||||
}
|
||||
|
||||
protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentSession? session = null,
|
||||
AgentRunOptions? options = null,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Process and transform approval messages, creating a new message list
|
||||
var processedMessages = ProcessOutgoingServerFunctionApprovals(messages.ToList(), this._jsonSerializerOptions);
|
||||
|
||||
// Run the inner agent and intercept any approval requests
|
||||
await foreach (var update in this.InnerAgent.RunStreamingAsync(
|
||||
processedMessages, session, options, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
yield return ProcessIncomingServerApprovalRequests(update, this._jsonSerializerOptions);
|
||||
}
|
||||
}
|
||||
|
||||
#pragma warning disable MEAI001 // Type is for evaluation purposes only
|
||||
private static FunctionResultContent ConvertApprovalResponseToToolResult(ToolApprovalResponseContent approvalResponse, JsonSerializerOptions jsonOptions)
|
||||
{
|
||||
return new FunctionResultContent(
|
||||
callId: approvalResponse.RequestId,
|
||||
result: JsonSerializer.SerializeToElement(
|
||||
new ApprovalResponse
|
||||
{
|
||||
ApprovalId = approvalResponse.RequestId,
|
||||
Approved = approvalResponse.Approved
|
||||
},
|
||||
jsonOptions));
|
||||
}
|
||||
|
||||
private static List<ChatMessage> CopyMessagesUpToIndex(List<ChatMessage> messages, int index)
|
||||
{
|
||||
var result = new List<ChatMessage>(index);
|
||||
for (int i = 0; i < index; i++)
|
||||
{
|
||||
result.Add(messages[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static List<AIContent> CopyContentsUpToIndex(IList<AIContent> contents, int index)
|
||||
{
|
||||
var result = new List<AIContent>(index);
|
||||
for (int i = 0; i < index; i++)
|
||||
{
|
||||
result.Add(contents[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static List<ChatMessage> ProcessOutgoingServerFunctionApprovals(
|
||||
List<ChatMessage> messages,
|
||||
JsonSerializerOptions jsonSerializerOptions)
|
||||
{
|
||||
List<ChatMessage>? result = null;
|
||||
|
||||
Dictionary<string, ToolApprovalRequestContent> approvalRequests = [];
|
||||
for (var messageIndex = 0; messageIndex < messages.Count; messageIndex++)
|
||||
{
|
||||
var message = messages[messageIndex];
|
||||
List<AIContent>? transformedContents = null;
|
||||
|
||||
// Process each content item in the message
|
||||
HashSet<string> approvalCalls = [];
|
||||
for (var contentIndex = 0; contentIndex < message.Contents.Count; contentIndex++)
|
||||
{
|
||||
var content = message.Contents[contentIndex];
|
||||
|
||||
// Handle pending approval requests (transform to tool call)
|
||||
if (content is ToolApprovalRequestContent approvalRequest &&
|
||||
approvalRequest.AdditionalProperties?.TryGetValue("original_function", out var originalFunction) == true &&
|
||||
originalFunction is FunctionCallContent original)
|
||||
{
|
||||
approvalRequests[approvalRequest.RequestId] = approvalRequest;
|
||||
transformedContents ??= CopyContentsUpToIndex(message.Contents, contentIndex);
|
||||
transformedContents.Add(original);
|
||||
}
|
||||
// Handle pending approval responses (transform to tool result)
|
||||
else if (content is ToolApprovalResponseContent approvalResponse &&
|
||||
approvalRequests.TryGetValue(approvalResponse.RequestId, out var correspondingRequest))
|
||||
{
|
||||
transformedContents ??= CopyContentsUpToIndex(message.Contents, contentIndex);
|
||||
transformedContents.Add(ConvertApprovalResponseToToolResult(approvalResponse, jsonSerializerOptions));
|
||||
approvalRequests.Remove(approvalResponse.RequestId);
|
||||
correspondingRequest.AdditionalProperties?.Remove("original_function");
|
||||
}
|
||||
// Skip historical approval content
|
||||
else if (content is FunctionCallContent { Name: "request_approval" } approvalCall)
|
||||
{
|
||||
transformedContents ??= CopyContentsUpToIndex(message.Contents, contentIndex);
|
||||
approvalCalls.Add(approvalCall.CallId);
|
||||
}
|
||||
else if (content is FunctionResultContent functionResult &&
|
||||
approvalCalls.Contains(functionResult.CallId))
|
||||
{
|
||||
transformedContents ??= CopyContentsUpToIndex(message.Contents, contentIndex);
|
||||
approvalCalls.Remove(functionResult.CallId);
|
||||
}
|
||||
else
|
||||
{
|
||||
transformedContents?.Add(content);
|
||||
}
|
||||
}
|
||||
|
||||
if (transformedContents?.Count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
else if (transformedContents != null)
|
||||
{
|
||||
// We made changes to contents, so use transformedContents
|
||||
var newMessage = new ChatMessage(message.Role, transformedContents)
|
||||
{
|
||||
AuthorName = message.AuthorName,
|
||||
MessageId = message.MessageId,
|
||||
CreatedAt = message.CreatedAt,
|
||||
RawRepresentation = message.RawRepresentation,
|
||||
AdditionalProperties = message.AdditionalProperties
|
||||
};
|
||||
result ??= CopyMessagesUpToIndex(messages, messageIndex);
|
||||
result.Add(newMessage);
|
||||
}
|
||||
else
|
||||
{
|
||||
// We're already copying messages, so copy this unchanged message too
|
||||
result?.Add(message);
|
||||
}
|
||||
// If result is null, we haven't made any changes yet, so keep processing
|
||||
}
|
||||
|
||||
return result ?? messages;
|
||||
}
|
||||
|
||||
private static AgentResponseUpdate ProcessIncomingServerApprovalRequests(
|
||||
AgentResponseUpdate update,
|
||||
JsonSerializerOptions jsonSerializerOptions)
|
||||
{
|
||||
IList<AIContent>? updatedContents = null;
|
||||
for (var i = 0; i < update.Contents.Count; i++)
|
||||
{
|
||||
var content = update.Contents[i];
|
||||
if (content is FunctionCallContent { Name: "request_approval" } request)
|
||||
{
|
||||
updatedContents ??= [.. update.Contents];
|
||||
|
||||
// Serialize the function arguments as JsonElement
|
||||
ApprovalRequest? approvalRequest;
|
||||
if (request.Arguments?.TryGetValue("request", out var reqObj) == true &&
|
||||
reqObj is JsonElement je)
|
||||
{
|
||||
approvalRequest = (ApprovalRequest?)je.Deserialize(jsonSerializerOptions.GetTypeInfo(typeof(ApprovalRequest)));
|
||||
}
|
||||
else
|
||||
{
|
||||
approvalRequest = null;
|
||||
}
|
||||
|
||||
if (approvalRequest == null)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to deserialize approval request.");
|
||||
}
|
||||
|
||||
var functionCallArgs = (Dictionary<string, object?>?)approvalRequest.FunctionArguments?
|
||||
.Deserialize(jsonSerializerOptions.GetTypeInfo(typeof(Dictionary<string, object?>)));
|
||||
|
||||
var approvalRequestContent = new ToolApprovalRequestContent(
|
||||
requestId: approvalRequest.ApprovalId,
|
||||
new FunctionCallContent(
|
||||
callId: approvalRequest.ApprovalId,
|
||||
name: approvalRequest.FunctionName,
|
||||
arguments: functionCallArgs));
|
||||
|
||||
approvalRequestContent.AdditionalProperties ??= [];
|
||||
approvalRequestContent.AdditionalProperties["original_function"] = content;
|
||||
|
||||
updatedContents[i] = approvalRequestContent;
|
||||
}
|
||||
}
|
||||
|
||||
if (updatedContents is not null)
|
||||
{
|
||||
var chatUpdate = update.AsChatResponseUpdate();
|
||||
return new AgentResponseUpdate(new ChatResponseUpdate()
|
||||
{
|
||||
Role = chatUpdate.Role,
|
||||
Contents = updatedContents,
|
||||
MessageId = chatUpdate.MessageId,
|
||||
AuthorName = chatUpdate.AuthorName,
|
||||
CreatedAt = chatUpdate.CreatedAt,
|
||||
RawRepresentation = chatUpdate.RawRepresentation,
|
||||
ResponseId = chatUpdate.ResponseId,
|
||||
AdditionalProperties = chatUpdate.AdditionalProperties
|
||||
})
|
||||
{
|
||||
AgentId = update.AgentId,
|
||||
ContinuationToken = update.ContinuationToken,
|
||||
};
|
||||
}
|
||||
|
||||
return update;
|
||||
}
|
||||
}
|
||||
#pragma warning restore MEAI001
|
||||
|
||||
namespace ServerFunctionApproval
|
||||
{
|
||||
public sealed class ApprovalRequest
|
||||
{
|
||||
[JsonPropertyName("approval_id")]
|
||||
public required string ApprovalId { get; init; }
|
||||
|
||||
[JsonPropertyName("function_name")]
|
||||
public required string FunctionName { get; init; }
|
||||
|
||||
[JsonPropertyName("function_arguments")]
|
||||
public JsonElement? FunctionArguments { get; init; }
|
||||
|
||||
[JsonPropertyName("message")]
|
||||
public string? Message { get; init; }
|
||||
}
|
||||
|
||||
public sealed class ApprovalResponse
|
||||
{
|
||||
[JsonPropertyName("approval_id")]
|
||||
public required string ApprovalId { get; init; }
|
||||
|
||||
[JsonPropertyName("approved")]
|
||||
public required bool Approved { get; init; }
|
||||
}
|
||||
}
|
||||
@@ -5,19 +5,37 @@ using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore;
|
||||
using Microsoft.AspNetCore.Http.Json;
|
||||
using Microsoft.AspNetCore.HttpLogging;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.Options;
|
||||
using OpenAI.Chat;
|
||||
using ServerFunctionApproval;
|
||||
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.AddHttpLogging(logging =>
|
||||
{
|
||||
logging.LoggingFields = HttpLoggingFields.RequestPropertiesAndHeaders | HttpLoggingFields.RequestBody
|
||||
| HttpLoggingFields.ResponsePropertiesAndHeaders | HttpLoggingFields.ResponseBody;
|
||||
logging.RequestBodyLogLimit = int.MaxValue;
|
||||
logging.ResponseBodyLogLimit = int.MaxValue;
|
||||
});
|
||||
|
||||
builder.Services.AddHttpClient().AddLogging();
|
||||
builder.Services.ConfigureHttpJsonOptions(options =>
|
||||
options.SerializerOptions.TypeInfoResolverChain.Add(ApprovalJsonContext.Default));
|
||||
builder.Services.AddAGUIServer();
|
||||
|
||||
// WARNING: When adding session persistence (e.g., WithInMemorySessionStore), or running in production,
|
||||
// make sure to also register an AgentIsolationKeyProvider to scope sessions by principal in multi-user
|
||||
// make sure to also register a SessionIsolationKeyProvider to scope sessions by principal in multi-user
|
||||
// deployments, e.g.:
|
||||
// builder.Services.UseClaimsBasedAgentIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });
|
||||
// builder.Services.UseClaimsBasedSessionIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });
|
||||
|
||||
WebApplication app = builder.Build();
|
||||
|
||||
app.UseHttpLogging();
|
||||
|
||||
string endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"]
|
||||
?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
string deploymentName = builder.Configuration["AZURE_OPENAI_DEPLOYMENT_NAME"]
|
||||
@@ -30,12 +48,13 @@ static string ApproveExpenseReport(string expenseReportId)
|
||||
return $"Expense report {expenseReportId} approved";
|
||||
}
|
||||
|
||||
// Wrap the tool in ApprovalRequiredAIFunction so the run interrupts for approval before it executes.
|
||||
AITool[] tools =
|
||||
[
|
||||
new ApprovalRequiredAIFunction(
|
||||
AIFunctionFactory.Create(ApproveExpenseReport, name: "approve_expense_report"))
|
||||
];
|
||||
// Get JsonSerializerOptions
|
||||
var jsonOptions = app.Services.GetRequiredService<IOptions<JsonOptions>>().Value;
|
||||
|
||||
// Create approval-required tool
|
||||
#pragma warning disable MEAI001 // Type is for evaluation purposes only
|
||||
AITool[] tools = [new ApprovalRequiredAIFunction(AIFunctionFactory.Create(ApproveExpenseReport))];
|
||||
#pragma warning restore MEAI001
|
||||
|
||||
// Create base agent
|
||||
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
||||
@@ -51,7 +70,8 @@ ChatClientAgent baseAgent = openAIChatClient.AsAIAgent(
|
||||
instructions: "You are a helpful assistant in charge of approving expenses",
|
||||
tools: tools);
|
||||
|
||||
// No custom approval protocol is required: MapAGUIServer emits the approval interrupt natively when the
|
||||
// model calls the approval-required tool, and resumes the run when the client sends the decision back.
|
||||
app.MapAGUIServer("/", baseAgent);
|
||||
// Wrap with ServerFunctionApprovalAgent
|
||||
var agent = new ServerFunctionApprovalAgent(baseAgent, jsonOptions.SerializerOptions);
|
||||
|
||||
app.MapAGUIServer("/", agent);
|
||||
await app.RunAsync();
|
||||
|
||||
+249
@@ -0,0 +1,249 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
using ServerFunctionApproval;
|
||||
|
||||
/// <summary>
|
||||
/// A delegating agent that handles function approval requests on the server side.
|
||||
/// Transforms between ToolApprovalRequestContent/ToolApprovalResponseContent
|
||||
/// and the request_approval tool call pattern for client communication.
|
||||
/// </summary>
|
||||
internal sealed class ServerFunctionApprovalAgent : DelegatingAIAgent
|
||||
{
|
||||
private readonly JsonSerializerOptions _jsonSerializerOptions;
|
||||
|
||||
public ServerFunctionApprovalAgent(AIAgent innerAgent, JsonSerializerOptions jsonSerializerOptions)
|
||||
: base(innerAgent)
|
||||
{
|
||||
this._jsonSerializerOptions = jsonSerializerOptions;
|
||||
}
|
||||
|
||||
protected override Task<AgentResponse> RunCoreAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentSession? session = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return this.RunCoreStreamingAsync(messages, session, options, cancellationToken)
|
||||
.ToAgentResponseAsync(cancellationToken);
|
||||
}
|
||||
|
||||
protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentSession? session = null,
|
||||
AgentRunOptions? options = null,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Process and transform incoming approval responses from client, creating a new message list
|
||||
var processedMessages = ProcessIncomingFunctionApprovals(messages.ToList(), this._jsonSerializerOptions);
|
||||
|
||||
// Run the inner agent and intercept any approval requests
|
||||
await foreach (var update in this.InnerAgent.RunStreamingAsync(
|
||||
processedMessages, session, options, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
yield return ProcessOutgoingApprovalRequests(update, this._jsonSerializerOptions);
|
||||
}
|
||||
}
|
||||
|
||||
#pragma warning disable MEAI001 // Type is for evaluation purposes only
|
||||
private static ToolApprovalRequestContent ConvertToolCallToApprovalRequest(FunctionCallContent toolCall, JsonSerializerOptions jsonSerializerOptions)
|
||||
{
|
||||
if (toolCall.Name != "request_approval" || toolCall.Arguments == null)
|
||||
{
|
||||
throw new InvalidOperationException("Invalid request_approval tool call");
|
||||
}
|
||||
|
||||
var request = (toolCall.Arguments.TryGetValue("request", out var reqObj) &&
|
||||
reqObj is JsonElement argsElement &&
|
||||
argsElement.Deserialize(jsonSerializerOptions.GetTypeInfo(typeof(ApprovalRequest))) is ApprovalRequest approvalRequest &&
|
||||
approvalRequest != null ? approvalRequest : null) ?? throw new InvalidOperationException("Failed to deserialize approval request from tool call");
|
||||
return new ToolApprovalRequestContent(
|
||||
requestId: request.ApprovalId,
|
||||
new FunctionCallContent(
|
||||
callId: request.ApprovalId,
|
||||
name: request.FunctionName,
|
||||
arguments: request.FunctionArguments));
|
||||
}
|
||||
|
||||
private static ToolApprovalResponseContent ConvertToolResultToApprovalResponse(FunctionResultContent result, ToolApprovalRequestContent approval, JsonSerializerOptions jsonSerializerOptions)
|
||||
{
|
||||
var approvalResponse = (result.Result is JsonElement je ?
|
||||
(ApprovalResponse?)je.Deserialize(jsonSerializerOptions.GetTypeInfo(typeof(ApprovalResponse))) :
|
||||
result.Result is string str ?
|
||||
(ApprovalResponse?)JsonSerializer.Deserialize(str, jsonSerializerOptions.GetTypeInfo(typeof(ApprovalResponse))) :
|
||||
result.Result as ApprovalResponse) ?? throw new InvalidOperationException("Failed to deserialize approval response from tool result");
|
||||
return approval.CreateResponse(approvalResponse.Approved);
|
||||
}
|
||||
#pragma warning restore MEAI001
|
||||
|
||||
private static List<ChatMessage> CopyMessagesUpToIndex(List<ChatMessage> messages, int index)
|
||||
{
|
||||
var result = new List<ChatMessage>(index);
|
||||
for (int i = 0; i < index; i++)
|
||||
{
|
||||
result.Add(messages[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static List<AIContent> CopyContentsUpToIndex(IList<AIContent> contents, int index)
|
||||
{
|
||||
var result = new List<AIContent>(index);
|
||||
for (int i = 0; i < index; i++)
|
||||
{
|
||||
result.Add(contents[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static List<ChatMessage> ProcessIncomingFunctionApprovals(
|
||||
List<ChatMessage> messages,
|
||||
JsonSerializerOptions jsonSerializerOptions)
|
||||
{
|
||||
List<ChatMessage>? result = null;
|
||||
|
||||
// Track approval ID to original call ID mapping
|
||||
_ = new Dictionary<string, string>();
|
||||
#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
|
||||
Dictionary<string, ToolApprovalRequestContent> trackedRequestApprovalToolCalls = []; // Remote approvals
|
||||
for (int messageIndex = 0; messageIndex < messages.Count; messageIndex++)
|
||||
{
|
||||
var message = messages[messageIndex];
|
||||
List<AIContent>? transformedContents = null;
|
||||
for (int j = 0; j < message.Contents.Count; j++)
|
||||
{
|
||||
var content = message.Contents[j];
|
||||
if (content is FunctionCallContent { Name: "request_approval" } toolCall)
|
||||
{
|
||||
result ??= CopyMessagesUpToIndex(messages, messageIndex);
|
||||
transformedContents ??= CopyContentsUpToIndex(message.Contents, j);
|
||||
var approvalRequest = ConvertToolCallToApprovalRequest(toolCall, jsonSerializerOptions);
|
||||
transformedContents.Add(approvalRequest);
|
||||
trackedRequestApprovalToolCalls[toolCall.CallId] = approvalRequest;
|
||||
result.Add(new ChatMessage(message.Role, transformedContents)
|
||||
{
|
||||
AuthorName = message.AuthorName,
|
||||
MessageId = message.MessageId,
|
||||
CreatedAt = message.CreatedAt,
|
||||
RawRepresentation = message.RawRepresentation,
|
||||
AdditionalProperties = message.AdditionalProperties
|
||||
});
|
||||
}
|
||||
else if (content is FunctionResultContent toolResult &&
|
||||
trackedRequestApprovalToolCalls.TryGetValue(toolResult.CallId, out var approval))
|
||||
{
|
||||
result ??= CopyMessagesUpToIndex(messages, messageIndex);
|
||||
transformedContents ??= CopyContentsUpToIndex(message.Contents, j);
|
||||
var approvalResponse = ConvertToolResultToApprovalResponse(toolResult, approval, jsonSerializerOptions);
|
||||
transformedContents.Add(approvalResponse);
|
||||
result.Add(new ChatMessage(message.Role, transformedContents)
|
||||
{
|
||||
AuthorName = message.AuthorName,
|
||||
MessageId = message.MessageId,
|
||||
CreatedAt = message.CreatedAt,
|
||||
RawRepresentation = message.RawRepresentation,
|
||||
AdditionalProperties = message.AdditionalProperties
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
result?.Add(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
#pragma warning restore MEAI001
|
||||
|
||||
return result ?? messages;
|
||||
}
|
||||
|
||||
private static AgentResponseUpdate ProcessOutgoingApprovalRequests(
|
||||
AgentResponseUpdate update,
|
||||
JsonSerializerOptions jsonSerializerOptions)
|
||||
{
|
||||
IList<AIContent>? updatedContents = null;
|
||||
for (var i = 0; i < update.Contents.Count; i++)
|
||||
{
|
||||
var content = update.Contents[i];
|
||||
#pragma warning disable MEAI001 // Type is for evaluation purposes only
|
||||
if (content is ToolApprovalRequestContent request && request.ToolCall is FunctionCallContent functionCall)
|
||||
{
|
||||
updatedContents ??= [.. update.Contents];
|
||||
var approvalId = request.RequestId;
|
||||
|
||||
var approvalData = new ApprovalRequest
|
||||
{
|
||||
ApprovalId = approvalId,
|
||||
FunctionName = functionCall.Name,
|
||||
FunctionArguments = functionCall.Arguments,
|
||||
Message = $"Approve execution of '{functionCall.Name}'?"
|
||||
};
|
||||
|
||||
updatedContents[i] = new FunctionCallContent(
|
||||
callId: approvalId,
|
||||
name: "request_approval",
|
||||
arguments: new Dictionary<string, object?> { ["request"] = approvalData });
|
||||
}
|
||||
#pragma warning restore MEAI001
|
||||
}
|
||||
|
||||
if (updatedContents is not null)
|
||||
{
|
||||
var chatUpdate = update.AsChatResponseUpdate();
|
||||
// Yield a tool call update that represents the approval request
|
||||
return new AgentResponseUpdate(new ChatResponseUpdate()
|
||||
{
|
||||
Role = chatUpdate.Role,
|
||||
Contents = updatedContents,
|
||||
MessageId = chatUpdate.MessageId,
|
||||
AuthorName = chatUpdate.AuthorName,
|
||||
CreatedAt = chatUpdate.CreatedAt,
|
||||
RawRepresentation = chatUpdate.RawRepresentation,
|
||||
ResponseId = chatUpdate.ResponseId,
|
||||
AdditionalProperties = chatUpdate.AdditionalProperties
|
||||
})
|
||||
{
|
||||
AgentId = update.AgentId,
|
||||
ContinuationToken = update.ContinuationToken
|
||||
};
|
||||
}
|
||||
|
||||
return update;
|
||||
}
|
||||
}
|
||||
|
||||
namespace ServerFunctionApproval
|
||||
{
|
||||
// Define approval models
|
||||
public sealed class ApprovalRequest
|
||||
{
|
||||
[JsonPropertyName("approval_id")]
|
||||
public required string ApprovalId { get; init; }
|
||||
|
||||
[JsonPropertyName("function_name")]
|
||||
public required string FunctionName { get; init; }
|
||||
|
||||
[JsonPropertyName("function_arguments")]
|
||||
public IDictionary<string, object?>? FunctionArguments { get; init; }
|
||||
|
||||
[JsonPropertyName("message")]
|
||||
public string? Message { get; init; }
|
||||
}
|
||||
|
||||
public sealed class ApprovalResponse
|
||||
{
|
||||
[JsonPropertyName("approval_id")]
|
||||
public required string ApprovalId { get; init; }
|
||||
|
||||
[JsonPropertyName("approved")]
|
||||
public required bool Approved { get; init; }
|
||||
}
|
||||
|
||||
[JsonSerializable(typeof(ApprovalRequest))]
|
||||
[JsonSerializable(typeof(ApprovalResponse))]
|
||||
[JsonSerializable(typeof(Dictionary<string, object?>))]
|
||||
public sealed partial class ApprovalJsonContext : JsonSerializerContext;
|
||||
}
|
||||
@@ -20,15 +20,16 @@ using HttpClient httpClient = new()
|
||||
|
||||
AGUIChatClient chatClient = new(new(httpClient, serverUrl));
|
||||
|
||||
AIAgent agent = chatClient.AsAIAgent(
|
||||
AIAgent baseAgent = chatClient.AsAIAgent(
|
||||
name: "recipe-client",
|
||||
description: "AG-UI Recipe Client Agent");
|
||||
|
||||
JsonSerializerOptions jsonOptions = RecipeSerializerContext.Default.Options;
|
||||
|
||||
// The recipe lives on the client. It is sent to the server on every turn (so the agent edits the
|
||||
// existing recipe) and refreshed from each STATE_SNAPSHOT the server streams back.
|
||||
Recipe currentRecipe = new();
|
||||
// Wrap the base agent with state management
|
||||
JsonSerializerOptions jsonOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
TypeInfoResolver = RecipeSerializerContext.Default
|
||||
};
|
||||
StatefulAgent<AgentState> agent = new(baseAgent, jsonOptions, new AgentState());
|
||||
|
||||
AgentSession session = await agent.CreateSessionAsync();
|
||||
List<ChatMessage> messages =
|
||||
@@ -41,7 +42,7 @@ try
|
||||
while (true)
|
||||
{
|
||||
// Get user input
|
||||
Console.Write("\nUser (:q to quit, :state to show recipe): ");
|
||||
Console.Write("\nUser (:q to quit, :state to show state): ");
|
||||
string? message = Console.ReadLine();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(message))
|
||||
@@ -57,51 +58,36 @@ try
|
||||
|
||||
if (message.Equals(":state", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
DisplayRecipe(currentRecipe);
|
||||
DisplayState(agent.State.Recipe);
|
||||
continue;
|
||||
}
|
||||
|
||||
messages.Add(new ChatMessage(ChatRole.User, message));
|
||||
|
||||
// Send the client's current recipe on the AG-UI RunAgentInput.State so the agent builds on it.
|
||||
JsonElement stateJson = JsonSerializer.SerializeToElement(
|
||||
new RecipeResponse { Recipe = currentRecipe }, jsonOptions);
|
||||
ChatClientAgentRunOptions runOptions = new()
|
||||
{
|
||||
ChatOptions = new ChatOptions
|
||||
{
|
||||
RawRepresentationFactory = _ => new RunAgentInput { State = stateJson }
|
||||
}
|
||||
};
|
||||
|
||||
// Stream the response
|
||||
bool isFirstUpdate = true;
|
||||
string? threadId = null;
|
||||
bool stateReceived = false;
|
||||
|
||||
Console.WriteLine();
|
||||
|
||||
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(messages, session, runOptions))
|
||||
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(messages, session))
|
||||
{
|
||||
ChatResponseUpdate chatUpdate = update.AsChatResponseUpdate();
|
||||
|
||||
// First update indicates run started
|
||||
if (isFirstUpdate)
|
||||
{
|
||||
// AGUIChatClient is stateless and never surfaces a ConversationId; the thread
|
||||
// id is carried on the AG-UI RUN_STARTED event's raw representation.
|
||||
threadId = (chatUpdate.RawRepresentation as RunStartedEvent)?.ThreadId;
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.WriteLine($"[Run Started - Run: {chatUpdate.ResponseId}]");
|
||||
Console.WriteLine($"[Run Started - Thread: {threadId}, Run: {chatUpdate.ResponseId}]");
|
||||
Console.ResetColor();
|
||||
isFirstUpdate = false;
|
||||
}
|
||||
|
||||
// A STATE_SNAPSHOT arrives as a StateSnapshotEvent on the update's raw representation.
|
||||
if (chatUpdate.RawRepresentation is StateSnapshotEvent snapshot &&
|
||||
snapshot.Snapshot.Deserialize<RecipeResponse>(jsonOptions) is { } response)
|
||||
{
|
||||
currentRecipe = response.Recipe;
|
||||
Console.ForegroundColor = ConsoleColor.Blue;
|
||||
Console.WriteLine("\n[State Snapshot Received]");
|
||||
Console.ResetColor();
|
||||
}
|
||||
|
||||
// Display streaming text content
|
||||
// Display streaming content
|
||||
foreach (AIContent content in update.Contents)
|
||||
{
|
||||
switch (content)
|
||||
@@ -112,6 +98,14 @@ try
|
||||
Console.ResetColor();
|
||||
break;
|
||||
|
||||
case DataContent dataContent when dataContent.MediaType == "application/json":
|
||||
// This is a state snapshot - the StatefulAgent has already updated the state
|
||||
stateReceived = true;
|
||||
Console.ForegroundColor = ConsoleColor.Blue;
|
||||
Console.WriteLine("\n[State Snapshot Received]");
|
||||
Console.ResetColor();
|
||||
break;
|
||||
|
||||
case ErrorContent errorContent:
|
||||
Console.ForegroundColor = ConsoleColor.Red;
|
||||
Console.WriteLine($"\n[Error: {errorContent.Message}]");
|
||||
@@ -121,14 +115,15 @@ try
|
||||
}
|
||||
}
|
||||
|
||||
// The session owns prior history, so the next run sends only the new user message.
|
||||
messages.Clear();
|
||||
|
||||
Console.ForegroundColor = ConsoleColor.Green;
|
||||
Console.WriteLine("\n[Run Finished]");
|
||||
Console.WriteLine($"\n[Run Finished - Thread: {threadId}]");
|
||||
Console.ResetColor();
|
||||
|
||||
DisplayRecipe(currentRecipe);
|
||||
// Display final state if received
|
||||
if (stateReceived)
|
||||
{
|
||||
DisplayState(agent.State.Recipe);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -136,53 +131,61 @@ catch (Exception ex)
|
||||
Console.WriteLine($"\nAn error occurred: {ex.Message}");
|
||||
}
|
||||
|
||||
static void DisplayRecipe(Recipe recipe)
|
||||
static void DisplayState(RecipeState? state)
|
||||
{
|
||||
if (state == null)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Gray;
|
||||
Console.WriteLine("\n[No state available]");
|
||||
Console.ResetColor();
|
||||
return;
|
||||
}
|
||||
|
||||
Console.ForegroundColor = ConsoleColor.Blue;
|
||||
Console.WriteLine("\n" + new string('=', 60));
|
||||
Console.WriteLine("CURRENT RECIPE");
|
||||
Console.WriteLine("CURRENT STATE");
|
||||
Console.WriteLine(new string('=', 60));
|
||||
Console.ResetColor();
|
||||
|
||||
if (string.IsNullOrEmpty(recipe.Title))
|
||||
if (!string.IsNullOrEmpty(state.Title))
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Gray;
|
||||
Console.WriteLine("\n[No recipe yet]");
|
||||
Console.ResetColor();
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"\n Title: {recipe.Title}");
|
||||
if (!string.IsNullOrEmpty(recipe.SkillLevel))
|
||||
Console.WriteLine("\nRecipe:");
|
||||
Console.WriteLine($" Title: {state.Title}");
|
||||
if (!string.IsNullOrEmpty(state.Cuisine))
|
||||
{
|
||||
Console.WriteLine($" Skill Level: {recipe.SkillLevel}");
|
||||
Console.WriteLine($" Cuisine: {state.Cuisine}");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(recipe.CookingTime))
|
||||
if (!string.IsNullOrEmpty(state.SkillLevel))
|
||||
{
|
||||
Console.WriteLine($" Cooking Time: {recipe.CookingTime}");
|
||||
Console.WriteLine($" Skill Level: {state.SkillLevel}");
|
||||
}
|
||||
|
||||
if (recipe.SpecialPreferences.Count > 0)
|
||||
if (state.PrepTimeMinutes > 0)
|
||||
{
|
||||
Console.WriteLine($" Preferences: {string.Join(", ", recipe.SpecialPreferences)}");
|
||||
Console.WriteLine($" Prep Time: {state.PrepTimeMinutes} minutes");
|
||||
}
|
||||
|
||||
if (recipe.Ingredients.Count > 0)
|
||||
if (state.CookTimeMinutes > 0)
|
||||
{
|
||||
Console.WriteLine($" Cook Time: {state.CookTimeMinutes} minutes");
|
||||
}
|
||||
|
||||
if (state.Ingredients.Count > 0)
|
||||
{
|
||||
Console.WriteLine("\n Ingredients:");
|
||||
foreach (Ingredient ingredient in recipe.Ingredients)
|
||||
foreach (var ingredient in state.Ingredients)
|
||||
{
|
||||
Console.WriteLine($" {ingredient.Icon} {ingredient.Name} - {ingredient.Amount}");
|
||||
Console.WriteLine($" - {ingredient}");
|
||||
}
|
||||
}
|
||||
|
||||
if (recipe.Instructions.Count > 0)
|
||||
if (state.Steps.Count > 0)
|
||||
{
|
||||
Console.WriteLine("\n Instructions:");
|
||||
for (int i = 0; i < recipe.Instructions.Count; i++)
|
||||
Console.WriteLine("\n Steps:");
|
||||
for (int i = 0; i < state.Steps.Count; i++)
|
||||
{
|
||||
Console.WriteLine($" {i + 1}. {recipe.Instructions[i]}");
|
||||
Console.WriteLine($" {i + 1}. {state.Steps[i]}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -192,53 +195,40 @@ static void DisplayRecipe(Recipe recipe)
|
||||
Console.ResetColor();
|
||||
}
|
||||
|
||||
namespace RecipeClient
|
||||
// State wrapper
|
||||
internal sealed class AgentState
|
||||
{
|
||||
// State response wrapper. Its shape mirrors what the server returns and renders as state.
|
||||
internal sealed class RecipeResponse
|
||||
{
|
||||
[JsonPropertyName("recipe")]
|
||||
public Recipe Recipe { get; set; } = new();
|
||||
}
|
||||
|
||||
// Recipe state model.
|
||||
internal sealed class Recipe
|
||||
{
|
||||
[JsonPropertyName("title")]
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("skill_level")]
|
||||
public string SkillLevel { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("cooking_time")]
|
||||
public string CookingTime { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("special_preferences")]
|
||||
public List<string> SpecialPreferences { get; set; } = [];
|
||||
|
||||
[JsonPropertyName("ingredients")]
|
||||
public List<Ingredient> Ingredients { get; set; } = [];
|
||||
|
||||
[JsonPropertyName("instructions")]
|
||||
public List<string> Instructions { get; set; } = [];
|
||||
}
|
||||
|
||||
// A single ingredient.
|
||||
internal sealed class Ingredient
|
||||
{
|
||||
[JsonPropertyName("icon")]
|
||||
public string Icon { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("amount")]
|
||||
public string Amount { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
// JSON serialization context.
|
||||
[JsonSerializable(typeof(RecipeResponse))]
|
||||
[JsonSerializable(typeof(Recipe))]
|
||||
[JsonSerializable(typeof(Ingredient))]
|
||||
internal sealed partial class RecipeSerializerContext : JsonSerializerContext;
|
||||
[JsonPropertyName("recipe")]
|
||||
public RecipeState Recipe { get; set; } = new();
|
||||
}
|
||||
|
||||
// Recipe state model
|
||||
internal sealed class RecipeState
|
||||
{
|
||||
[JsonPropertyName("title")]
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("cuisine")]
|
||||
public string Cuisine { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("ingredients")]
|
||||
public List<string> Ingredients { get; set; } = [];
|
||||
|
||||
[JsonPropertyName("steps")]
|
||||
public List<string> Steps { get; set; } = [];
|
||||
|
||||
[JsonPropertyName("prep_time_minutes")]
|
||||
public int PrepTimeMinutes { get; set; }
|
||||
|
||||
[JsonPropertyName("cook_time_minutes")]
|
||||
public int CookTimeMinutes { get; set; }
|
||||
|
||||
[JsonPropertyName("skill_level")]
|
||||
public string SkillLevel { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
// JSON serialization context
|
||||
[JsonSerializable(typeof(AgentState))]
|
||||
[JsonSerializable(typeof(RecipeState))]
|
||||
[JsonSerializable(typeof(JsonElement))]
|
||||
internal sealed partial class RecipeSerializerContext : JsonSerializerContext;
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace RecipeClient;
|
||||
|
||||
/// <summary>
|
||||
/// A delegating agent that manages client-side state and automatically attaches it to requests.
|
||||
/// </summary>
|
||||
/// <typeparam name="TState">The state type.</typeparam>
|
||||
internal sealed class StatefulAgent<TState> : DelegatingAIAgent
|
||||
where TState : class, new()
|
||||
{
|
||||
private readonly JsonSerializerOptions _jsonSerializerOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the current state.
|
||||
/// </summary>
|
||||
public TState State { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StatefulAgent{TState}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="innerAgent">The underlying agent to delegate to.</param>
|
||||
/// <param name="jsonSerializerOptions">The JSON serializer options for state serialization.</param>
|
||||
/// <param name="initialState">The initial state. If null, a new instance will be created.</param>
|
||||
public StatefulAgent(AIAgent innerAgent, JsonSerializerOptions jsonSerializerOptions, TState? initialState = null)
|
||||
: base(innerAgent)
|
||||
{
|
||||
this._jsonSerializerOptions = jsonSerializerOptions;
|
||||
this.State = initialState ?? new TState();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task<AgentResponse> RunCoreAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentSession? session = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return this.RunCoreStreamingAsync(messages, session, options, cancellationToken)
|
||||
.ToAgentResponseAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentSession? session = null,
|
||||
AgentRunOptions? options = null,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Add state to messages
|
||||
List<ChatMessage> messagesWithState = [.. messages];
|
||||
|
||||
// Serialize the state using AgentState wrapper
|
||||
byte[] stateBytes = JsonSerializer.SerializeToUtf8Bytes(
|
||||
this.State,
|
||||
this._jsonSerializerOptions.GetTypeInfo(typeof(TState)));
|
||||
DataContent stateContent = new(stateBytes, "application/json");
|
||||
ChatMessage stateMessage = new(ChatRole.System, [stateContent]);
|
||||
messagesWithState.Add(stateMessage);
|
||||
|
||||
// Stream the response and update state when received
|
||||
await foreach (AgentResponseUpdate update in this.InnerAgent.RunStreamingAsync(messagesWithState, session, options, cancellationToken))
|
||||
{
|
||||
// Check if this update contains a state snapshot
|
||||
foreach (AIContent content in update.Contents)
|
||||
{
|
||||
if (content is DataContent dataContent && dataContent.MediaType == "application/json")
|
||||
{
|
||||
// Deserialize the state
|
||||
if (JsonSerializer.Deserialize(
|
||||
dataContent.Data.Span,
|
||||
this._jsonSerializerOptions.GetTypeInfo(typeof(TState))) is TState newState)
|
||||
{
|
||||
this.State = newState;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
yield return update;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,15 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.ComponentModel;
|
||||
using AGUI.Server;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.Options;
|
||||
using OpenAI.Chat;
|
||||
using RecipeAssistant;
|
||||
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
|
||||
builder.Services.AddHttpClient().AddLogging();
|
||||
builder.Services.ConfigureHttpJsonOptions(options =>
|
||||
options.SerializerOptions.TypeInfoResolverChain.Add(RecipeSerializerContext.Default));
|
||||
builder.Services.AddAGUIServer();
|
||||
@@ -19,9 +18,9 @@ builder.Services.AddAGUIServer();
|
||||
builder.WebHost.UseUrls("http://localhost:8888");
|
||||
|
||||
// WARNING: When adding session persistence (e.g., WithInMemorySessionStore), or running in production,
|
||||
// make sure to also register an AgentIsolationKeyProvider to scope sessions by principal in multi-user
|
||||
// make sure to also register a SessionIsolationKeyProvider to scope sessions by principal in multi-user
|
||||
// deployments, e.g.:
|
||||
// builder.Services.UseClaimsBasedAgentIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });
|
||||
// builder.Services.UseClaimsBasedSessionIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });
|
||||
|
||||
WebApplication app = builder.Build();
|
||||
|
||||
@@ -30,32 +29,10 @@ string endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"]
|
||||
string deploymentName = builder.Configuration["AZURE_OPENAI_DEPLOYMENT_NAME"]
|
||||
?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set.");
|
||||
|
||||
// The tool returns the complete recipe. The hosting layer turns each result into a STATE_SNAPSHOT
|
||||
// event via AGUIStreamOptions.MapResultAsStateSnapshot("generate_recipe") - no protocol content by hand.
|
||||
[Description("Generate or update the shared recipe and display it to the user.")]
|
||||
static RecipeResponse GenerateRecipe(
|
||||
[Description("The complete recipe to display.")] Recipe recipe) => new() { Recipe = recipe };
|
||||
// Get JsonSerializerOptions
|
||||
var jsonOptions = app.Services.GetRequiredService<IOptions<Microsoft.AspNetCore.Http.Json.JsonOptions>>().Value;
|
||||
|
||||
AITool generateRecipe = AIFunctionFactory.Create(
|
||||
GenerateRecipe,
|
||||
name: "generate_recipe",
|
||||
description: "Generate or update the shared recipe and display it to the user.",
|
||||
RecipeSerializerContext.Default.Options);
|
||||
|
||||
const string SharedStateSystemPrompt =
|
||||
"""
|
||||
You are a helpful recipe assistant that maintains a shared recipe state with the user.
|
||||
|
||||
IMPORTANT:
|
||||
- When the user asks you to create, change, or improve a recipe, call the `generate_recipe`
|
||||
tool with a COMPLETE recipe: a title, skill_level, cooking_time, special_preferences, the
|
||||
full list of ingredients (each with an icon, name and amount) and the step-by-step
|
||||
instructions.
|
||||
- Always include every ingredient the recipe needs, keeping any the user already added.
|
||||
- When the user only asks a question about the recipe, answer in plain text and do NOT call the tool.
|
||||
""";
|
||||
|
||||
// Create the AI agent with the recipe tool.
|
||||
// Create base agent
|
||||
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
||||
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
@@ -64,22 +41,26 @@ ChatClient chatClient = new AzureOpenAIClient(
|
||||
new DefaultAzureCredential())
|
||||
.GetChatClient(deploymentName);
|
||||
|
||||
AIAgent baseAgent = chatClient.AsAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
Name = "RecipeAgent",
|
||||
Description = "An agent that maintains a shared recipe state with the user.",
|
||||
ChatOptions = new ChatOptions
|
||||
{
|
||||
Instructions = SharedStateSystemPrompt,
|
||||
Tools = [generateRecipe],
|
||||
},
|
||||
});
|
||||
AIAgent baseAgent = chatClient.AsAIAgent(
|
||||
name: "RecipeAgent",
|
||||
instructions: """
|
||||
You are a helpful recipe assistant. When users ask you to create or suggest a recipe,
|
||||
respond with a complete AgentState JSON object that includes:
|
||||
- recipe.title: The recipe name
|
||||
- recipe.cuisine: Type of cuisine (e.g., Italian, Mexican, Japanese)
|
||||
- recipe.ingredients: Array of ingredient strings with quantities
|
||||
- recipe.steps: Array of cooking instruction strings
|
||||
- recipe.prep_time_minutes: Preparation time in minutes
|
||||
- recipe.cook_time_minutes: Cooking time in minutes
|
||||
- recipe.skill_level: One of "beginner", "intermediate", or "advanced"
|
||||
|
||||
// Wrap with a thin agent that injects the client's current recipe (input side of shared state).
|
||||
AIAgent agent = new RecipeStateAgent(baseAgent);
|
||||
Always include all fields in the response. Be creative and helpful.
|
||||
""");
|
||||
|
||||
// Map the AG-UI endpoint. A generate_recipe result becomes a STATE_SNAPSHOT event (output side).
|
||||
app.MapAGUIServer("/", agent)
|
||||
.WithMetadata(new AGUIStreamOptions().MapResultAsStateSnapshot("generate_recipe"));
|
||||
// Wrap with state management middleware
|
||||
AIAgent agent = new SharedStateAgent(baseAgent, jsonOptions.SerializerOptions);
|
||||
|
||||
// Map the AG-UI agent endpoint
|
||||
app.MapAGUIServer("/", agent);
|
||||
|
||||
await app.RunAsync();
|
||||
|
||||
@@ -4,50 +4,40 @@ using System.Text.Json.Serialization;
|
||||
|
||||
namespace RecipeAssistant;
|
||||
|
||||
// State response wrapper returned by the tool. Its shape is what the client renders as state.
|
||||
internal sealed class RecipeResponse
|
||||
// State wrapper
|
||||
internal sealed class AgentState
|
||||
{
|
||||
[JsonPropertyName("recipe")]
|
||||
public Recipe Recipe { get; set; } = new();
|
||||
public RecipeState Recipe { get; set; } = new();
|
||||
}
|
||||
|
||||
// Recipe state model.
|
||||
internal sealed class Recipe
|
||||
// Recipe state model
|
||||
internal sealed class RecipeState
|
||||
{
|
||||
[JsonPropertyName("title")]
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("skill_level")]
|
||||
public string SkillLevel { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("cooking_time")]
|
||||
public string CookingTime { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("special_preferences")]
|
||||
public List<string> SpecialPreferences { get; set; } = [];
|
||||
[JsonPropertyName("cuisine")]
|
||||
public string Cuisine { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("ingredients")]
|
||||
public List<Ingredient> Ingredients { get; set; } = [];
|
||||
public List<string> Ingredients { get; set; } = [];
|
||||
|
||||
[JsonPropertyName("instructions")]
|
||||
public List<string> Instructions { get; set; } = [];
|
||||
[JsonPropertyName("steps")]
|
||||
public List<string> Steps { get; set; } = [];
|
||||
|
||||
[JsonPropertyName("prep_time_minutes")]
|
||||
public int PrepTimeMinutes { get; set; }
|
||||
|
||||
[JsonPropertyName("cook_time_minutes")]
|
||||
public int CookTimeMinutes { get; set; }
|
||||
|
||||
[JsonPropertyName("skill_level")]
|
||||
public string SkillLevel { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
// A single ingredient.
|
||||
internal sealed class Ingredient
|
||||
{
|
||||
[JsonPropertyName("icon")]
|
||||
public string Icon { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("amount")]
|
||||
public string Amount { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
// JSON serialization context for the tool payloads.
|
||||
[JsonSerializable(typeof(RecipeResponse))]
|
||||
[JsonSerializable(typeof(Recipe))]
|
||||
[JsonSerializable(typeof(Ingredient))]
|
||||
// JSON serialization context
|
||||
[JsonSerializable(typeof(AgentState))]
|
||||
[JsonSerializable(typeof(RecipeState))]
|
||||
[JsonSerializable(typeof(System.Text.Json.JsonElement))]
|
||||
internal sealed partial class RecipeSerializerContext : JsonSerializerContext;
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json;
|
||||
using AGUI.Abstractions;
|
||||
using AGUI.Server;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace RecipeAssistant;
|
||||
|
||||
/// <summary>
|
||||
/// A thin agent that reads the client's current recipe from the AG-UI <see cref="RunAgentInput.State"/>
|
||||
/// and prepends it to the conversation as a system message, so the model edits the existing recipe
|
||||
/// instead of starting over. This handles the input side of shared state only. The output side is
|
||||
/// declarative: the inner agent's <c>generate_recipe</c> tool result becomes a <c>STATE_SNAPSHOT</c>
|
||||
/// via <c>AGUIStreamOptions.MapResultAsStateSnapshot</c>.
|
||||
/// </summary>
|
||||
internal sealed class RecipeStateAgent(AIAgent innerAgent) : DelegatingAIAgent(innerAgent)
|
||||
{
|
||||
protected override Task<AgentResponse> RunCoreAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentSession? session = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
this.RunCoreStreamingAsync(messages, session, options, cancellationToken)
|
||||
.ToAgentResponseAsync(cancellationToken);
|
||||
|
||||
protected override IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentSession? session = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (options is ChatClientAgentRunOptions { ChatOptions: { } chatOptions } &&
|
||||
chatOptions.TryGetRunAgentInput(out RunAgentInput? input) &&
|
||||
input.State is { ValueKind: JsonValueKind.Object } state)
|
||||
{
|
||||
ChatMessage stateMessage = new(
|
||||
ChatRole.System,
|
||||
$"The user's current recipe state is:\n{state.GetRawText()}");
|
||||
messages = [stateMessage, .. messages];
|
||||
}
|
||||
|
||||
return this.InnerAgent.RunStreamingAsync(messages, session, options, cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
using AGUI.Abstractions;
|
||||
using AGUI.Server;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace RecipeAssistant;
|
||||
|
||||
internal sealed class SharedStateAgent : DelegatingAIAgent
|
||||
{
|
||||
private readonly JsonSerializerOptions _jsonSerializerOptions;
|
||||
|
||||
public SharedStateAgent(AIAgent innerAgent, JsonSerializerOptions jsonSerializerOptions)
|
||||
: base(innerAgent)
|
||||
{
|
||||
this._jsonSerializerOptions = jsonSerializerOptions;
|
||||
}
|
||||
|
||||
protected override Task<AgentResponse> RunCoreAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentSession? session = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return this.RunCoreStreamingAsync(messages, session, options, cancellationToken)
|
||||
.ToAgentResponseAsync(cancellationToken);
|
||||
}
|
||||
|
||||
protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentSession? session = null,
|
||||
AgentRunOptions? options = null,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Check if the client sent state in the request
|
||||
if (options is not ChatClientAgentRunOptions { ChatOptions: { } chatOptions } chatRunOptions ||
|
||||
!chatOptions.TryGetRunAgentInput(out RunAgentInput? agentInput) ||
|
||||
agentInput.State is not { ValueKind: JsonValueKind.Object } state)
|
||||
{
|
||||
// No state management requested, pass through to inner agent
|
||||
await foreach (var update in this.InnerAgent.RunStreamingAsync(messages, session, options, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
yield return update;
|
||||
}
|
||||
yield break;
|
||||
}
|
||||
|
||||
// Check if state has properties (not empty {})
|
||||
bool hasProperties = false;
|
||||
foreach (JsonProperty _ in state.EnumerateObject())
|
||||
{
|
||||
hasProperties = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!hasProperties)
|
||||
{
|
||||
// Empty state - treat as no state
|
||||
await foreach (var update in this.InnerAgent.RunStreamingAsync(messages, session, options, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
yield return update;
|
||||
}
|
||||
yield break;
|
||||
}
|
||||
|
||||
// First run: Generate structured state update
|
||||
var firstRunOptions = new ChatClientAgentRunOptions
|
||||
{
|
||||
ChatOptions = chatRunOptions.ChatOptions.Clone(),
|
||||
AllowBackgroundResponses = chatRunOptions.AllowBackgroundResponses,
|
||||
ContinuationToken = chatRunOptions.ContinuationToken,
|
||||
ChatClientFactory = chatRunOptions.ChatClientFactory,
|
||||
};
|
||||
|
||||
// Configure JSON schema response format for structured state output
|
||||
firstRunOptions.ChatOptions.ResponseFormat = ChatResponseFormat.ForJsonSchema<AgentState>(
|
||||
schemaName: "AgentState",
|
||||
schemaDescription: "A response containing a recipe with title, skill level, cooking time, ingredients, and instructions");
|
||||
|
||||
// Add current state to the conversation - state is already a JsonElement
|
||||
ChatMessage stateUpdateMessage = new(
|
||||
ChatRole.System,
|
||||
[
|
||||
new TextContent("Here is the current state in JSON format:"),
|
||||
new TextContent(JsonSerializer.Serialize(state, this._jsonSerializerOptions.GetTypeInfo(typeof(JsonElement)))),
|
||||
new TextContent("The new state is:")
|
||||
]);
|
||||
|
||||
var firstRunMessages = messages.Append(stateUpdateMessage);
|
||||
|
||||
// Collect all updates from first run
|
||||
var allUpdates = new List<AgentResponseUpdate>();
|
||||
await foreach (var update in this.InnerAgent.RunStreamingAsync(firstRunMessages, session, firstRunOptions, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
allUpdates.Add(update);
|
||||
|
||||
// Yield all non-text updates (tool calls, etc.)
|
||||
bool hasNonTextContent = update.Contents.Any(c => c is not TextContent);
|
||||
if (hasNonTextContent)
|
||||
{
|
||||
yield return update;
|
||||
}
|
||||
}
|
||||
|
||||
var response = allUpdates.ToAgentResponse();
|
||||
|
||||
// Try to deserialize the structured state response
|
||||
if (TryDeserialize(response.Text, this._jsonSerializerOptions, out JsonElement stateSnapshot))
|
||||
{
|
||||
// Serialize and emit as STATE_SNAPSHOT via DataContent
|
||||
byte[] stateBytes = JsonSerializer.SerializeToUtf8Bytes(
|
||||
stateSnapshot,
|
||||
this._jsonSerializerOptions.GetTypeInfo(typeof(JsonElement)));
|
||||
yield return new AgentResponseUpdate
|
||||
{
|
||||
Contents = [new DataContent(stateBytes, "application/json")]
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
// Second run: Generate user-friendly summary
|
||||
var secondRunMessages = messages.Concat(response.Messages).Append(
|
||||
new ChatMessage(
|
||||
ChatRole.System,
|
||||
[new TextContent("Please provide a concise summary of the state changes in at most two sentences.")]));
|
||||
|
||||
await foreach (var update in this.InnerAgent.RunStreamingAsync(secondRunMessages, session, options, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
yield return update;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryDeserialize<T>(string json, JsonSerializerOptions jsonSerializerOptions, out T structuredOutput)
|
||||
{
|
||||
try
|
||||
{
|
||||
T? deserialized = JsonSerializer.Deserialize<T>(json, jsonSerializerOptions);
|
||||
if (deserialized is null)
|
||||
{
|
||||
structuredOutput = default!;
|
||||
return false;
|
||||
}
|
||||
|
||||
structuredOutput = deserialized;
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
structuredOutput = default!;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,7 +60,6 @@ covering basics, function tools, structured output, middleware, MCP, code interp
|
||||
| Sample | Description |
|
||||
| --- | --- |
|
||||
| [GitHub Copilot](./github-copilot/Agent_With_GitHubCopilot/) | Create an AIAgent using GitHub Copilot SDK |
|
||||
| [GitHub Copilot BYOK](./github-copilot/Agent_With_GitHubCopilot_BYOK/) | Route GitHub Copilot agent requests through your own endpoint (Bring Your Own Key) |
|
||||
|
||||
### [Google Gemini](./google-gemini/)
|
||||
|
||||
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<NoWarn>$(NoWarn);GHCP001</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="GitHub.Copilot.SDK" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.GitHub.Copilot\Microsoft.Agents.AI.GitHub.Copilot.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
-52
@@ -1,52 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample shows how to configure a GitHub Copilot agent with BYOK (Bring Your Own Key),
|
||||
// routing requests through your own endpoint (OpenAI, Azure OpenAI, Anthropic, or an
|
||||
// OpenAI-compatible service such as vLLM/LiteLLM/Ollama) instead of the GitHub Copilot backend.
|
||||
//
|
||||
// SECURITY NOTE: BYOK uses static credentials (no automatic token refresh) and usage is tracked
|
||||
// by your provider rather than GitHub. Keep API keys out of source control; load them from
|
||||
// environment variables or a secret store, as shown here.
|
||||
|
||||
using GitHub.Copilot;
|
||||
using Microsoft.Agents.AI;
|
||||
|
||||
string providerType = Environment.GetEnvironmentVariable("BYOK_PROVIDER_TYPE") ?? "openai";
|
||||
string baseUrl = Environment.GetEnvironmentVariable("BYOK_BASE_URL")
|
||||
?? throw new InvalidOperationException("The BYOK_BASE_URL environment variable is not set.");
|
||||
string apiKey = Environment.GetEnvironmentVariable("BYOK_API_KEY")
|
||||
?? throw new InvalidOperationException("The BYOK_API_KEY environment variable is not set.");
|
||||
string modelId = Environment.GetEnvironmentVariable("BYOK_MODEL_ID") ?? "gpt-4o";
|
||||
|
||||
// Create and start a Copilot client
|
||||
await using CopilotClient copilotClient = new();
|
||||
await copilotClient.StartAsync();
|
||||
|
||||
// Provider routes the session through a custom endpoint instead of the GitHub Copilot backend.
|
||||
// Type is "openai", "azure", or "anthropic". WireApi "completions" is the broadly compatible
|
||||
// choice; use "responses" for providers that support the OpenAI Responses API. BYOK also
|
||||
// requires Model to be set at the session level.
|
||||
SessionConfig sessionConfig = new()
|
||||
{
|
||||
Model = modelId,
|
||||
Provider = new ProviderConfig
|
||||
{
|
||||
Type = providerType,
|
||||
WireApi = "completions",
|
||||
BaseUrl = baseUrl,
|
||||
ApiKey = apiKey,
|
||||
ModelId = modelId,
|
||||
},
|
||||
};
|
||||
|
||||
AIAgent agent = copilotClient.AsAIAgent(sessionConfig, ownsClient: true);
|
||||
|
||||
string prompt = "What are the benefits of using your own API keys with an agent framework?";
|
||||
Console.WriteLine($"User: {prompt}\n");
|
||||
|
||||
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(prompt))
|
||||
{
|
||||
Console.Write(update);
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
-76
@@ -1,76 +0,0 @@
|
||||
# About BYOK (Bring Your Own Key)
|
||||
|
||||
BYOK lets you route model requests through your own API keys and infrastructure instead of the
|
||||
GitHub Copilot backend — useful for enterprise deployments, custom hosting, or direct billing
|
||||
arrangements. See [GitHub's BYOK documentation](https://docs.github.com/en/copilot/how-tos/copilot-sdk/auth/byok)
|
||||
for the full list of supported providers and configuration options.
|
||||
|
||||
# Prerequisites
|
||||
|
||||
Before you begin, ensure you have the following prerequisites:
|
||||
|
||||
- .NET 10 SDK or later
|
||||
- GitHub Copilot CLI installed and available in your PATH (or provide a custom path)
|
||||
- An OpenAI, Azure OpenAI, Anthropic, or OpenAI-compatible endpoint and API key (e.g. vLLM,
|
||||
LiteLLM, or Ollama)
|
||||
|
||||
## Setting up GitHub Copilot CLI
|
||||
|
||||
To use this sample, you need to have the GitHub Copilot CLI installed. You can install it by
|
||||
following the instructions at:
|
||||
https://github.com/github/copilot-sdk
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `BYOK_PROVIDER_TYPE` | Provider type (`openai`, `azure`, `anthropic`) | `openai` |
|
||||
| `BYOK_BASE_URL` | Base URL of your provider endpoint | *(required)* |
|
||||
| `BYOK_API_KEY` | API key for that endpoint | *(required)* |
|
||||
| `BYOK_MODEL_ID` | Model name to request (e.g. "gpt-4o") | `gpt-4o` |
|
||||
|
||||
## Running the Sample
|
||||
|
||||
```powershell
|
||||
dotnet run
|
||||
```
|
||||
|
||||
The sample will:
|
||||
|
||||
1. Create a GitHub Copilot client with default options
|
||||
2. Configure a session with a `Provider` (BYOK) pointing at your own endpoint instead of the
|
||||
default GitHub Copilot backend
|
||||
3. Send a message to the agent
|
||||
4. Stream the response
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
```csharp
|
||||
using GitHub.Copilot;
|
||||
using Microsoft.Agents.AI;
|
||||
|
||||
await using CopilotClient copilotClient = new();
|
||||
await copilotClient.StartAsync();
|
||||
|
||||
SessionConfig sessionConfig = new()
|
||||
{
|
||||
// BYOK requires Model to also be set at the session level.
|
||||
Model = "gpt-4o",
|
||||
Provider = new ProviderConfig
|
||||
{
|
||||
Type = "azure", // or "openai", "anthropic"
|
||||
WireApi = "completions", // or "responses"
|
||||
BaseUrl = "https://api.example.com/v1",
|
||||
ApiKey = "your-api-key",
|
||||
ModelId = "your-model-id", // "deployment-name"
|
||||
},
|
||||
};
|
||||
|
||||
AIAgent agent = copilotClient.AsAIAgent(sessionConfig, ownsClient: true);
|
||||
AgentResponse response = await agent.RunAsync("Hello!");
|
||||
Console.WriteLine(response);
|
||||
```
|
||||
|
||||
> **Note:** BYOK uses static credentials only — dynamic token refresh is not automatic, and
|
||||
> model availability depends entirely on your provider's offerings. Usage is tracked through
|
||||
> your provider rather than GitHub.
|
||||
+2
-2
@@ -38,8 +38,8 @@
|
||||
<ItemGroup>
|
||||
<!-- AgentMemory (published) — an unofficial .NET port of the Neo4j Labs agent-memory library + its
|
||||
Microsoft Agent Framework adapter. -->
|
||||
<PackageReference Include="AgentMemory" Version="1.4.1" />
|
||||
<PackageReference Include="AgentMemory.AgentFramework" Version="1.4.1" />
|
||||
<PackageReference Include="AgentMemory" Version="1.2.0" />
|
||||
<PackageReference Include="AgentMemory.AgentFramework" Version="1.2.0" />
|
||||
<!-- Microsoft Agent Framework (matches AgentMemory's target) + the OpenAI/Foundry chat & embedding clients. -->
|
||||
<PackageReference Include="Microsoft.Agents.AI" Version="1.9.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="10.5.1" />
|
||||
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
-98
@@ -1,98 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample shows how to give an agent file-based memory using the FileMemoryProvider.
|
||||
// The FileMemoryProvider exposes a set of tools to the agent (write, read, delete, list, grep and replace)
|
||||
// that allow it to store memories as individual files in an AgentFileStore.
|
||||
// Because the files are stored outside of the conversation, the agent can recall them
|
||||
// in later conversations, even after the original chat history is gone.
|
||||
//
|
||||
// The sample also shows how to control the folder that memory files are written to,
|
||||
// by supplying a state initializer callback that sets the working folder for each session.
|
||||
|
||||
#pragma warning disable MAAI001 // AgentFileStore and its implementations are experimental.
|
||||
|
||||
using Azure.AI.Projects;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
|
||||
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
|
||||
|
||||
// The id of the user that we are storing memories for.
|
||||
// It is used below to give each user their own memory folder.
|
||||
const string UserId = "UID1";
|
||||
|
||||
// Create the file store that the FileMemoryProvider will use to persist memory files.
|
||||
// Here we use a file system backed store rooted at a local folder called "agent-memory",
|
||||
// but any AgentFileStore implementation can be used, e.g. InMemoryAgentFileStore or a custom
|
||||
// implementation backed by blob storage.
|
||||
var memoryRoot = Path.Combine(AppContext.BaseDirectory, "agent-memory");
|
||||
var fileStore = new FileSystemAgentFileStore(memoryRoot);
|
||||
|
||||
// The working folder that memories for this user will be written to, relative to the store root.
|
||||
// The folder you choose determines the scope and lifetime of the memories:
|
||||
// - A stable folder, like the per-user one below, gives you durable memories that are shared by
|
||||
// every session for that user. That is what allows the second conversation further down to
|
||||
// recall what the user said in the first.
|
||||
// - A unique folder per session gives you memories that are isolated to a single session, e.g.
|
||||
// generate one in the state initializer callback below:
|
||||
// _ => new FileMemoryState { WorkingFolder = Guid.NewGuid().ToString() }
|
||||
var workingFolder = $"users/{UserId}";
|
||||
|
||||
Console.WriteLine($"Memory files will be written to: {Path.Combine(memoryRoot, workingFolder)}");
|
||||
Console.WriteLine();
|
||||
|
||||
// Create the file memory provider.
|
||||
// The second parameter is a state initializer callback that is invoked whenever the provider
|
||||
// cannot find existing state in a session, i.e. typically the first time it is used with a new session.
|
||||
// It allows us to configure the folder that memory files for that session are written to.
|
||||
// If no callback is supplied, the working folder defaults to the root of the store,
|
||||
// which means all sessions share a single, flat set of memory files.
|
||||
using var fileMemoryProvider = new FileMemoryProvider(
|
||||
fileStore,
|
||||
_ => new FileMemoryState { WorkingFolder = workingFolder });
|
||||
|
||||
// Create the agent and attach the FileMemoryProvider so that the agent gets the file memory tools.
|
||||
AIAgent agent = new AIProjectClient(
|
||||
new Uri(endpoint),
|
||||
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
||||
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
new DefaultAzureCredential())
|
||||
.AsAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
ChatOptions = new()
|
||||
{
|
||||
ModelId = deploymentName,
|
||||
Instructions = "You are a helpful travel assistant. Remember what the user tells you about themselves so that you can give better recommendations later."
|
||||
},
|
||||
Name = "TravelAssistant",
|
||||
AIContextProviders = [fileMemoryProvider],
|
||||
});
|
||||
|
||||
// First conversation: tell the agent something worth remembering.
|
||||
// The agent should use the file_memory_write tool to store it as a file in the working folder.
|
||||
AgentSession firstSession = await agent.CreateSessionAsync();
|
||||
Console.WriteLine("=== First conversation ===");
|
||||
Console.WriteLine(await agent.RunAsync(
|
||||
"I'm vegetarian and I always travel with my dog. Please remember this for future trips.",
|
||||
firstSession));
|
||||
Console.WriteLine();
|
||||
|
||||
// Show the memory files that the agent created on disk.
|
||||
Console.WriteLine("=== Memory files on disk ===");
|
||||
foreach (var file in Directory.EnumerateFiles(Path.Combine(memoryRoot, workingFolder)))
|
||||
{
|
||||
Console.WriteLine(Path.GetFileName(file));
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
|
||||
// Second conversation: a brand new session with no chat history from the first conversation.
|
||||
// The provider surfaces the memory index to the agent, and the agent can read the memory files
|
||||
// using the file_memory_read tool, so it can still recall the user's preferences.
|
||||
AgentSession secondSession = await agent.CreateSessionAsync();
|
||||
Console.WriteLine("=== Second conversation (new session) ===");
|
||||
Console.WriteLine(await agent.RunAsync(
|
||||
"Suggest a hotel and a restaurant for my trip to Paris next week.",
|
||||
secondSession));
|
||||
-68
@@ -1,68 +0,0 @@
|
||||
# File Based Memory with FileMemoryProvider
|
||||
|
||||
This sample demonstrates how to give an agent file-based memory using the `FileMemoryProvider`.
|
||||
|
||||
The `FileMemoryProvider` is an `AIContextProvider` that exposes a set of memory tools to the agent, allowing the agent to decide what to remember and when to recall it. Each memory is stored as an individual file in an `AgentFileStore`, so memories survive beyond the lifetime of a single conversation.
|
||||
|
||||
## Concepts
|
||||
|
||||
- **`FileMemoryProvider`**: An `AIContextProvider` that adds the following tools to the agent:
|
||||
|
||||
| Tool | Description |
|
||||
|---|---|
|
||||
| `file_memory_write` | Write a memory file with a name, content and optional description. |
|
||||
| `file_memory_read` | Read the content of a memory file by name. |
|
||||
| `file_memory_delete` | Delete a memory file by name. |
|
||||
| `file_memory_ls` | List all memory files with their descriptions. |
|
||||
| `file_memory_grep` | Search memory file contents using a regular expression. |
|
||||
| `file_memory_replace` | Replace occurrences of a substring within a memory file. |
|
||||
| `file_memory_replace_lines` | Replace whole lines within a memory file. |
|
||||
|
||||
The provider also maintains a `memories.md` index file, which it injects into the conversation so the agent knows which memories are available without having to list them first.
|
||||
|
||||
- **`AgentFileStore`**: The pluggable storage abstraction used by the provider. This sample uses `FileSystemAgentFileStore` to store memories on the local disk, but `InMemoryAgentFileStore` or a custom implementation (e.g. backed by blob storage) can be used instead.
|
||||
|
||||
- **`FileMemoryState`**: The per-session state of the provider. Its `WorkingFolder` property determines the folder, relative to the store root, that memory files are written to.
|
||||
|
||||
## Configuring the memory folder
|
||||
|
||||
By default, all sessions share the root folder of the store, which means every session reads and writes the same flat set of memory files.
|
||||
|
||||
To scope memories, e.g. per user, per tenant or per session, pass a state initializer callback to the `FileMemoryProvider` constructor. The callback receives the `AgentSession` and is invoked whenever the provider cannot find existing state in that session, i.e. typically the first time the provider is used with a new session:
|
||||
|
||||
```csharp
|
||||
using var fileMemoryProvider = new FileMemoryProvider(
|
||||
fileStore,
|
||||
session => new FileMemoryState { WorkingFolder = $"users/{userId}" });
|
||||
```
|
||||
|
||||
In this sample, memories are written to `agent-memory/users/UID1` under the application's base directory. Because the folder is derived from a fixed user id rather than the session, a new session for the same user picks up the memories written by earlier sessions.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)
|
||||
- A Microsoft Foundry project with a chat model deployment
|
||||
- Run `az login` to authenticate with `DefaultAzureCredential`
|
||||
|
||||
## Configuration
|
||||
|
||||
Set the following environment variables:
|
||||
|
||||
| Variable | Description | Default |
|
||||
|---|---|---|
|
||||
| `FOUNDRY_PROJECT_ENDPOINT` | Your Foundry project endpoint | *(required)* |
|
||||
| `FOUNDRY_MODEL` | Chat model deployment name | `gpt-5.4-mini` |
|
||||
|
||||
## Running the Sample
|
||||
|
||||
```bash
|
||||
dotnet run
|
||||
```
|
||||
|
||||
## How it Works
|
||||
|
||||
1. A `FileSystemAgentFileStore` is created, rooted at a local `agent-memory` folder.
|
||||
2. A `FileMemoryProvider` is created over that store, with a state initializer that puts the memories for the current user in their own working folder.
|
||||
3. The provider is attached to the agent via `ChatClientAgentOptions.AIContextProviders`, which gives the agent the `file_memory_*` tools and instructions for using them.
|
||||
4. In the first conversation, the user shares some preferences and the agent calls `file_memory_write` to store them as a file in the working folder. The sample then lists the files that were created on disk.
|
||||
5. In the second conversation, a brand new session is created with no chat history from the first conversation. The provider injects the memory index into the conversation, and the agent calls `file_memory_read` to recall the stored preferences when making its recommendations.
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="CommunityToolkit.VectorData.CosmosNoSql" />
|
||||
<PackageReference Include="Microsoft.Azure.Cosmos" />
|
||||
<PackageReference Include="Newtonsoft.Json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
-92
@@ -1,92 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample shows how to persist chat history in Azure Cosmos DB for NoSQL using the ChatHistoryMemoryProvider.
|
||||
// The agent can then use chat history from prior conversations to inform responses in new conversations.
|
||||
|
||||
using System.Text.Json;
|
||||
using Azure.AI.Projects;
|
||||
using Azure.Identity;
|
||||
using CommunityToolkit.VectorData.CosmosNoSql;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Azure.Cosmos;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.VectorData;
|
||||
|
||||
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
|
||||
var embeddingDeploymentName = Environment.GetEnvironmentVariable("FOUNDRY_EMBEDDING_MODEL") ?? "text-embedding-3-large";
|
||||
var embeddingDimensions = 3072;
|
||||
if (Environment.GetEnvironmentVariable("FOUNDRY_EMBEDDING_DIMENSIONS") is string embeddingDimensionsValue &&
|
||||
(!int.TryParse(embeddingDimensionsValue, out embeddingDimensions) || embeddingDimensions <= 0))
|
||||
{
|
||||
throw new InvalidOperationException("FOUNDRY_EMBEDDING_DIMENSIONS must be a positive integer.");
|
||||
}
|
||||
var cosmosEndpoint = Environment.GetEnvironmentVariable("COSMOS_ENDPOINT") ?? throw new InvalidOperationException("COSMOS_ENDPOINT is not set.");
|
||||
var cosmosDatabaseName = Environment.GetEnvironmentVariable("COSMOS_DATABASE_NAME") ?? "agent-memory";
|
||||
|
||||
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
||||
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
DefaultAzureCredential credential = new();
|
||||
AIProjectClient aiProjectClient = new(new Uri(endpoint), credential);
|
||||
|
||||
using CosmosClient cosmosClient = new(
|
||||
cosmosEndpoint,
|
||||
credential,
|
||||
new CosmosClientOptions
|
||||
{
|
||||
UseSystemTextJsonSerializerWithOptions = JsonSerializerOptions.Default,
|
||||
});
|
||||
|
||||
DatabaseResponse databaseResponse = await cosmosClient.CreateDatabaseIfNotExistsAsync(cosmosDatabaseName);
|
||||
|
||||
VectorStore vectorStore = new CosmosNoSqlVectorStore(
|
||||
databaseResponse.Database,
|
||||
new CosmosNoSqlVectorStoreOptions
|
||||
{
|
||||
JsonSerializerOptions = JsonSerializerOptions.Default,
|
||||
EmbeddingGenerator = aiProjectClient
|
||||
.GetProjectOpenAIClient()
|
||||
.GetEmbeddingClient(embeddingDeploymentName)
|
||||
.AsIEmbeddingGenerator(),
|
||||
});
|
||||
|
||||
var userId = $"sample-{Guid.NewGuid():N}";
|
||||
|
||||
// Create the agent and add the ChatHistoryMemoryProvider to store chat messages in Cosmos DB.
|
||||
AIAgent agent = aiProjectClient
|
||||
.AsAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
ChatOptions = new() { ModelId = deploymentName, Instructions = "You are good at telling jokes." },
|
||||
Name = "Joker",
|
||||
AIContextProviders = [new ChatHistoryMemoryProvider(
|
||||
vectorStore,
|
||||
collectionName: "chathistory",
|
||||
vectorDimensions: embeddingDimensions,
|
||||
// Callback to configure the initial state of the ChatHistoryMemoryProvider.
|
||||
// The ChatHistoryMemoryProvider stores its state in the AgentSession and this callback
|
||||
// will be called whenever the ChatHistoryMemoryProvider cannot find existing state in the session,
|
||||
// typically the first time it is used with a new session.
|
||||
_ => new ChatHistoryMemoryProvider.State(
|
||||
// Configure the scope values under which chat messages will be stored.
|
||||
// In this case, we are using a per-run user ID and a unique session ID for each new session.
|
||||
storageScope: new() { UserId = userId, SessionId = Guid.NewGuid().ToString("N") },
|
||||
// Configure the scope which would be used to search for relevant prior messages.
|
||||
// In this case, we are searching for any messages for the user across all sessions.
|
||||
searchScope: new() { UserId = userId }))]
|
||||
});
|
||||
|
||||
// Start a new session for the agent conversation.
|
||||
AgentSession session = await agent.CreateSessionAsync();
|
||||
|
||||
// Run the agent with the session that stores conversation history in Cosmos DB.
|
||||
Console.WriteLine("First session:");
|
||||
Console.WriteLine(await agent.RunAsync("I like jokes about Pirates. Tell me a joke about a pirate.", session));
|
||||
|
||||
// Start a second session. Since we configured the search scope to be across all sessions for the user,
|
||||
// the agent should remember that the user likes pirate jokes.
|
||||
AgentSession session2 = await agent.CreateSessionAsync();
|
||||
|
||||
// Run the agent with the second session.
|
||||
Console.WriteLine("Second session (recalling prior chat history from Cosmos DB):");
|
||||
Console.WriteLine(await agent.RunAsync("Tell me a joke that I might like.", session2));
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
# Agent with Memory Using Azure Cosmos DB for NoSQL
|
||||
|
||||
This sample uses `ChatHistoryMemoryProvider` with `CosmosNoSqlVectorStore` to persist chat history in Azure Cosmos DB for NoSQL and recall relevant messages in a new agent session.
|
||||
|
||||
## Features Demonstrated
|
||||
|
||||
- Authenticating to Microsoft Foundry and Azure Cosmos DB with `DefaultAzureCredential`
|
||||
- Storing chat messages in an Azure Cosmos DB vector store
|
||||
- Creating the configured database and chat-history container when they do not exist
|
||||
- Recalling relevant chat history across agent sessions
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)
|
||||
2. A Microsoft Foundry project with:
|
||||
- A chat model deployment (the default is `gpt-5.4-mini`)
|
||||
- A `text-embedding-3-large` deployment with 3,072 dimensions
|
||||
3. An Azure Cosmos DB for NoSQL account with [vector search enabled](https://learn.microsoft.com/azure/cosmos-db/nosql/vector-search)
|
||||
4. An Azure identity that can create the configured database and container and read and write items
|
||||
5. Azure CLI authentication (`az login`)
|
||||
|
||||
## Configuration
|
||||
|
||||
Set the following environment variables:
|
||||
|
||||
| Variable | Description | Default |
|
||||
|---|---|---|
|
||||
| `FOUNDRY_PROJECT_ENDPOINT` | Microsoft Foundry project endpoint | *(required)* |
|
||||
| `COSMOS_ENDPOINT` | Azure Cosmos DB account endpoint | *(required)* |
|
||||
| `FOUNDRY_MODEL` | Chat model deployment name | `gpt-5.4-mini` |
|
||||
| `FOUNDRY_EMBEDDING_MODEL` | Embedding model deployment name | `text-embedding-3-large` |
|
||||
| `FOUNDRY_EMBEDDING_DIMENSIONS` | Number of dimensions produced by the embedding deployment | `3072` |
|
||||
| `COSMOS_DATABASE_NAME` | Database used to store agent memory | `agent-memory` |
|
||||
|
||||
## Run the Sample
|
||||
|
||||
```bash
|
||||
dotnet run
|
||||
```
|
||||
|
||||
The first session stores the user's preference for pirate jokes. The second session uses a different `AgentSession` but the same per-run user search scope, allowing the agent to retrieve that preference from Azure Cosmos DB without recalling data from earlier sample runs.
|
||||
@@ -10,8 +10,6 @@ These samples show how to create an agent with the Agent Framework that uses Mem
|
||||
|[Memory with Microsoft Foundry](./AgentWithMemory_Step04_MemoryUsingFoundry/)|This sample demonstrates how to create and run an agent that uses Microsoft Foundry's managed memory service to extract and retrieve individual memories.|
|
||||
|[Bounded Chat History with Overflow](./AgentWithMemory_Step05_BoundedChatHistory/)|This sample demonstrates how to create a bounded chat history provider that overflows older messages to a vector store and recalls them as memories.|
|
||||
|[Memory Using AgentMemory](./AgentWithMemory_Step06_MemoryUsingAgentMemory/)|This sample demonstrates a retail shopping assistant built with [`AgentMemory`](https://www.nuget.org/packages/AgentMemory), an unofficial .NET port of the Neo4j Labs graph-memory provider, to learn customer preferences and recommend products via graph traversal.|
|
||||
|[File Based Memory](./AgentWithMemory_Step07_FileMemoryProvider/)|This sample demonstrates how to use the `FileMemoryProvider` to give an agent tools for storing and recalling memories as files, and how to configure the folder that those memory files are written to.|
|
||||
|[Memory with Azure Cosmos DB for NoSQL](./AgentWithMemory_Step08_MemoryUsingCosmosNoSql/)|This sample demonstrates how to persist and retrieve chat history across sessions with Azure Cosmos DB for NoSQL.|
|
||||
|
||||
> **See also**: [Memory Search with Foundry Agents](../AgentProviders/foundry/Agent_Step22_MemorySearch/) - demonstrates using the built-in Memory Search tool with Microsoft Foundry agents.
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ TextSearchStore textSearchStore = new(vectorStore, "product-and-policy-info", 30
|
||||
await textSearchStore.UpsertDocumentsAsync(GetSampleDocuments());
|
||||
|
||||
// Create an adapter function that the TextSearchProvider can use to run searches against the TextSearchStore.
|
||||
async Task<IEnumerable<TextSearchProvider.TextSearchResult>> SearchAdapterAsync(string text, CancellationToken ct)
|
||||
Func<string, CancellationToken, Task<IEnumerable<TextSearchProvider.TextSearchResult>>> SearchAdapter = async (text, ct) =>
|
||||
{
|
||||
// Here we are limiting the search results to the single top result to demonstrate that we are accurately matching
|
||||
// specific search results for each question, but in a real world case, more results should be used.
|
||||
@@ -49,7 +49,7 @@ async Task<IEnumerable<TextSearchProvider.TextSearchResult>> SearchAdapterAsync(
|
||||
Text = r.Text ?? string.Empty,
|
||||
RawRepresentation = r
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Configure the options for the TextSearchProvider.
|
||||
TextSearchProviderOptions textSearchOptions = new()
|
||||
@@ -63,7 +63,7 @@ AIAgent agent = aiProjectClient
|
||||
.AsAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
ChatOptions = new() { ModelId = deploymentName, Instructions = "You are a helpful support specialist for Contoso Outdoors. Answer questions using the provided context and cite the source document when available." },
|
||||
AIContextProviders = [new TextSearchProvider(SearchAdapterAsync, textSearchOptions)],
|
||||
AIContextProviders = [new TextSearchProvider(SearchAdapter, textSearchOptions)],
|
||||
// Since we are using ChatCompletion which stores chat history locally, we can also add a message filter
|
||||
// that removes messages produced by the TextSearchProvider before they are added to the chat history, so that
|
||||
// we don't bloat chat history with all the search result messages.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user