Compare commits

..
Author SHA1 Message Date
Tao Chen 565933a829 Fix more workflow samples 2026-03-30 17:15:57 -07:00
Tao Chen bfd2f671d5 Fix workflows HIL sample 2026-03-30 11:56:44 -07:00
Tao Chen 120a9f4c42 Increase idle timeout 2026-03-30 09:41:41 -07:00
Tao Chen c305017b4e Merge branch 'main' into local-branch-fix-samples-and-sample-validation 2026-03-30 08:15:48 -07:00
Tao Chen 6c44d04950 Correct env vars 2026-03-25 14:03:53 -07:00
Tao Chen f74b527f78 Fix workflow dependency 2026-03-25 11:39:13 -07:00
Tao Chen f7831be114 Fix workflow samples 2026-03-25 11:35:31 -07:00
Tao Chen e6a86b4d87 Fix sample paths 2026-03-25 11:13:05 -07:00
Tao Chen 5f68216863 First samples 1st batch 2026-03-25 11:00:17 -07:00
142 changed files with 1085 additions and 3775 deletions
@@ -34,7 +34,7 @@ runs:
- name: Test Copilot CLI - name: Test Copilot CLI
shell: bash shell: bash
run: copilot -p "What can you do in one sentence?" run: copilot --version && copilot -p "What can you do in one sentence?"
- name: Azure CLI Login - name: Azure CLI Login
uses: azure/login@v2 uses: azure/login@v2
+29 -94
View File
@@ -67,11 +67,13 @@ jobs:
# Azure AI configuration # Azure AI configuration
AZURE_AI_PROJECT_ENDPOINT: ${{ vars.AZURE_AI_PROJECT_ENDPOINT }} AZURE_AI_PROJECT_ENDPOINT: ${{ vars.AZURE_AI_PROJECT_ENDPOINT }}
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }} AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
# Azure OpenAI configuration # Azure OpenAI configuration
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }} AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }} AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }} AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__EMBEDDINGDEPLOYMENTNAME }} AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME }}
# OpenAI configuration # OpenAI configuration
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }} OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }} OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }}
@@ -97,6 +99,8 @@ jobs:
- name: Create .env for samples - name: Create .env for samples
run: | run: |
echo "FOUNDRY_PROJECT_ENDPOINT=$FOUNDRY_PROJECT_ENDPOINT" >> .env
echo "FOUNDRY_MODEL=$FOUNDRY_MODEL" >> .env
echo "AZURE_AI_PROJECT_ENDPOINT=$AZURE_AI_PROJECT_ENDPOINT" >> .env echo "AZURE_AI_PROJECT_ENDPOINT=$AZURE_AI_PROJECT_ENDPOINT" >> .env
echo "AZURE_AI_MODEL_DEPLOYMENT_NAME=$AZURE_AI_MODEL_DEPLOYMENT_NAME" >> .env echo "AZURE_AI_MODEL_DEPLOYMENT_NAME=$AZURE_AI_MODEL_DEPLOYMENT_NAME" >> .env
echo "AZURE_OPENAI_ENDPOINT=$AZURE_OPENAI_ENDPOINT" >> .env echo "AZURE_OPENAI_ENDPOINT=$AZURE_OPENAI_ENDPOINT" >> .env
@@ -125,6 +129,7 @@ jobs:
environment: integration environment: integration
env: env:
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }} OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
OPENAI_MODEL: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }} OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_RESPONSES_MODEL_ID: ${{ vars.OPENAI__RESPONSESMODELID }} OPENAI_RESPONSES_MODEL_ID: ${{ vars.OPENAI__RESPONSESMODELID }}
defaults: defaults:
@@ -144,6 +149,7 @@ jobs:
- name: Create .env for samples - name: Create .env for samples
run: | run: |
echo "OPENAI_API_KEY=$OPENAI_API_KEY" >> .env echo "OPENAI_API_KEY=$OPENAI_API_KEY" >> .env
echo "OPENAI_MODEL=$OPENAI_MODEL" >> .env
echo "OPENAI_CHAT_MODEL_ID=$OPENAI_CHAT_MODEL_ID" >> .env echo "OPENAI_CHAT_MODEL_ID=$OPENAI_CHAT_MODEL_ID" >> .env
echo "OPENAI_RESPONSES_MODEL_ID=$OPENAI_RESPONSES_MODEL_ID" >> .env echo "OPENAI_RESPONSES_MODEL_ID=$OPENAI_RESPONSES_MODEL_ID" >> .env
@@ -158,8 +164,8 @@ jobs:
name: validation-report-02-agents-openai name: validation-report-02-agents-openai
path: python/samples/sample_validation/reports/ path: python/samples/sample_validation/reports/
validate-02-agents-azure-openai: validate-02-agents-azure:
name: Validate 02-agents/providers/azure_openai name: Validate 02-agents/providers/azure
runs-on: ubuntu-latest runs-on: ubuntu-latest
environment: integration environment: integration
env: env:
@@ -190,93 +196,13 @@ jobs:
- name: Run sample validation - name: Run sample validation
run: | run: |
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/azure_openai --save-report --report-name 02-agents-azure-openai cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/azure --save-report --report-name 02-agents-azure
- name: Upload validation report - name: Upload validation report
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v7
if: always() if: always()
with: with:
name: validation-report-02-agents-azure-openai name: validation-report-02-agents-azure
path: python/samples/sample_validation/reports/
validate-02-agents-azure-ai:
name: Validate 02-agents/providers/azure_ai
runs-on: ubuntu-latest
environment: integration
env:
AZURE_AI_PROJECT_ENDPOINT: ${{ vars.AZURE_AI_PROJECT_ENDPOINT }}
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
AZURE_AI_CHAT_MODEL_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
AZURE_AI_EMBEDDING_MODEL_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__EMBEDDINGDEPLOYMENTNAME }}
BING_CONNECTION_ID: ${{ secrets.BING_CONNECTION_ID }}
defaults:
run:
working-directory: python
steps:
- uses: actions/checkout@v6
- 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 "AZURE_AI_PROJECT_ENDPOINT=$AZURE_AI_PROJECT_ENDPOINT" >> .env
echo "AZURE_AI_MODEL_DEPLOYMENT_NAME=$AZURE_AI_MODEL_DEPLOYMENT_NAME" >> .env
echo "AZURE_AI_CHAT_MODEL_DEPLOYMENT_NAME=$AZURE_AI_CHAT_MODEL_DEPLOYMENT_NAME" >> .env
echo "AZURE_AI_EMBEDDING_MODEL_DEPLOYMENT_NAME=$AZURE_AI_EMBEDDING_MODEL_DEPLOYMENT_NAME" >> .env
echo "BING_CONNECTION_ID=$BING_CONNECTION_ID" >> .env
- name: Run sample validation
run: |
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/azure_ai --save-report --report-name 02-agents-azure-ai
- name: Upload validation report
uses: actions/upload-artifact@v7
if: always()
with:
name: validation-report-02-agents-azure-ai
path: python/samples/sample_validation/reports/
validate-02-agents-azure-ai-agent:
name: Validate 02-agents/providers/azure_ai_agent
runs-on: ubuntu-latest
environment: integration
env:
AZURE_AI_PROJECT_ENDPOINT: ${{ vars.AZURE_AI_PROJECT_ENDPOINT }}
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
defaults:
run:
working-directory: python
steps:
- uses: actions/checkout@v6
- 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 "AZURE_AI_PROJECT_ENDPOINT=$AZURE_AI_PROJECT_ENDPOINT" >> .env
echo "AZURE_AI_MODEL_DEPLOYMENT_NAME=$AZURE_AI_MODEL_DEPLOYMENT_NAME" >> .env
- name: Run sample validation
run: |
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/azure_ai_agent --save-report --report-name 02-agents-azure-ai-agent
- name: Upload validation report
uses: actions/upload-artifact@v7
if: always()
with:
name: validation-report-02-agents-azure-ai-agent
path: python/samples/sample_validation/reports/ path: python/samples/sample_validation/reports/
validate-02-agents-anthropic: validate-02-agents-anthropic:
@@ -409,11 +335,13 @@ jobs:
name: validation-report-02-agents-ollama name: validation-report-02-agents-ollama
path: python/samples/sample_validation/reports/ path: python/samples/sample_validation/reports/
validate-02-agents-foundry-local: validate-02-agents-foundry:
name: Validate 02-agents/providers/foundry_local name: Validate 02-agents/providers/foundry
if: false # Temporarily disabled - requires local Foundry setup
runs-on: ubuntu-latest runs-on: ubuntu-latest
environment: integration environment: integration
env:
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
defaults: defaults:
run: run:
working-directory: python working-directory: python
@@ -428,15 +356,20 @@ jobs:
azure-subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} azure-subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
os: ${{ runner.os }} 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 - name: Run sample validation
run: | run: |
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/foundry_local --save-report --report-name 02-agents-foundry-local cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/foundry --save-report --report-name 02-agents-foundry
- name: Upload validation report - name: Upload validation report
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v7
if: always() if: always()
with: with:
name: validation-report-02-agents-foundry-local name: validation-report-02-agents-foundry
path: python/samples/sample_validation/reports/ path: python/samples/sample_validation/reports/
validate-02-agents-copilotstudio: validate-02-agents-copilotstudio:
@@ -516,6 +449,8 @@ jobs:
environment: integration environment: integration
env: env:
# Azure AI configuration # Azure AI configuration
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
AZURE_AI_PROJECT_ENDPOINT: ${{ vars.AZURE_AI_PROJECT_ENDPOINT }} AZURE_AI_PROJECT_ENDPOINT: ${{ vars.AZURE_AI_PROJECT_ENDPOINT }}
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }} AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
# Azure OpenAI configuration # Azure OpenAI configuration
@@ -538,6 +473,8 @@ jobs:
- name: Create .env for samples - name: Create .env for samples
run: | run: |
echo "FOUNDRY_PROJECT_ENDPOINT=$FOUNDRY_PROJECT_ENDPOINT" >> .env
echo "FOUNDRY_MODEL=$FOUNDRY_MODEL" >> .env
echo "AZURE_AI_PROJECT_ENDPOINT=$AZURE_AI_PROJECT_ENDPOINT" >> .env echo "AZURE_AI_PROJECT_ENDPOINT=$AZURE_AI_PROJECT_ENDPOINT" >> .env
echo "AZURE_AI_MODEL_DEPLOYMENT_NAME=$AZURE_AI_MODEL_DEPLOYMENT_NAME" >> .env echo "AZURE_AI_MODEL_DEPLOYMENT_NAME=$AZURE_AI_MODEL_DEPLOYMENT_NAME" >> .env
echo "AZURE_OPENAI_ENDPOINT=$AZURE_OPENAI_ENDPOINT" >> .env echo "AZURE_OPENAI_ENDPOINT=$AZURE_OPENAI_ENDPOINT" >> .env
@@ -759,14 +696,12 @@ jobs:
- validate-01-get-started - validate-01-get-started
- validate-02-agents - validate-02-agents
- validate-02-agents-openai - validate-02-agents-openai
- validate-02-agents-azure-openai - validate-02-agents-azure
- validate-02-agents-azure-ai
- validate-02-agents-azure-ai-agent
- validate-02-agents-anthropic - validate-02-agents-anthropic
- validate-02-agents-github-copilot - validate-02-agents-github-copilot
- validate-02-agents-amazon - validate-02-agents-amazon
- validate-02-agents-ollama - validate-02-agents-ollama
- validate-02-agents-foundry-local - validate-02-agents-foundry
- validate-02-agents-copilotstudio - validate-02-agents-copilotstudio
- validate-02-agents-custom - validate-02-agents-custom
- validate-03-workflows - validate-03-workflows
-31
View File
@@ -74,37 +74,6 @@ Contributions must maintain API signature and behavioral compatibility. Contribu
that include breaking changes will be rejected. Please file an issue to discuss that include breaking changes will be rejected. Please file an issue to discuss
your idea or change if you believe that a breaking change is warranted. your idea or change if you believe that a breaking change is warranted.
#### Automated API Compatibility Validation
The .NET projects use [Package Validation](https://learn.microsoft.com/dotnet/fundamentals/package-validation/overview)
to automatically detect API breaking changes. This validation runs during `dotnet build`
(Release configuration) and `dotnet pack`, comparing the current API surface against the
latest published NuGet baseline version.
**What gets validated:** By default, packable RC packages (`IsReleaseCandidate=true`) and
GA packages (`IsGenerallyAvailable=true`) that have a published NuGet baseline and do not
override validation settings are automatically validated. The shared baseline version and
default validation settings are defined in `dotnet/nuget/nuget-package.props`, but
individual projects may opt out (for example by setting `EnablePackageValidation=false`).
**If the build fails with CP errors (e.g., CP0001, CP0002):**
1. **Unintentional breaking change** — Refactor your code to maintain backward compatibility.
2. **Intentional breaking change** (approved by maintainers) — Generate a suppression file:
```bash
dotnet build <project>.csproj -c Release /p:ApiCompatGenerateSuppressionFile=true
```
This creates or updates a `CompatibilitySuppressions.xml` in the project directory.
Include this file in your PR with justification for the breaking change.
**After each release:**
1. Delete all `CompatibilitySuppressions.xml` files from validated projects.
2. Update `PackageValidationBaselineVersion` in `dotnet/nuget/nuget-package.props` to the
newly published version.
For more details, see the [Package Validation diagnostic IDs](https://learn.microsoft.com/dotnet/fundamentals/package-validation/diagnostic-ids).
### Suggested Workflow ### Suggested Workflow
We use and recommend the following workflow: We use and recommend the following workflow:
@@ -31,6 +31,8 @@ The persistence timing and `FunctionResultContent` trimming behaviors are interr
- **Per-run persistence**: When messages are batched and persisted at the end of the full run, trailing `FunctionResultContent` trimming becomes necessary to match the service's behavior. Without trimming, the stored history contains `FunctionResultContent` that the service would never have stored. - **Per-run persistence**: When messages are batched and persisted at the end of the full run, trailing `FunctionResultContent` trimming becomes necessary to match the service's behavior. Without trimming, the stored history contains `FunctionResultContent` that the service would never have stored.
This means the trimming feature (introduced in [PR #4792](https://github.com/microsoft/agent-framework/pull/4792)) is primarily needed as a complement to per-run persistence. The `PersistChatHistoryAtEndOfRun` setting (introduced in [PR #4762](https://github.com/microsoft/agent-framework/pull/4762)) inverts the default so that per-service-call persistence is the standard behavior, and per-run persistence is opt-in.
## Decision Drivers ## Decision Drivers
- **A. Consistency**: The default behavior of `ChatHistoryProvider` should produce stored history that closely matches what the underlying AI service would store, minimizing surprise when switching between framework-managed and service-managed chat history. - **A. Consistency**: The default behavior of `ChatHistoryProvider` should produce stored history that closely matches what the underlying AI service would store, minimizing surprise when switching between framework-managed and service-managed chat history.
@@ -41,30 +43,33 @@ The persistence timing and `FunctionResultContent` trimming behaviors are interr
## Considered Options ## Considered Options
- Option 1: Per-run persistence with opt-in FRC (FunctionResultContent) trimming - Option 1: Default to per-run persistence with `FunctionResultContent` trimming (opt-in to per-service-call)
- Option 2: Opt-in per-service-call persistence (via `SimulateServiceStoredChatHistory`) - Option 2: Default to per-service-call persistence (opt-in to per-run)
## Pros and Cons of the Options ## Pros and Cons of the Options
### Option 1: Per-run persistence with opt-in FRC trimming ### Option 1: Default to per-run persistence with `FunctionResultContent` trimming
Keep the current default behavior of persisting chat history only at the end of the full agent run. Add `FunctionResultContent` trimming as an opt-in behavior to improve consistency with service storage. Keep the current default behavior of persisting chat history only at the end of the full agent run. Add `FunctionResultContent` trimming as the default to improve consistency with service storage. Provide an opt-in setting for users who want per-service-call persistence.
Settings:
- `PersistChatHistoryAtEndOfRun` = `true`
- Good, because runs are atomic — chat history is only updated when the full run succeeds, satisfying driver B. - Good, because runs are atomic — chat history is only updated when the full run succeeds, satisfying driver B.
- Good, because the mental model is simple: one run = one history update, satisfying driver D. - Good, because the mental model is simple: one run = one history update, satisfying driver D.
- Good, because trimming trailing `FunctionResultContent` improves consistency with service storage, partially satisfying driver A. - Good, because trimming trailing `FunctionResultContent` improves consistency with service storage, partially satisfying driver A.
- Good, because users can opt in to per-service-call persistence for checkpointing/recovery scenarios, satisfying drivers C and E.
- Bad, because the default persistence timing still differs from the service's behavior (per-run vs. per-service-call), only partially satisfying driver A. - Bad, because the default persistence timing still differs from the service's behavior (per-run vs. per-service-call), only partially satisfying driver A.
- Bad, because if the process crashes mid-loop, all intermediate progress from the current run is lost, not satisfying driver C. - Bad, because if the process crashes mid-loop, all intermediate progress from the current run is lost, not satisfying driver C by default.
- Bad, because this option alone does not provide a way for users to opt into per-service-call persistence, not satisfying driver E.
### Option 2: Opt-in per-service-call persistence (via `SimulateServiceStoredChatHistory`) ### Option 2: Default to per-service-call persistence
Introduce an optional SimulateServiceStoredChatHistory setting to persist chat history after each individual service call within the FIC loop, matching the AI service's behavior. Trailing `FunctionResultContent` trimming is unnecessary with this approach (it is naturally handled). Change the default to persist chat history after each individual service call within the FIC loop, matching the AI service's behavior. Trailing `FunctionResultContent` trimming is unnecessary with this approach (it is naturally handled). Provide an opt-in setting for users who want per-run atomicity with trimming.
Settings: Settings:
- `SimulateServiceStoredChatHistory` = `true` - `PersistChatHistoryAtEndOfRun` = `false` (default)
- Good, because the stored history matches the service's behavior when opting in for both timing and content, fully satisfying driver A. - Good, because the stored history matches the service's behavior by default for both timing and content, fully satisfying driver A.
- Good, because intermediate progress is preserved if the process is interrupted, satisfying driver C. - Good, because intermediate progress is preserved if the process is interrupted, satisfying driver C.
- Good, because no separate `FunctionResultContent` trimming logic is needed, reducing complexity. - Good, because no separate `FunctionResultContent` trimming logic is needed, reducing complexity.
- Bad, because chat history may be left in an incomplete state if the run fails mid-loop (e.g., `FunctionCallContent` stored without corresponding `FunctionResultContent`), not satisfying driver B. A subsequent run cannot proceed without manually providing the missing `FunctionResultContent`. - Bad, because chat history may be left in an incomplete state if the run fails mid-loop (e.g., `FunctionCallContent` stored without corresponding `FunctionResultContent`), not satisfying driver B. A subsequent run cannot proceed without manually providing the missing `FunctionResultContent`.
@@ -73,36 +78,39 @@ Settings:
## Decision Outcome ## Decision Outcome
Chosen option: **Option 2: Opt-in per-service-call persistence (via `SimulateServiceStoredChatHistory`)**. The existing per-run persistence behavior is retained as-is, requiring no changes from users. Per-service-call persistence is available as an opt-in feature via the `SimulateServiceStoredChatHistory` setting. This satisfies drivers B (atomicity) and D (simplicity) for the common case, while fully satisfying driver A (consistency) for users who opt into simulated service-stored behavior. Users who need per-service-call persistence for recoverability (driver C) can enable it explicitly. Chosen option: **Option 2 — Default to per-service-call persistence**, because it fully satisfies the consistency driver (A), naturally handles `FunctionResultContent` trimming without additional logic, and provides better recoverability for long-running tool-calling loops. Per-run persistence remains available via the `PersistChatHistoryAtEndOfRun` setting for users who prefer atomic run semantics.
### Configuration Matrix ### Configuration Matrix
The behavior depends on the combination of `UseProvidedChatClientAsIs` and `SimulateServiceStoredChatHistory`: The behavior depends on the combination of `UseProvidedChatClientAsIs` and `PersistChatHistoryAtEndOfRun`:
| `UseProvidedChatClientAsIs` | `SimulateServiceStoredChatHistory` | Behavior | | `UseProvidedChatClientAsIs` | `PersistChatHistoryAtEndOfRun` | Behavior |
|---|---|---| |---|---|---|
| `false` (default) | `false` (default) | **Per-run persistence.** Messages are persisted at the end of the full agent run via the `ChatHistoryProvider`. | | `false` (default) | `false` (default) | **Per-service-call persistence.** A `ChatHistoryPersistingChatClient` middleware is automatically injected into the chat client pipeline between `FunctionInvokingChatClient` and the leaf `IChatClient`. Messages are persisted after each service call. |
| `false` | `true` | **Per-service-call persistence (simulated).** A `ServiceStoredSimulatingChatClient` middleware is automatically injected into the chat client pipeline between `FunctionInvokingChatClient` and the leaf `IChatClient`. Messages are persisted after each service call. A sentinel `ConversationId` causes FIC to treat the conversation as service-managed. | | `true` | `false` | **User responsibility.** No middleware is injected because the user has provided a custom chat client stack. The user is responsible for ensuring correct persistence behavior (e.g., by including their own persisting middleware). |
| `true` | `false` | **Per-run persistence.** No middleware is injected because the user has provided a custom chat client stack. Messages are persisted at the end of the run. | | `false` | `true` | **Per-run persistence with marking.** A `ChatHistoryPersistingChatClient` middleware is injected, but configured to *mark* messages with metadata rather than store them immediately. At the end of the run, marked messages are stored. Trailing `FunctionResultContent` is trimmed. |
| `true` | `true` | **User responsibility.** The system checks whether the custom chat client stack includes a `ServiceStoredSimulatingChatClient`. If not, a warning is emitted — the user is expected to have added their own per-service-call persistence mechanism. End-of-run persistence is skipped. | | `true` | `true` | **Per-run persistence with warning.** The system checks whether the custom chat client stack includes a `ChatHistoryPersistingChatClient`. If not, a warning is emitted (particularly relevant for workflow handoff scenarios where trimming cannot be guaranteed). If no `ChatHistoryPersistingChatClient` is preset, all messages are stored at the end of the run, otherwise marked messages are stored. |
### Consequences ### Consequences
- Good, because per-run persistence is atomic by default — chat history is only updated when the full run succeeds, satisfying driver B. - Good, because the stored history matches the service's behavior by default for both timing and content, fully satisfying consistency (driver A).
- Good, because the default mental model is simple: one run = one history update, satisfying driver D. - Good, because intermediate progress is preserved if the process is interrupted, satisfying recoverability (driver C).
- Good, because users who opt into `SimulateServiceStoredChatHistory` get stored history that matches the service's behavior for both timing and content, fully satisfying driver A. - Good, because no separate `FunctionResultContent` trimming logic is needed in the default path, reducing complexity.
- Good, because per-service-call persistence preserves intermediate progress if the process is interrupted, satisfying driver C when opted in. - Good, because marking persisted messages with metadata enables deduplication and aids debugging.
- Good, because no separate `FunctionResultContent` trimming logic is needed when per-service-call persistence is active — it is naturally handled. - Good, because warnings for custom chat client configurations without the persisting middleware help prevent silent failures in workflow handoff scenarios.
- Good, because conflict detection (configurable via `ThrowOnChatHistoryProviderConflict`, `WarnOnChatHistoryProviderConflict`, `ClearOnChatHistoryProviderConflict`) prevents misconfiguration when a service returns a `ConversationId` alongside a configured `ChatHistoryProvider`. - Bad, because chat history may be left in an incomplete state if the run fails mid-loop (e.g., `FunctionCallContent` stored without corresponding `FunctionResultContent`), requiring manual recovery in rare cases.
- Bad, because per-service-call persistence (when opted in) may leave chat history in an incomplete state if the run fails mid-loop (e.g., `FunctionCallContent` stored without corresponding `FunctionResultContent`), requiring manual recovery in rare cases. - Bad, because the mental model is more complex for the default path: a single run may produce multiple history updates.
- Neutral, because users who want per-service-call consistency can opt in via `SimulateServiceStoredChatHistory = true`, satisfying driver E. - Neutral, because users who prefer atomic run semantics can opt in to per-run persistence via `PersistChatHistoryAtEndOfRun = true`.
- Neutral, because increased write frequency from per-service-call persistence may impact performance for some storage backends; this can be mitigated with a caching decorator. - Neutral, because increased write frequency from per-service-call persistence may impact performance for some storage backends; this can be mitigated with a caching decorator.
### Implementation Notes ### Implementation Notes
#### Conversation ID Consistency #### Conversation ID Consistency
We should introduce a separate `ConversationIdPersistingChatClient`, middleware which allows us to The `ChatHistoryPersistingChatClient` middleware must also update the session's `ConversationId` consistently for both response-based and conversation-based service interactions, ensuring the session always reflects the latest service-provided identifier.
persist response `ConversationIds` during the FICC loop. This could be used with or without
`ServiceStoredSimulatingChatClient`.
## More Information
- [PR #4762: Persist messages during function call loop](https://github.com/microsoft/agent-framework/pull/4762) — introduces `PersistChatHistoryAfterEachServiceCall` option and `ChatHistoryPersistingChatClient` decorator
- [PR #4792: Trim final FRC to match service storage](https://github.com/microsoft/agent-framework/pull/4792) — introduces `StoreFinalFunctionResultContent` option and `FilterFinalFunctionResultContent` logic
- [Issue #2889](https://github.com/microsoft/agent-framework/issues/2889) — original issue tracking chat history persistence during function call loops
-1
View File
@@ -17,7 +17,6 @@
<PropertyGroup> <PropertyGroup>
<IsReleaseCandidate>false</IsReleaseCandidate> <IsReleaseCandidate>false</IsReleaseCandidate>
<IsGenerallyAvailable>false</IsGenerallyAvailable>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
-1
View File
@@ -105,7 +105,6 @@
<Folder Name="/Samples/02-agents/AgentSkills/"> <Folder Name="/Samples/02-agents/AgentSkills/">
<File Path="samples/02-agents/AgentSkills/README.md" /> <File Path="samples/02-agents/AgentSkills/README.md" />
<Project Path="samples/02-agents/AgentSkills/Agent_Step01_FileBasedSkills/Agent_Step01_FileBasedSkills.csproj" /> <Project Path="samples/02-agents/AgentSkills/Agent_Step01_FileBasedSkills/Agent_Step01_FileBasedSkills.csproj" />
<Project Path="samples/02-agents/AgentSkills/Agent_Step02_CodeDefinedSkills/Agent_Step02_CodeDefinedSkills.csproj" />
</Folder> </Folder>
<Folder Name="/Samples/02-agents/AGUI/Step05_StateManagement/"> <Folder Name="/Samples/02-agents/AGUI/Step05_StateManagement/">
<Project Path="samples/02-agents/AGUI/Step05_StateManagement/Client/Client.csproj" /> <Project Path="samples/02-agents/AGUI/Step05_StateManagement/Client/Client.csproj" />
+1 -3
View File
@@ -12,9 +12,7 @@
<IsPackable>true</IsPackable> <IsPackable>true</IsPackable>
<!-- Package validation. Baseline Version should be the latest version available on NuGet. --> <!-- Package validation. Baseline Version should be the latest version available on NuGet. -->
<PackageValidationBaselineVersion>1.0.0-rc4</PackageValidationBaselineVersion> <PackageValidationBaselineVersion>0.0.1</PackageValidationBaselineVersion>
<!-- Enable validation for RC packages and GA packages -->
<EnablePackageValidation Condition="'$(IsReleaseCandidate)' == 'true' OR '$(IsGenerallyAvailable)' == 'true'">true</EnablePackageValidation>
<!-- Validate assembly attributes only for Publish builds --> <!-- Validate assembly attributes only for Publish builds -->
<NoWarn Condition="'$(Configuration)' != 'Publish'">$(NoWarn);CP0003</NoWarn> <NoWarn Condition="'$(Configuration)' != 'Publish'">$(NoWarn);CP0003</NoWarn>
<!-- Do not validate reference assemblies --> <!-- Do not validate reference assemblies -->
@@ -1,21 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<NoWarn>$(NoWarn);MAAI001</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
</Project>
@@ -1,90 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample demonstrates how to define Agent Skills entirely in code using AgentInlineSkill.
// No SKILL.md files are needed — skills, resources, and scripts are all defined programmatically.
//
// Three approaches are shown using a unit-converter skill:
// 1. Static resources — inline content provided via AddResource
// 2. Dynamic resources — computed at runtime via a factory delegate
// 3. Code scripts — executable delegates the agent can invoke directly
using System.Text.Json;
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using OpenAI.Responses;
// --- Configuration ---
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
// --- Build the code-defined skill ---
var unitConverterSkill = new AgentInlineSkill(
name: "unit-converter",
description: "Convert between common units using a multiplication factor. Use when asked to convert miles, kilometers, pounds, or kilograms.",
instructions: """
Use this skill when the user asks to convert between units.
1. Review the conversion-table resource to find the factor for the requested conversion.
2. Check the conversion-policy resource for rounding and formatting rules.
3. Use the convert script, passing the value and factor from the table.
""")
// 1. Static Resource: conversion tables
.AddResource(
"conversion-table",
"""
# Conversion Tables
Formula: **result = value Ă— factor**
| From | To | Factor |
|-------------|-------------|----------|
| miles | kilometers | 1.60934 |
| kilometers | miles | 0.621371 |
| pounds | kilograms | 0.453592 |
| kilograms | pounds | 2.20462 |
""")
// 2. Dynamic Resource: conversion policy (computed at runtime)
.AddResource("conversion-policy", () =>
{
const int Precision = 4;
return $"""
# Conversion Policy
**Decimal places:** {Precision}
**Format:** Always show both the original and converted values with units
**Generated at:** {DateTime.UtcNow:O}
""";
})
// 3. Code Script: convert
.AddScript("convert", (double value, double factor) =>
{
double result = Math.Round(value * factor, 4);
return JsonSerializer.Serialize(new { value, factor, result });
});
// --- Skills Provider ---
var skillsProvider = new AgentSkillsProvider(unitConverterSkill);
// --- Agent Setup ---
AIAgent agent = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
.GetResponsesClient()
.AsAIAgent(new ChatClientAgentOptions
{
Name = "UnitConverterAgent",
ChatOptions = new()
{
Instructions = "You are a helpful assistant that can convert units.",
},
AIContextProviders = [skillsProvider],
},
model: deploymentName);
// --- Example: Unit conversion ---
Console.WriteLine("Converting units with code-defined skills");
Console.WriteLine(new string('-', 60));
AgentResponse response = await agent.RunAsync(
"How many kilometers is a marathon (26.2 miles)? And how many pounds is 75 kilograms?");
Console.WriteLine($"Agent: {response.Text}");
@@ -1,52 +0,0 @@
# Code-Defined Agent Skills Sample
This sample demonstrates how to define **Agent Skills entirely in code** using `AgentInlineSkill`.
## What it demonstrates
- Creating skills programmatically with `AgentInlineSkill` — no SKILL.md files needed
- **Static resources** via `AddResource` with inline content
- **Dynamic resources** via `AddResource` with a factory delegate (computed at runtime)
- **Code scripts** via `AddScript` with a delegate handler
- Using the `AgentSkillsProvider` constructor with inline skills
## Skills Included
### unit-converter (code-defined)
Converts between common units using multiplication factors. Defined entirely in C# code:
- `conversion-table` — Static resource with factor table
- `conversion-policy` — Dynamic resource with formatting rules (generated at runtime)
- `convert` — Script that performs `value × factor` conversion
## Running the Sample
### Prerequisites
- .NET 10.0 SDK
- Azure OpenAI endpoint with a deployed model
### Setup
```bash
export AZURE_OPENAI_ENDPOINT="https://your-endpoint.openai.azure.com/"
export AZURE_OPENAI_DEPLOYMENT_NAME="gpt-4o-mini"
```
### Run
```bash
dotnet run
```
### Expected Output
```
Converting units with code-defined skills
------------------------------------------------------------
Agent: Here are your conversions:
1. **26.2 miles → 42.16 km** (a marathon distance)
2. **75 kg → 165.35 lbs**
```
+1 -18
View File
@@ -1,24 +1,7 @@
# AgentSkills Samples # AgentSkills Samples
Samples demonstrating Agent Skills capabilities. Each sample shows a different way to define and use skills. Samples demonstrating Agent Skills capabilities.
| Sample | Description | | Sample | Description |
|--------|-------------| |--------|-------------|
| [Agent_Step01_FileBasedSkills](Agent_Step01_FileBasedSkills/) | Define skills as `SKILL.md` files on disk with reference documents. Uses a unit-converter skill. | | [Agent_Step01_FileBasedSkills](Agent_Step01_FileBasedSkills/) | Define skills as `SKILL.md` files on disk with reference documents. Uses a unit-converter skill. |
| [Agent_Step02_CodeDefinedSkills](Agent_Step02_CodeDefinedSkills/) | Define skills entirely in C# code using `AgentInlineSkill`, with static/dynamic resources and scripts. |
## Key Concepts
### File-Based vs Code-Defined Skills
| Aspect | File-Based | Code-Defined |
|--------|-----------|--------------|
| Definition | `SKILL.md` files on disk | `AgentInlineSkill` instances in C# |
| Resources | All files in skill directory (filtered by extension) | `AddResource` (static value or delegate-backed) |
| Scripts | Supported via script executor delegate | `AddScript` delegates |
| Discovery | Automatic from directory path | Explicit via constructor |
| Dynamic content | No (static files only) | Yes (factory delegates) |
| Reusability | Copy skill directory | Inline or shared instances |
For single-source scenarios, use the `AgentSkillsProvider` constructors directly. To combine multiple skill types, use the `AgentSkillsProviderBuilder`.
@@ -16,7 +16,7 @@ using Qdrant.Client;
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini"; var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
var embeddingDeploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME") ?? "text-embedding-3-large"; var embeddingDeploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME") ?? "text-embedding-3-large";
var afOverviewUrl = "https://raw.githubusercontent.com/MicrosoftDocs/semantic-kernel-docs/refs/heads/main/agent-framework/overview/index.md"; var afOverviewUrl = "https://github.com/MicrosoftDocs/semantic-kernel-docs/blob/main/agent-framework/overview/agent-framework-overview.md";
var afMigrationUrl = "https://raw.githubusercontent.com/MicrosoftDocs/semantic-kernel-docs/refs/heads/main/agent-framework/migration-guide/from-semantic-kernel/index.md"; var afMigrationUrl = "https://raw.githubusercontent.com/MicrosoftDocs/semantic-kernel-docs/refs/heads/main/agent-framework/migration-guide/from-semantic-kernel/index.md";
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production. // WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
@@ -1,16 +1,15 @@
// Copyright (c) Microsoft. All rights reserved. // Copyright (c) Microsoft. All rights reserved.
// This sample demonstrates how the ChatClientAgent persists chat history after each individual // This sample demonstrates how the ChatClientAgent persists chat history after each individual
// call to the AI service, using the SimulateServiceStoredChatHistory option. // call to the AI service.
// When an agent uses tools, FunctionInvokingChatClient may loop multiple times // When an agent uses tools, FunctionInvokingChatClient may loop multiple times
// (service call → tool execution → service call), and intermediate messages (tool calls and // (service call → tool execution → service call), and intermediate messages (tool calls and
// results) are persisted after each service call. This allows you to inspect or recover them // results) are persisted after each service call. This allows you to inspect or recover them
// even if the process is interrupted mid-loop, but may also result in chat history that is not // even if the process is interrupted mid-loop, but may also result in chat history that is not
// yet finalized (e.g., tool calls without results) being persisted, which may be undesirable in some cases. // yet finalized (e.g., tool calls without results) being persisted, which may be undesirable in some cases.
// //
// To use end-of-run persistence instead (atomic run semantics), remove the // To opt into end-of-run persistence instead (atomic run semantics), set
// SimulateServiceStoredChatHistory = true setting (or set it to false). End-of-run // PersistChatHistoryAtEndOfRun = true on ChatClientAgentOptions.
// persistence is the default behavior.
// //
// The sample runs two multi-turn conversations: one using non-streaming (RunAsync) and one // The sample runs two multi-turn conversations: one using non-streaming (RunAsync) and one
// using streaming (RunStreamingAsync), to demonstrate correct behavior in both modes. // using streaming (RunStreamingAsync), to demonstrate correct behavior in both modes.
@@ -54,7 +53,7 @@ static string GetTime([Description("The city name.")] string city) =>
_ => $"{city}: time data not available." _ => $"{city}: time data not available."
}; };
// Create the agent — per-service-call persistence is enabled via SimulateServiceStoredChatHistory. // Create the agent — per-service-call persistence is the default behavior.
// The in-memory ChatHistoryProvider is used by default when the service does not require service stored chat // The in-memory ChatHistoryProvider is used by default when the service does not require service stored chat
// history, so for those cases, we can inspect the chat history via session.TryGetInMemoryChatHistory(). // history, so for those cases, we can inspect the chat history via session.TryGetInMemoryChatHistory().
IChatClient chatClient = string.Equals(store, "TRUE", StringComparison.OrdinalIgnoreCase) ? IChatClient chatClient = string.Equals(store, "TRUE", StringComparison.OrdinalIgnoreCase) ?
@@ -64,7 +63,6 @@ AIAgent agent = chatClient.AsAIAgent(
new ChatClientAgentOptions new ChatClientAgentOptions
{ {
Name = "WeatherAssistant", Name = "WeatherAssistant",
SimulateServiceStoredChatHistory = true,
ChatOptions = new() ChatOptions = new()
{ {
Instructions = "You are a helpful assistant. When asked about multiple cities, call the appropriate tool for each city.", Instructions = "You are a helpful assistant. When asked about multiple cities, call the appropriate tool for each city.",
@@ -1,19 +1,16 @@
# In-Function-Loop Checkpointing # In-Function-Loop Checkpointing
This sample demonstrates how `ChatClientAgent` can persist chat history after each individual call to the AI service using the `SimulateServiceStoredChatHistory` option. This per-service-call persistence ensures intermediate progress is saved during the function invocation loop. This sample demonstrates how `ChatClientAgent` persists chat history after each individual call to the AI service by default. This per-service-call persistence ensures intermediate progress is saved during the function invocation loop.
## What This Sample Shows ## What This Sample Shows
When an agent uses tools, the `FunctionInvokingChatClient` loops multiple times (service call → tool execution → service call → …). By enabling `SimulateServiceStoredChatHistory = true`, chat history is persisted after each service call via the `ServiceStoredSimulatingChatClient` decorator: When an agent uses tools, the `FunctionInvokingChatClient` loops multiple times (service call → tool execution → service call → …). By default, chat history is persisted after each service call via the `ChatHistoryPersistingChatClient` decorator:
- A `ServiceStoredSimulatingChatClient` decorator is inserted into the chat client pipeline - A `ChatHistoryPersistingChatClient` decorator is automatically inserted into the chat client pipeline
- Before each service call, the decorator loads history from the `ChatHistoryProvider` and prepends it to the request
- After each service call, the decorator notifies the `ChatHistoryProvider` (and any `AIContextProvider` instances) with the new messages - After each service call, the decorator notifies the `ChatHistoryProvider` (and any `AIContextProvider` instances) with the new messages
- Only **new** messages are sent to providers on each notification — messages that were already persisted in an earlier call within the same run are deduplicated automatically - Only **new** messages are sent to providers on each notification — messages that were already persisted in an earlier call within the same run are deduplicated automatically
By default (without `SimulateServiceStoredChatHistory`), chat history is persisted at the end of the full agent run instead. To use per-service-call persistence, set `SimulateServiceStoredChatHistory = true` on `ChatClientAgentOptions`. To opt into end-of-run persistence instead (atomic run semantics), set `PersistChatHistoryAtEndOfRun = true` on `ChatClientAgentOptions`. In that mode, the decorator marks messages with metadata rather than persisting them immediately, and `ChatClientAgent` persists only the marked messages at the end of the run.
With `SimulateServiceStoredChatHistory` = true, the behavior matches that of chat history stored in the underlying AI service exactly.
Per-service-call persistence is useful for: Per-service-call persistence is useful for:
- **Crash recovery** — if the process is interrupted mid-loop, the intermediate tool calls and results are already persisted - **Crash recovery** — if the process is interrupted mid-loop, the intermediate tool calls and results are already persisted
@@ -29,7 +26,7 @@ The sample asks the agent about the weather and time in three cities. The model
``` ```
ChatClientAgent ChatClientAgent
└─ FunctionInvokingChatClient (handles tool call loop) └─ FunctionInvokingChatClient (handles tool call loop)
└─ ServiceStoredSimulatingChatClient (persists after each service call) └─ ChatHistoryPersistingChatClient (persists after each service call)
└─ Leaf IChatClient (Azure OpenAI) └─ Leaf IChatClient (Azure OpenAI)
``` ```
@@ -1,284 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- https://learn.microsoft.com/dotnet/fundamentals/package-validation/diagnostic-ids -->
<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.AsAIAgent(Azure.AI.Projects.AIProjectClient,Azure.AI.Projects.OpenAI.AgentRecord,System.Collections.Generic.IList{Microsoft.Extensions.AI.AITool},System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider)</Target>
<Left>lib/net10.0/Microsoft.Agents.AI.AzureAI.dll</Left>
<Right>lib/net10.0/Microsoft.Agents.AI.AzureAI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.AsAIAgent(Azure.AI.Projects.AIProjectClient,Azure.AI.Projects.OpenAI.AgentReference,System.Collections.Generic.IList{Microsoft.Extensions.AI.AITool},System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider)</Target>
<Left>lib/net10.0/Microsoft.Agents.AI.AzureAI.dll</Left>
<Right>lib/net10.0/Microsoft.Agents.AI.AzureAI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.AsAIAgent(Azure.AI.Projects.AIProjectClient,Azure.AI.Projects.OpenAI.AgentVersion,System.Collections.Generic.IList{Microsoft.Extensions.AI.AITool},System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider)</Target>
<Left>lib/net10.0/Microsoft.Agents.AI.AzureAI.dll</Left>
<Right>lib/net10.0/Microsoft.Agents.AI.AzureAI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.CreateAIAgentAsync(Azure.AI.Projects.AIProjectClient,System.String,Azure.AI.Projects.AgentVersionCreationOptions,System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.Threading.CancellationToken)</Target>
<Left>lib/net10.0/Microsoft.Agents.AI.AzureAI.dll</Left>
<Right>lib/net10.0/Microsoft.Agents.AI.AzureAI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.CreateAIAgentAsync(Azure.AI.Projects.AIProjectClient,System.String,Microsoft.Agents.AI.ChatClientAgentOptions,System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider,System.Threading.CancellationToken)</Target>
<Left>lib/net10.0/Microsoft.Agents.AI.AzureAI.dll</Left>
<Right>lib/net10.0/Microsoft.Agents.AI.AzureAI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.CreateAIAgentAsync(Azure.AI.Projects.AIProjectClient,System.String,System.String,System.String,System.String,System.Collections.Generic.IList{Microsoft.Extensions.AI.AITool},System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider,System.Threading.CancellationToken)</Target>
<Left>lib/net10.0/Microsoft.Agents.AI.AzureAI.dll</Left>
<Right>lib/net10.0/Microsoft.Agents.AI.AzureAI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.GetAIAgentAsync(Azure.AI.Projects.AIProjectClient,Microsoft.Agents.AI.ChatClientAgentOptions,System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider,System.Threading.CancellationToken)</Target>
<Left>lib/net10.0/Microsoft.Agents.AI.AzureAI.dll</Left>
<Right>lib/net10.0/Microsoft.Agents.AI.AzureAI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.GetAIAgentAsync(Azure.AI.Projects.AIProjectClient,System.String,System.Collections.Generic.IList{Microsoft.Extensions.AI.AITool},System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider,System.Threading.CancellationToken)</Target>
<Left>lib/net10.0/Microsoft.Agents.AI.AzureAI.dll</Left>
<Right>lib/net10.0/Microsoft.Agents.AI.AzureAI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.AsAIAgent(Azure.AI.Projects.AIProjectClient,Azure.AI.Projects.OpenAI.AgentRecord,System.Collections.Generic.IList{Microsoft.Extensions.AI.AITool},System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider)</Target>
<Left>lib/net472/Microsoft.Agents.AI.AzureAI.dll</Left>
<Right>lib/net472/Microsoft.Agents.AI.AzureAI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.AsAIAgent(Azure.AI.Projects.AIProjectClient,Azure.AI.Projects.OpenAI.AgentReference,System.Collections.Generic.IList{Microsoft.Extensions.AI.AITool},System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider)</Target>
<Left>lib/net472/Microsoft.Agents.AI.AzureAI.dll</Left>
<Right>lib/net472/Microsoft.Agents.AI.AzureAI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.AsAIAgent(Azure.AI.Projects.AIProjectClient,Azure.AI.Projects.OpenAI.AgentVersion,System.Collections.Generic.IList{Microsoft.Extensions.AI.AITool},System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider)</Target>
<Left>lib/net472/Microsoft.Agents.AI.AzureAI.dll</Left>
<Right>lib/net472/Microsoft.Agents.AI.AzureAI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.CreateAIAgentAsync(Azure.AI.Projects.AIProjectClient,System.String,Azure.AI.Projects.AgentVersionCreationOptions,System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.Threading.CancellationToken)</Target>
<Left>lib/net472/Microsoft.Agents.AI.AzureAI.dll</Left>
<Right>lib/net472/Microsoft.Agents.AI.AzureAI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.CreateAIAgentAsync(Azure.AI.Projects.AIProjectClient,System.String,Microsoft.Agents.AI.ChatClientAgentOptions,System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider,System.Threading.CancellationToken)</Target>
<Left>lib/net472/Microsoft.Agents.AI.AzureAI.dll</Left>
<Right>lib/net472/Microsoft.Agents.AI.AzureAI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.CreateAIAgentAsync(Azure.AI.Projects.AIProjectClient,System.String,System.String,System.String,System.String,System.Collections.Generic.IList{Microsoft.Extensions.AI.AITool},System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider,System.Threading.CancellationToken)</Target>
<Left>lib/net472/Microsoft.Agents.AI.AzureAI.dll</Left>
<Right>lib/net472/Microsoft.Agents.AI.AzureAI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.GetAIAgentAsync(Azure.AI.Projects.AIProjectClient,Microsoft.Agents.AI.ChatClientAgentOptions,System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider,System.Threading.CancellationToken)</Target>
<Left>lib/net472/Microsoft.Agents.AI.AzureAI.dll</Left>
<Right>lib/net472/Microsoft.Agents.AI.AzureAI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.GetAIAgentAsync(Azure.AI.Projects.AIProjectClient,System.String,System.Collections.Generic.IList{Microsoft.Extensions.AI.AITool},System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider,System.Threading.CancellationToken)</Target>
<Left>lib/net472/Microsoft.Agents.AI.AzureAI.dll</Left>
<Right>lib/net472/Microsoft.Agents.AI.AzureAI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.AsAIAgent(Azure.AI.Projects.AIProjectClient,Azure.AI.Projects.OpenAI.AgentRecord,System.Collections.Generic.IList{Microsoft.Extensions.AI.AITool},System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider)</Target>
<Left>lib/net8.0/Microsoft.Agents.AI.AzureAI.dll</Left>
<Right>lib/net8.0/Microsoft.Agents.AI.AzureAI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.AsAIAgent(Azure.AI.Projects.AIProjectClient,Azure.AI.Projects.OpenAI.AgentReference,System.Collections.Generic.IList{Microsoft.Extensions.AI.AITool},System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider)</Target>
<Left>lib/net8.0/Microsoft.Agents.AI.AzureAI.dll</Left>
<Right>lib/net8.0/Microsoft.Agents.AI.AzureAI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.AsAIAgent(Azure.AI.Projects.AIProjectClient,Azure.AI.Projects.OpenAI.AgentVersion,System.Collections.Generic.IList{Microsoft.Extensions.AI.AITool},System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider)</Target>
<Left>lib/net8.0/Microsoft.Agents.AI.AzureAI.dll</Left>
<Right>lib/net8.0/Microsoft.Agents.AI.AzureAI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.CreateAIAgentAsync(Azure.AI.Projects.AIProjectClient,System.String,Azure.AI.Projects.AgentVersionCreationOptions,System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.Threading.CancellationToken)</Target>
<Left>lib/net8.0/Microsoft.Agents.AI.AzureAI.dll</Left>
<Right>lib/net8.0/Microsoft.Agents.AI.AzureAI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.CreateAIAgentAsync(Azure.AI.Projects.AIProjectClient,System.String,Microsoft.Agents.AI.ChatClientAgentOptions,System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider,System.Threading.CancellationToken)</Target>
<Left>lib/net8.0/Microsoft.Agents.AI.AzureAI.dll</Left>
<Right>lib/net8.0/Microsoft.Agents.AI.AzureAI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.CreateAIAgentAsync(Azure.AI.Projects.AIProjectClient,System.String,System.String,System.String,System.String,System.Collections.Generic.IList{Microsoft.Extensions.AI.AITool},System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider,System.Threading.CancellationToken)</Target>
<Left>lib/net8.0/Microsoft.Agents.AI.AzureAI.dll</Left>
<Right>lib/net8.0/Microsoft.Agents.AI.AzureAI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.GetAIAgentAsync(Azure.AI.Projects.AIProjectClient,Microsoft.Agents.AI.ChatClientAgentOptions,System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider,System.Threading.CancellationToken)</Target>
<Left>lib/net8.0/Microsoft.Agents.AI.AzureAI.dll</Left>
<Right>lib/net8.0/Microsoft.Agents.AI.AzureAI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.GetAIAgentAsync(Azure.AI.Projects.AIProjectClient,System.String,System.Collections.Generic.IList{Microsoft.Extensions.AI.AITool},System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider,System.Threading.CancellationToken)</Target>
<Left>lib/net8.0/Microsoft.Agents.AI.AzureAI.dll</Left>
<Right>lib/net8.0/Microsoft.Agents.AI.AzureAI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.AsAIAgent(Azure.AI.Projects.AIProjectClient,Azure.AI.Projects.OpenAI.AgentRecord,System.Collections.Generic.IList{Microsoft.Extensions.AI.AITool},System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider)</Target>
<Left>lib/net9.0/Microsoft.Agents.AI.AzureAI.dll</Left>
<Right>lib/net9.0/Microsoft.Agents.AI.AzureAI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.AsAIAgent(Azure.AI.Projects.AIProjectClient,Azure.AI.Projects.OpenAI.AgentReference,System.Collections.Generic.IList{Microsoft.Extensions.AI.AITool},System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider)</Target>
<Left>lib/net9.0/Microsoft.Agents.AI.AzureAI.dll</Left>
<Right>lib/net9.0/Microsoft.Agents.AI.AzureAI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.AsAIAgent(Azure.AI.Projects.AIProjectClient,Azure.AI.Projects.OpenAI.AgentVersion,System.Collections.Generic.IList{Microsoft.Extensions.AI.AITool},System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider)</Target>
<Left>lib/net9.0/Microsoft.Agents.AI.AzureAI.dll</Left>
<Right>lib/net9.0/Microsoft.Agents.AI.AzureAI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.CreateAIAgentAsync(Azure.AI.Projects.AIProjectClient,System.String,Azure.AI.Projects.AgentVersionCreationOptions,System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.Threading.CancellationToken)</Target>
<Left>lib/net9.0/Microsoft.Agents.AI.AzureAI.dll</Left>
<Right>lib/net9.0/Microsoft.Agents.AI.AzureAI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.CreateAIAgentAsync(Azure.AI.Projects.AIProjectClient,System.String,Microsoft.Agents.AI.ChatClientAgentOptions,System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider,System.Threading.CancellationToken)</Target>
<Left>lib/net9.0/Microsoft.Agents.AI.AzureAI.dll</Left>
<Right>lib/net9.0/Microsoft.Agents.AI.AzureAI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.CreateAIAgentAsync(Azure.AI.Projects.AIProjectClient,System.String,System.String,System.String,System.String,System.Collections.Generic.IList{Microsoft.Extensions.AI.AITool},System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider,System.Threading.CancellationToken)</Target>
<Left>lib/net9.0/Microsoft.Agents.AI.AzureAI.dll</Left>
<Right>lib/net9.0/Microsoft.Agents.AI.AzureAI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.GetAIAgentAsync(Azure.AI.Projects.AIProjectClient,Microsoft.Agents.AI.ChatClientAgentOptions,System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider,System.Threading.CancellationToken)</Target>
<Left>lib/net9.0/Microsoft.Agents.AI.AzureAI.dll</Left>
<Right>lib/net9.0/Microsoft.Agents.AI.AzureAI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.GetAIAgentAsync(Azure.AI.Projects.AIProjectClient,System.String,System.Collections.Generic.IList{Microsoft.Extensions.AI.AITool},System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider,System.Threading.CancellationToken)</Target>
<Left>lib/net9.0/Microsoft.Agents.AI.AzureAI.dll</Left>
<Right>lib/net9.0/Microsoft.Agents.AI.AzureAI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.AsAIAgent(Azure.AI.Projects.AIProjectClient,Azure.AI.Projects.OpenAI.AgentRecord,System.Collections.Generic.IList{Microsoft.Extensions.AI.AITool},System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider)</Target>
<Left>lib/netstandard2.0/Microsoft.Agents.AI.AzureAI.dll</Left>
<Right>lib/netstandard2.0/Microsoft.Agents.AI.AzureAI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.AsAIAgent(Azure.AI.Projects.AIProjectClient,Azure.AI.Projects.OpenAI.AgentReference,System.Collections.Generic.IList{Microsoft.Extensions.AI.AITool},System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider)</Target>
<Left>lib/netstandard2.0/Microsoft.Agents.AI.AzureAI.dll</Left>
<Right>lib/netstandard2.0/Microsoft.Agents.AI.AzureAI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.AsAIAgent(Azure.AI.Projects.AIProjectClient,Azure.AI.Projects.OpenAI.AgentVersion,System.Collections.Generic.IList{Microsoft.Extensions.AI.AITool},System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider)</Target>
<Left>lib/netstandard2.0/Microsoft.Agents.AI.AzureAI.dll</Left>
<Right>lib/netstandard2.0/Microsoft.Agents.AI.AzureAI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.CreateAIAgentAsync(Azure.AI.Projects.AIProjectClient,System.String,Azure.AI.Projects.AgentVersionCreationOptions,System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.Threading.CancellationToken)</Target>
<Left>lib/netstandard2.0/Microsoft.Agents.AI.AzureAI.dll</Left>
<Right>lib/netstandard2.0/Microsoft.Agents.AI.AzureAI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.CreateAIAgentAsync(Azure.AI.Projects.AIProjectClient,System.String,Microsoft.Agents.AI.ChatClientAgentOptions,System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider,System.Threading.CancellationToken)</Target>
<Left>lib/netstandard2.0/Microsoft.Agents.AI.AzureAI.dll</Left>
<Right>lib/netstandard2.0/Microsoft.Agents.AI.AzureAI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.CreateAIAgentAsync(Azure.AI.Projects.AIProjectClient,System.String,System.String,System.String,System.String,System.Collections.Generic.IList{Microsoft.Extensions.AI.AITool},System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider,System.Threading.CancellationToken)</Target>
<Left>lib/netstandard2.0/Microsoft.Agents.AI.AzureAI.dll</Left>
<Right>lib/netstandard2.0/Microsoft.Agents.AI.AzureAI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.GetAIAgentAsync(Azure.AI.Projects.AIProjectClient,Microsoft.Agents.AI.ChatClientAgentOptions,System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider,System.Threading.CancellationToken)</Target>
<Left>lib/netstandard2.0/Microsoft.Agents.AI.AzureAI.dll</Left>
<Right>lib/netstandard2.0/Microsoft.Agents.AI.AzureAI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Azure.AI.Projects.AzureAIProjectChatClientExtensions.GetAIAgentAsync(Azure.AI.Projects.AIProjectClient,System.String,System.Collections.Generic.IList{Microsoft.Extensions.AI.AITool},System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},System.IServiceProvider,System.Threading.CancellationToken)</Target>
<Left>lib/netstandard2.0/Microsoft.Agents.AI.AzureAI.dll</Left>
<Right>lib/netstandard2.0/Microsoft.Agents.AI.AzureAI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
</Suppressions>
@@ -1,109 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- https://learn.microsoft.com/dotnet/fundamentals/package-validation/diagnostic-ids -->
<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:OpenAI.Responses.OpenAIResponseClientExtensions.AsAIAgent(OpenAI.Responses.ResponsesClient,Microsoft.Agents.AI.ChatClientAgentOptions,System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},Microsoft.Extensions.Logging.ILoggerFactory,System.IServiceProvider)</Target>
<Left>lib/net10.0/Microsoft.Agents.AI.OpenAI.dll</Left>
<Right>lib/net10.0/Microsoft.Agents.AI.OpenAI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:OpenAI.Responses.OpenAIResponseClientExtensions.AsAIAgent(OpenAI.Responses.ResponsesClient,System.String,System.String,System.String,System.Collections.Generic.IList{Microsoft.Extensions.AI.AITool},System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},Microsoft.Extensions.Logging.ILoggerFactory,System.IServiceProvider)</Target>
<Left>lib/net10.0/Microsoft.Agents.AI.OpenAI.dll</Left>
<Right>lib/net10.0/Microsoft.Agents.AI.OpenAI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:OpenAI.Responses.OpenAIResponseClientExtensions.AsIChatClientWithStoredOutputDisabled(OpenAI.Responses.ResponsesClient)</Target>
<Left>lib/net10.0/Microsoft.Agents.AI.OpenAI.dll</Left>
<Right>lib/net10.0/Microsoft.Agents.AI.OpenAI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:OpenAI.Responses.OpenAIResponseClientExtensions.AsAIAgent(OpenAI.Responses.ResponsesClient,Microsoft.Agents.AI.ChatClientAgentOptions,System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},Microsoft.Extensions.Logging.ILoggerFactory,System.IServiceProvider)</Target>
<Left>lib/net472/Microsoft.Agents.AI.OpenAI.dll</Left>
<Right>lib/net472/Microsoft.Agents.AI.OpenAI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:OpenAI.Responses.OpenAIResponseClientExtensions.AsAIAgent(OpenAI.Responses.ResponsesClient,System.String,System.String,System.String,System.Collections.Generic.IList{Microsoft.Extensions.AI.AITool},System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},Microsoft.Extensions.Logging.ILoggerFactory,System.IServiceProvider)</Target>
<Left>lib/net472/Microsoft.Agents.AI.OpenAI.dll</Left>
<Right>lib/net472/Microsoft.Agents.AI.OpenAI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:OpenAI.Responses.OpenAIResponseClientExtensions.AsIChatClientWithStoredOutputDisabled(OpenAI.Responses.ResponsesClient)</Target>
<Left>lib/net472/Microsoft.Agents.AI.OpenAI.dll</Left>
<Right>lib/net472/Microsoft.Agents.AI.OpenAI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:OpenAI.Responses.OpenAIResponseClientExtensions.AsAIAgent(OpenAI.Responses.ResponsesClient,Microsoft.Agents.AI.ChatClientAgentOptions,System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},Microsoft.Extensions.Logging.ILoggerFactory,System.IServiceProvider)</Target>
<Left>lib/net8.0/Microsoft.Agents.AI.OpenAI.dll</Left>
<Right>lib/net8.0/Microsoft.Agents.AI.OpenAI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:OpenAI.Responses.OpenAIResponseClientExtensions.AsAIAgent(OpenAI.Responses.ResponsesClient,System.String,System.String,System.String,System.Collections.Generic.IList{Microsoft.Extensions.AI.AITool},System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},Microsoft.Extensions.Logging.ILoggerFactory,System.IServiceProvider)</Target>
<Left>lib/net8.0/Microsoft.Agents.AI.OpenAI.dll</Left>
<Right>lib/net8.0/Microsoft.Agents.AI.OpenAI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:OpenAI.Responses.OpenAIResponseClientExtensions.AsIChatClientWithStoredOutputDisabled(OpenAI.Responses.ResponsesClient)</Target>
<Left>lib/net8.0/Microsoft.Agents.AI.OpenAI.dll</Left>
<Right>lib/net8.0/Microsoft.Agents.AI.OpenAI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:OpenAI.Responses.OpenAIResponseClientExtensions.AsAIAgent(OpenAI.Responses.ResponsesClient,Microsoft.Agents.AI.ChatClientAgentOptions,System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},Microsoft.Extensions.Logging.ILoggerFactory,System.IServiceProvider)</Target>
<Left>lib/net9.0/Microsoft.Agents.AI.OpenAI.dll</Left>
<Right>lib/net9.0/Microsoft.Agents.AI.OpenAI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:OpenAI.Responses.OpenAIResponseClientExtensions.AsAIAgent(OpenAI.Responses.ResponsesClient,System.String,System.String,System.String,System.Collections.Generic.IList{Microsoft.Extensions.AI.AITool},System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},Microsoft.Extensions.Logging.ILoggerFactory,System.IServiceProvider)</Target>
<Left>lib/net9.0/Microsoft.Agents.AI.OpenAI.dll</Left>
<Right>lib/net9.0/Microsoft.Agents.AI.OpenAI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:OpenAI.Responses.OpenAIResponseClientExtensions.AsIChatClientWithStoredOutputDisabled(OpenAI.Responses.ResponsesClient)</Target>
<Left>lib/net9.0/Microsoft.Agents.AI.OpenAI.dll</Left>
<Right>lib/net9.0/Microsoft.Agents.AI.OpenAI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:OpenAI.Responses.OpenAIResponseClientExtensions.AsAIAgent(OpenAI.Responses.ResponsesClient,Microsoft.Agents.AI.ChatClientAgentOptions,System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},Microsoft.Extensions.Logging.ILoggerFactory,System.IServiceProvider)</Target>
<Left>lib/netstandard2.0/Microsoft.Agents.AI.OpenAI.dll</Left>
<Right>lib/netstandard2.0/Microsoft.Agents.AI.OpenAI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:OpenAI.Responses.OpenAIResponseClientExtensions.AsAIAgent(OpenAI.Responses.ResponsesClient,System.String,System.String,System.String,System.Collections.Generic.IList{Microsoft.Extensions.AI.AITool},System.Func{Microsoft.Extensions.AI.IChatClient,Microsoft.Extensions.AI.IChatClient},Microsoft.Extensions.Logging.ILoggerFactory,System.IServiceProvider)</Target>
<Left>lib/netstandard2.0/Microsoft.Agents.AI.OpenAI.dll</Left>
<Right>lib/netstandard2.0/Microsoft.Agents.AI.OpenAI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:OpenAI.Responses.OpenAIResponseClientExtensions.AsIChatClientWithStoredOutputDisabled(OpenAI.Responses.ResponsesClient)</Target>
<Left>lib/netstandard2.0/Microsoft.Agents.AI.OpenAI.dll</Left>
<Right>lib/netstandard2.0/Microsoft.Agents.AI.OpenAI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
</Suppressions>
@@ -1,39 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- https://learn.microsoft.com/dotnet/fundamentals/package-validation/diagnostic-ids -->
<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.Workflows.Declarative.AzureAgentProvider.get_OpenAIClientOptions</Target>
<Left>lib/net10.0/Microsoft.Agents.AI.Workflows.Declarative.AzureAI.dll</Left>
<Right>lib/net10.0/Microsoft.Agents.AI.Workflows.Declarative.AzureAI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.Workflows.Declarative.AzureAgentProvider.get_OpenAIClientOptions</Target>
<Left>lib/net472/Microsoft.Agents.AI.Workflows.Declarative.AzureAI.dll</Left>
<Right>lib/net472/Microsoft.Agents.AI.Workflows.Declarative.AzureAI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.Workflows.Declarative.AzureAgentProvider.get_OpenAIClientOptions</Target>
<Left>lib/net8.0/Microsoft.Agents.AI.Workflows.Declarative.AzureAI.dll</Left>
<Right>lib/net8.0/Microsoft.Agents.AI.Workflows.Declarative.AzureAI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.Workflows.Declarative.AzureAgentProvider.get_OpenAIClientOptions</Target>
<Left>lib/net9.0/Microsoft.Agents.AI.Workflows.Declarative.AzureAI.dll</Left>
<Right>lib/net9.0/Microsoft.Agents.AI.Workflows.Declarative.AzureAI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.Workflows.Declarative.AzureAgentProvider.get_OpenAIClientOptions</Target>
<Left>lib/netstandard2.0/Microsoft.Agents.AI.Workflows.Declarative.AzureAI.dll</Left>
<Right>lib/netstandard2.0/Microsoft.Agents.AI.Workflows.Declarative.AzureAI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
</Suppressions>
@@ -13,11 +13,6 @@
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" /> <Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
<!-- Package not yet published to NuGet — disable baseline validation until first release -->
<PropertyGroup>
<EnablePackageValidation>false</EnablePackageValidation>
</PropertyGroup>
<PropertyGroup> <PropertyGroup>
<!-- NuGet Package Settings --> <!-- NuGet Package Settings -->
<Title>Microsoft Agent Framework Declarative Workflows MCP</Title> <Title>Microsoft Agent Framework Declarative Workflows MCP</Title>
@@ -1,319 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- https://learn.microsoft.com/dotnet/fundamentals/package-validation/diagnostic-ids -->
<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.Workflows.Config</Target>
<Left>lib/net10.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net10.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.Workflows.Config`1</Target>
<Left>lib/net10.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net10.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.Workflows.ConfigurationExtensions</Target>
<Left>lib/net10.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net10.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.Workflows.Configured</Target>
<Left>lib/net10.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net10.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.Workflows.Configured`1</Target>
<Left>lib/net10.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net10.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.Workflows.Configured`2</Target>
<Left>lib/net10.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net10.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.Workflows.Config</Target>
<Left>lib/net472/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net472/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.Workflows.Config`1</Target>
<Left>lib/net472/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net472/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.Workflows.ConfigurationExtensions</Target>
<Left>lib/net472/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net472/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.Workflows.Configured</Target>
<Left>lib/net472/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net472/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.Workflows.Configured`1</Target>
<Left>lib/net472/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net472/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.Workflows.Configured`2</Target>
<Left>lib/net472/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net472/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.Workflows.Config</Target>
<Left>lib/net8.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net8.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.Workflows.Config`1</Target>
<Left>lib/net8.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net8.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.Workflows.ConfigurationExtensions</Target>
<Left>lib/net8.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net8.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.Workflows.Configured</Target>
<Left>lib/net8.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net8.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.Workflows.Configured`1</Target>
<Left>lib/net8.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net8.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.Workflows.Configured`2</Target>
<Left>lib/net8.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net8.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.Workflows.Config</Target>
<Left>lib/net9.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net9.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.Workflows.Config`1</Target>
<Left>lib/net9.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net9.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.Workflows.ConfigurationExtensions</Target>
<Left>lib/net9.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net9.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.Workflows.Configured</Target>
<Left>lib/net9.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net9.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.Workflows.Configured`1</Target>
<Left>lib/net9.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net9.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.Workflows.Configured`2</Target>
<Left>lib/net9.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net9.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.Workflows.Config</Target>
<Left>lib/netstandard2.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/netstandard2.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.Workflows.Config`1</Target>
<Left>lib/netstandard2.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/netstandard2.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.Workflows.ConfigurationExtensions</Target>
<Left>lib/netstandard2.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/netstandard2.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.Workflows.Configured</Target>
<Left>lib/netstandard2.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/netstandard2.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.Workflows.Configured`1</Target>
<Left>lib/netstandard2.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/netstandard2.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.Workflows.Configured`2</Target>
<Left>lib/netstandard2.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/netstandard2.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.Workflows.AgentWorkflowBuilder.CreateHandoffBuilderWith(Microsoft.Agents.AI.AIAgent)</Target>
<Left>lib/net10.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net10.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.Workflows.ExecutorBindingExtensions.BindExecutor``2(System.Func{Microsoft.Agents.AI.Workflows.Config{``1},System.String,System.Threading.Tasks.ValueTask{``0}},System.String,``1)</Target>
<Left>lib/net10.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net10.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.Workflows.ExecutorBindingExtensions.ConfigureFactory``2(System.Func{Microsoft.Agents.AI.Workflows.Config{``1},System.String,System.Threading.Tasks.ValueTask{``0}},System.String,``1)</Target>
<Left>lib/net10.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net10.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.Workflows.AgentWorkflowBuilder.CreateHandoffBuilderWith(Microsoft.Agents.AI.AIAgent)</Target>
<Left>lib/net472/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net472/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.Workflows.ExecutorBindingExtensions.BindExecutor``2(System.Func{Microsoft.Agents.AI.Workflows.Config{``1},System.String,System.Threading.Tasks.ValueTask{``0}},System.String,``1)</Target>
<Left>lib/net472/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net472/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.Workflows.ExecutorBindingExtensions.ConfigureFactory``2(System.Func{Microsoft.Agents.AI.Workflows.Config{``1},System.String,System.Threading.Tasks.ValueTask{``0}},System.String,``1)</Target>
<Left>lib/net472/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net472/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.Workflows.AgentWorkflowBuilder.CreateHandoffBuilderWith(Microsoft.Agents.AI.AIAgent)</Target>
<Left>lib/net8.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net8.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.Workflows.ExecutorBindingExtensions.BindExecutor``2(System.Func{Microsoft.Agents.AI.Workflows.Config{``1},System.String,System.Threading.Tasks.ValueTask{``0}},System.String,``1)</Target>
<Left>lib/net8.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net8.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.Workflows.ExecutorBindingExtensions.ConfigureFactory``2(System.Func{Microsoft.Agents.AI.Workflows.Config{``1},System.String,System.Threading.Tasks.ValueTask{``0}},System.String,``1)</Target>
<Left>lib/net8.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net8.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.Workflows.AgentWorkflowBuilder.CreateHandoffBuilderWith(Microsoft.Agents.AI.AIAgent)</Target>
<Left>lib/net9.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net9.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.Workflows.ExecutorBindingExtensions.BindExecutor``2(System.Func{Microsoft.Agents.AI.Workflows.Config{``1},System.String,System.Threading.Tasks.ValueTask{``0}},System.String,``1)</Target>
<Left>lib/net9.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net9.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.Workflows.ExecutorBindingExtensions.ConfigureFactory``2(System.Func{Microsoft.Agents.AI.Workflows.Config{``1},System.String,System.Threading.Tasks.ValueTask{``0}},System.String,``1)</Target>
<Left>lib/net9.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/net9.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.Workflows.AgentWorkflowBuilder.CreateHandoffBuilderWith(Microsoft.Agents.AI.AIAgent)</Target>
<Left>lib/netstandard2.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/netstandard2.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.Workflows.ExecutorBindingExtensions.BindExecutor``2(System.Func{Microsoft.Agents.AI.Workflows.Config{``1},System.String,System.Threading.Tasks.ValueTask{``0}},System.String,``1)</Target>
<Left>lib/netstandard2.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/netstandard2.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.Workflows.ExecutorBindingExtensions.ConfigureFactory``2(System.Func{Microsoft.Agents.AI.Workflows.Config{``1},System.String,System.Threading.Tasks.ValueTask{``0}},System.String,``1)</Target>
<Left>lib/netstandard2.0/Microsoft.Agents.AI.Workflows.dll</Left>
<Right>lib/netstandard2.0/Microsoft.Agents.AI.Workflows.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
</Suppressions>
@@ -139,8 +139,8 @@ public sealed partial class ChatClientAgent : AIAgent
this._logger = (loggerFactory ?? chatClient.GetService<ILoggerFactory>() ?? NullLoggerFactory.Instance).CreateLogger<ChatClientAgent>(); this._logger = (loggerFactory ?? chatClient.GetService<ILoggerFactory>() ?? NullLoggerFactory.Instance).CreateLogger<ChatClientAgent>();
// Warn if using a custom chat client stack with simulated service stored persistence but no ServiceStoredSimulatingChatClient. // Warn if using a custom chat client stack with end-of-run persistence but no ChatHistoryPersistingChatClient.
this.WarnOnMissingServiceStoredSimulatingClient(); this.WarnOnMissingPersistingClient();
} }
/// <summary> /// <summary>
@@ -454,7 +454,7 @@ public sealed partial class ChatClientAgent : AIAgent
/// Notifies the <see cref="ChatHistoryProvider"/> and all <see cref="AIContextProviders"/> of successfully completed messages. /// Notifies the <see cref="ChatHistoryProvider"/> and all <see cref="AIContextProviders"/> of successfully completed messages.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This method is also called by <see cref="ServiceStoredSimulatingChatClient"/> to persist messages per-service-call. /// This method is also called by <see cref="ChatHistoryPersistingChatClient"/> to persist messages per-service-call.
/// </remarks> /// </remarks>
internal async Task NotifyProvidersOfNewMessagesAsync( internal async Task NotifyProvidersOfNewMessagesAsync(
ChatClientAgentSession session, ChatClientAgentSession session,
@@ -463,7 +463,7 @@ public sealed partial class ChatClientAgent : AIAgent
ChatOptions? chatOptions, ChatOptions? chatOptions,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
ChatHistoryProvider? chatHistoryProvider = this.ResolveChatHistoryProvider(chatOptions); ChatHistoryProvider? chatHistoryProvider = this.ResolveChatHistoryProvider(chatOptions, session);
if (chatHistoryProvider is not null) if (chatHistoryProvider is not null)
{ {
@@ -486,7 +486,7 @@ public sealed partial class ChatClientAgent : AIAgent
/// Notifies the <see cref="ChatHistoryProvider"/> and all <see cref="AIContextProviders"/> of a failure during a service call. /// Notifies the <see cref="ChatHistoryProvider"/> and all <see cref="AIContextProviders"/> of a failure during a service call.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This method is also called by <see cref="ServiceStoredSimulatingChatClient"/> to report failures per-service-call. /// This method is also called by <see cref="ChatHistoryPersistingChatClient"/> to report failures per-service-call.
/// </remarks> /// </remarks>
internal async Task NotifyProvidersOfFailureAsync( internal async Task NotifyProvidersOfFailureAsync(
ChatClientAgentSession session, ChatClientAgentSession session,
@@ -495,7 +495,7 @@ public sealed partial class ChatClientAgent : AIAgent
ChatOptions? chatOptions, ChatOptions? chatOptions,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
ChatHistoryProvider? chatHistoryProvider = this.ResolveChatHistoryProvider(chatOptions); ChatHistoryProvider? chatHistoryProvider = this.ResolveChatHistoryProvider(chatOptions, session);
if (chatHistoryProvider is not null) if (chatHistoryProvider is not null)
{ {
@@ -701,7 +701,7 @@ public sealed partial class ChatClientAgent : AIAgent
throw new InvalidOperationException("A session must be provided when continuing a background response with a continuation token."); throw new InvalidOperationException("A session must be provided when continuing a background response with a continuation token.");
} }
if ((continuationToken is not null || chatOptions?.AllowBackgroundResponses is true) && this.SimulatesServiceStoredChatHistory && this._logger.IsEnabled(LogLevel.Warning)) if ((continuationToken is not null || chatOptions?.AllowBackgroundResponses is true) && this.PersistsChatHistoryPerServiceCall && this._logger.IsEnabled(LogLevel.Warning))
{ {
var warningAgentName = this.GetLoggingAgentName(); var warningAgentName = this.GetLoggingAgentName();
this._logger.LogAgentChatClientBackgroundResponseFallback(this.Id, warningAgentName); this._logger.LogAgentChatClientBackgroundResponseFallback(this.Id, warningAgentName);
@@ -719,6 +719,57 @@ public sealed partial class ChatClientAgent : AIAgent
throw new InvalidOperationException("Input messages are not allowed when continuing a background response using a continuation token."); throw new InvalidOperationException("Input messages are not allowed when continuing a background response using a continuation token.");
} }
IEnumerable<ChatMessage> inputMessagesForChatClient = inputMessages;
// Populate the session messages only if we are not continuing an existing response as it's not allowed
if (chatOptions?.ContinuationToken is null)
{
ChatHistoryProvider? chatHistoryProvider = this.ResolveChatHistoryProvider(chatOptions, typedSession);
// Add any existing messages from the session to the messages to be sent to the chat client.
// The ChatHistoryProvider returns the merged result (history + input messages).
if (chatHistoryProvider is not null)
{
var invokingContext = new ChatHistoryProvider.InvokingContext(this, typedSession, inputMessagesForChatClient);
inputMessagesForChatClient = await chatHistoryProvider.InvokingAsync(invokingContext, cancellationToken).ConfigureAwait(false);
}
// If we have an AIContextProvider, we should get context from it, and update our
// messages and options with the additional context.
// The AIContextProvider returns the accumulated AIContext (original + new contributions).
if (this.AIContextProviders is { Count: > 0 } aiContextProviders)
{
var aiContext = new AIContext
{
Instructions = chatOptions?.Instructions,
Messages = inputMessagesForChatClient,
Tools = chatOptions?.Tools
};
foreach (var aiContextProvider in aiContextProviders)
{
var invokingContext = new AIContextProvider.InvokingContext(this, typedSession, aiContext);
aiContext = await aiContextProvider.InvokingAsync(invokingContext, cancellationToken).ConfigureAwait(false);
}
// Materialize the accumulated messages and tools once at the end of the provider pipeline.
inputMessagesForChatClient = aiContext.Messages ?? [];
var tools = aiContext.Tools as IList<AITool> ?? aiContext.Tools?.ToList();
if (chatOptions?.Tools is { Count: > 0 } || tools is { Count: > 0 })
{
chatOptions ??= new();
chatOptions.Tools = tools;
}
if (chatOptions?.Instructions is not null || aiContext.Instructions is not null)
{
chatOptions ??= new();
chatOptions.Instructions = aiContext.Instructions;
}
}
}
// If a user provided two different session ids, via the session object and options, we should throw // If a user provided two different session ids, via the session object and options, we should throw
// since we don't know which one to use. // since we don't know which one to use.
if (!string.IsNullOrWhiteSpace(typedSession.ConversationId) && !string.IsNullOrWhiteSpace(chatOptions?.ConversationId) && typedSession.ConversationId != chatOptions!.ConversationId) if (!string.IsNullOrWhiteSpace(typedSession.ConversationId) && !string.IsNullOrWhiteSpace(chatOptions?.ConversationId) && typedSession.ConversationId != chatOptions!.ConversationId)
@@ -737,53 +788,12 @@ public sealed partial class ChatClientAgent : AIAgent
chatOptions.ConversationId = typedSession.ConversationId; chatOptions.ConversationId = typedSession.ConversationId;
} }
IEnumerable<ChatMessage> inputMessagesForChatClient = inputMessages; // When per-service-call persistence is active, set a sentinel conversation ID so that
// FunctionInvokingChatClient treats locally-persisted history the same as service-managed
// Populate the session messages only if we are not continuing an existing response as it's not allowed. // history. This prevents it from adding duplicate FunctionCallContent messages into the
// When SimulateServiceStoredChatHistory is active, the ServiceStoredSimulatingChatClient // request when processing approval responses — the loaded history already contains them.
// owns the chat history lifecycle — it loads history before each service call. The agent // ChatHistoryPersistingChatClient strips the sentinel before forwarding to the inner client.
// must not load history itself, as that would result in duplicate messages. chatOptions = this.SetLocalHistoryConversationIdIfNeeded(chatOptions);
if (chatOptions?.ContinuationToken is null && !this.SimulatesServiceStoredChatHistory)
{
// Add any existing messages from the session to the messages to be sent to the chat client.
// The ChatHistoryProvider returns the merged result (history + input messages).
inputMessagesForChatClient = await this.LoadChatHistoryAsync(typedSession, inputMessagesForChatClient, chatOptions, cancellationToken).ConfigureAwait(false);
}
// AIContextProviders should always be invoked (unless continuing an existing response)
// to contribute additional messages, tools, and instructions — even when the decorator
// handles history loading.
if (chatOptions?.ContinuationToken is null && this.AIContextProviders is { Count: > 0 } aiContextProviders)
{
var aiContext = new AIContext
{
Instructions = chatOptions?.Instructions,
Messages = inputMessagesForChatClient,
Tools = chatOptions?.Tools
};
foreach (var aiContextProvider in aiContextProviders)
{
var invokingContext = new AIContextProvider.InvokingContext(this, typedSession, aiContext);
aiContext = await aiContextProvider.InvokingAsync(invokingContext, cancellationToken).ConfigureAwait(false);
}
// Materialize the accumulated messages and tools once at the end of the provider pipeline.
inputMessagesForChatClient = aiContext.Messages ?? [];
var tools = aiContext.Tools as IList<AITool> ?? aiContext.Tools?.ToList();
if (chatOptions?.Tools is { Count: > 0 } || tools is { Count: > 0 })
{
chatOptions ??= new();
chatOptions.Tools = tools;
}
if (chatOptions?.Instructions is not null || aiContext.Instructions is not null)
{
chatOptions ??= new();
chatOptions.Instructions = aiContext.Instructions;
}
}
// Materialize the accumulated messages once at the end of the provider pipeline, reusing the existing list if possible. // Materialize the accumulated messages once at the end of the provider pipeline, reusing the existing list if possible.
List<ChatMessage> messagesList = inputMessagesForChatClient as List<ChatMessage> ?? inputMessagesForChatClient.ToList(); List<ChatMessage> messagesList = inputMessagesForChatClient as List<ChatMessage> ?? inputMessagesForChatClient.ToList();
@@ -829,6 +839,8 @@ public sealed partial class ChatClientAgent : AIAgent
} }
} }
// If we got a conversation id back from the chat client, it means that the service supports server side session storage
// so we should update the session with the new id.
session.ConversationId = responseConversationId; session.ConversationId = responseConversationId;
} }
} }
@@ -837,14 +849,14 @@ public sealed partial class ChatClientAgent : AIAgent
/// Updates the session conversation ID at the end of an agent run. /// Updates the session conversation ID at the end of an agent run.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// When a <see cref="ServiceStoredSimulatingChatClient"/> handles per-service-call /// When a <see cref="ChatHistoryPersistingChatClient"/> in persist mode handles per-service-call
/// conversation ID updates, this end-of-run update is skipped. When the decorator is /// conversation ID updates, this end-of-run update is skipped. When the decorator is in mark-only
/// absent, the update is performed here. When <paramref name="forceUpdate"/> is <see langword="true"/> /// mode or absent, the update is performed here. When <paramref name="forceUpdate"/> is <see langword="true"/>
/// (continuation token scenarios), the update is always performed. /// (continuation token scenarios), the update is always performed.
/// </remarks> /// </remarks>
private void UpdateSessionConversationIdAtEndOfRun(ChatClientAgentSession session, string? responseConversationId, CancellationToken cancellationToken, bool forceUpdate = false) private void UpdateSessionConversationIdAtEndOfRun(ChatClientAgentSession session, string? responseConversationId, CancellationToken cancellationToken, bool forceUpdate = false)
{ {
if (!forceUpdate && this.SimulatesServiceStoredChatHistory) if (!forceUpdate && this.PersistsChatHistoryPerServiceCall)
{ {
return; return;
} }
@@ -856,9 +868,10 @@ public sealed partial class ChatClientAgent : AIAgent
/// Notifies providers of successfully completed messages at the end of an agent run. /// Notifies providers of successfully completed messages at the end of an agent run.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// When a <see cref="ServiceStoredSimulatingChatClient"/> handles per-service-call /// When a <see cref="ChatHistoryPersistingChatClient"/> in persist mode handles per-service-call
/// notification, this end-of-run notification is skipped. When no decorator is present, /// notification, this end-of-run notification is skipped. When the decorator is in mark-only mode,
/// all messages are persisted. /// only the marked messages are persisted. When no decorator is present (custom stack with
/// <see cref="ChatClientAgentOptions.PersistChatHistoryAtEndOfRun"/>), all messages are persisted.
/// When <paramref name="forceNotify"/> is <see langword="true"/> (continuation token or /// When <paramref name="forceNotify"/> is <see langword="true"/> (continuation token or
/// background response scenarios), notification is always performed with all messages because /// background response scenarios), notification is always performed with all messages because
/// per-service-call persistence is unreliable in these scenarios. /// per-service-call persistence is unreliable in these scenarios.
@@ -871,11 +884,19 @@ public sealed partial class ChatClientAgent : AIAgent
CancellationToken cancellationToken, CancellationToken cancellationToken,
bool forceNotify = false) bool forceNotify = false)
{ {
if (!forceNotify && this.SimulatesServiceStoredChatHistory) if (!forceNotify && this.PersistsChatHistoryPerServiceCall)
{ {
return Task.CompletedTask; return Task.CompletedTask;
} }
if (!forceNotify && this.HasMarkOnlyChatHistoryPersistingClient)
{
// In mark-only mode, persist only messages that were marked by the decorator.
var markedRequestMessages = GetMarkedMessages(requestMessages);
var markedResponseMessages = GetMarkedMessages(responseMessages);
return this.NotifyProvidersOfNewMessagesAsync(session, markedRequestMessages, markedResponseMessages, chatOptions, cancellationToken);
}
return this.NotifyProvidersOfNewMessagesAsync(session, requestMessages, responseMessages, chatOptions, cancellationToken); return this.NotifyProvidersOfNewMessagesAsync(session, requestMessages, responseMessages, chatOptions, cancellationToken);
} }
@@ -883,7 +904,7 @@ public sealed partial class ChatClientAgent : AIAgent
/// Notifies providers of a failure at the end of an agent run. /// Notifies providers of a failure at the end of an agent run.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// When a <see cref="ServiceStoredSimulatingChatClient"/> handles per-service-call /// When a <see cref="ChatHistoryPersistingChatClient"/> in persist mode handles per-service-call
/// notification (including failure), this end-of-run notification is skipped to avoid /// notification (including failure), this end-of-run notification is skipped to avoid
/// duplicate notification. In all other cases, failure is reported at the end of the run. /// duplicate notification. In all other cases, failure is reported at the end of the run.
/// </remarks> /// </remarks>
@@ -894,7 +915,7 @@ public sealed partial class ChatClientAgent : AIAgent
ChatOptions? chatOptions, ChatOptions? chatOptions,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
if (this.SimulatesServiceStoredChatHistory) if (this.PersistsChatHistoryPerServiceCall)
{ {
return Task.CompletedTask; return Task.CompletedTask;
} }
@@ -903,19 +924,60 @@ public sealed partial class ChatClientAgent : AIAgent
} }
/// <summary> /// <summary>
/// Gets a value indicating whether the agent is configured to simulate service-stored chat history. /// Gets a value indicating whether the agent has a <see cref="ChatHistoryPersistingChatClient"/>
/// When <see langword="true"/>, end-of-run persistence and history loading are skipped because a /// decorator in persist mode (not mark-only), which handles per-service-call persistence.
/// per-service-call decorator (such as <see cref="ServiceStoredSimulatingChatClient"/> or a
/// user-supplied equivalent) is expected to handle the history lifecycle.
/// </summary> /// </summary>
private bool SimulatesServiceStoredChatHistory private bool PersistsChatHistoryPerServiceCall
{ {
get get
{ {
return this._agentOptions?.SimulateServiceStoredChatHistory is true; var persistingClient = this.ChatClient.GetService<ChatHistoryPersistingChatClient>();
return persistingClient?.MarkOnly == false;
} }
} }
/// <summary>
/// Sets the <see cref="ChatHistoryPersistingChatClient.LocalHistoryConversationId"/> sentinel on
/// <paramref name="chatOptions"/> when per-service-call persistence is active and no real
/// conversation ID is present.
/// </summary>
/// <returns>
/// The (possibly new) <see cref="ChatOptions"/> with the sentinel set, or the original
/// <paramref name="chatOptions"/> if no sentinel is needed.
/// </returns>
private ChatOptions? SetLocalHistoryConversationIdIfNeeded(ChatOptions? chatOptions)
{
if (this.PersistsChatHistoryPerServiceCall && string.IsNullOrWhiteSpace(chatOptions?.ConversationId))
{
chatOptions ??= new ChatOptions();
chatOptions.ConversationId = ChatHistoryPersistingChatClient.LocalHistoryConversationId;
}
return chatOptions;
}
/// <summary>
/// Gets a value indicating whether the agent has a <see cref="ChatHistoryPersistingChatClient"/>
/// decorator in mark-only mode, which marks messages for later persistence at the end of the run.
/// </summary>
private bool HasMarkOnlyChatHistoryPersistingClient
{
get
{
var persistingClient = this.ChatClient.GetService<ChatHistoryPersistingChatClient>();
return persistingClient?.MarkOnly == true;
}
}
/// <summary>
/// Returns only the messages that have been marked as persisted by a <see cref="ChatHistoryPersistingChatClient"/> in mark-only mode.
/// </summary>
private static List<ChatMessage> GetMarkedMessages(IEnumerable<ChatMessage> messages)
{
return messages.Where(m =>
m.AdditionalProperties?.TryGetValue(ChatHistoryPersistingChatClient.PersistedMarkerKey, out var value) == true && value is true).ToList();
}
/// <summary> /// <summary>
/// Ensures that <see cref="AIAgent.CurrentRunContext"/> contains the resolved session. /// Ensures that <see cref="AIAgent.CurrentRunContext"/> contains the resolved session.
/// </summary> /// </summary>
@@ -923,7 +985,7 @@ public sealed partial class ChatClientAgent : AIAgent
/// The base class sets <see cref="AIAgent.CurrentRunContext"/> with the raw session parameter /// The base class sets <see cref="AIAgent.CurrentRunContext"/> with the raw session parameter
/// (which may be null) and restores it after each yield in streaming scenarios. After /// (which may be null) and restores it after each yield in streaming scenarios. After
/// <see cref="PrepareSessionAndMessagesAsync"/> resolves or creates a session, we update the /// <see cref="PrepareSessionAndMessagesAsync"/> resolves or creates a session, we update the
/// context so the <see cref="ServiceStoredSimulatingChatClient"/> decorator always has a valid session. /// context so the <see cref="ChatHistoryPersistingChatClient"/> decorator always has a valid session.
/// The original agent from the context is preserved to maintain the top-of-stack agent in /// The original agent from the context is preserved to maintain the top-of-stack agent in
/// decorated agent scenarios. /// decorated agent scenarios.
/// </remarks> /// </remarks>
@@ -939,36 +1001,36 @@ public sealed partial class ChatClientAgent : AIAgent
/// <summary> /// <summary>
/// Checks for potential misconfiguration when using a custom chat client stack and logs warnings. /// Checks for potential misconfiguration when using a custom chat client stack and logs warnings.
/// </summary> /// </summary>
private void WarnOnMissingServiceStoredSimulatingClient() private void WarnOnMissingPersistingClient()
{ {
if (this._agentOptions?.UseProvidedChatClientAsIs is not true) if (this._agentOptions?.UseProvidedChatClientAsIs is not true)
{ {
return; return;
} }
if (this._agentOptions?.SimulateServiceStoredChatHistory is not true) if (this._agentOptions?.PersistChatHistoryAtEndOfRun is not true)
{ {
return; return;
} }
var persistingClient = this.ChatClient.GetService<ServiceStoredSimulatingChatClient>(); var persistingClient = this.ChatClient.GetService<ChatHistoryPersistingChatClient>();
if (persistingClient is null && this._logger.IsEnabled(LogLevel.Warning)) if (persistingClient is null && this._logger.IsEnabled(LogLevel.Warning))
{ {
var loggingAgentName = this.GetLoggingAgentName(); var loggingAgentName = this.GetLoggingAgentName();
this._logger.LogAgentChatClientMissingPersistingClient( this._logger.LogAgentChatClientMissingPersistingClient(
this.Id, this.Id,
loggingAgentName); // CodeQL [CWE-359] False positive: Agent name is not personal information, but rather just the name of a code component (agent in this case). loggingAgentName);
} }
} }
private ChatHistoryProvider? ResolveChatHistoryProvider(ChatOptions? chatOptions) private ChatHistoryProvider? ResolveChatHistoryProvider(ChatOptions? chatOptions, ChatClientAgentSession session)
{ {
ChatHistoryProvider? provider = chatOptions?.ConversationId is null ? this.ChatHistoryProvider : null; ChatHistoryProvider? provider = session.ConversationId is null ? this.ChatHistoryProvider : null;
// If someone provided an override ChatHistoryProvider via AdditionalProperties, we should use that instead. // If someone provided an override ChatHistoryProvider via AdditionalProperties, we should use that instead.
if (chatOptions?.AdditionalProperties?.TryGetValue(out ChatHistoryProvider? overrideProvider) is true) if (chatOptions?.AdditionalProperties?.TryGetValue(out ChatHistoryProvider? overrideProvider) is true)
{ {
if (this._agentOptions?.ThrowOnChatHistoryProviderConflict is true && string.IsNullOrWhiteSpace(chatOptions?.ConversationId) is false) if (session.ConversationId is not null && overrideProvider is not null)
{ {
throw new InvalidOperationException( throw new InvalidOperationException(
$"Only {nameof(ChatClientAgentSession.ConversationId)} or {nameof(this.ChatHistoryProvider)} may be used, but not both. The current {nameof(ChatClientAgentSession)} has a {nameof(ChatClientAgentSession.ConversationId)} indicating server-side chat history management, but an override {nameof(this.ChatHistoryProvider)} was provided via {nameof(AgentRunOptions.AdditionalProperties)}."); $"Only {nameof(ChatClientAgentSession.ConversationId)} or {nameof(this.ChatHistoryProvider)} may be used, but not both. The current {nameof(ChatClientAgentSession)} has a {nameof(ChatClientAgentSession.ConversationId)} indicating server-side chat history management, but an override {nameof(this.ChatHistoryProvider)} was provided via {nameof(AgentRunOptions.AdditionalProperties)}.");
@@ -993,29 +1055,6 @@ public sealed partial class ChatClientAgent : AIAgent
return provider; return provider;
} }
/// <summary>
/// Loads chat history from the resolved <see cref="ChatHistoryProvider"/> and prepends it to the given messages.
/// </summary>
/// <remarks>
/// This method is used by both the agent (during <see cref="PrepareSessionAndMessagesAsync"/>) and by
/// <see cref="ServiceStoredSimulatingChatClient"/> to load history before each service call.
/// </remarks>
internal async Task<IEnumerable<ChatMessage>> LoadChatHistoryAsync(
ChatClientAgentSession session,
IEnumerable<ChatMessage> messages,
ChatOptions? chatOptions,
CancellationToken cancellationToken)
{
var chatHistoryProvider = this.ResolveChatHistoryProvider(chatOptions);
if (chatHistoryProvider is null)
{
return messages;
}
var invokingContext = new ChatHistoryProvider.InvokingContext(this, session, messages);
return await chatHistoryProvider.InvokingAsync(invokingContext, cancellationToken).ConfigureAwait(false);
}
private static ChatClientAgentContinuationToken? WrapContinuationToken(ResponseContinuationToken? continuationToken, IEnumerable<ChatMessage>? inputMessages = null, List<ChatResponseUpdate>? responseUpdates = null) private static ChatClientAgentContinuationToken? WrapContinuationToken(ResponseContinuationToken? continuationToken, IEnumerable<ChatMessage>? inputMessages = null, List<ChatResponseUpdate>? responseUpdates = null)
{ {
if (continuationToken is null) if (continuationToken is null)
@@ -72,12 +72,12 @@ internal static partial class ChatClientAgentLogMessages
/// <summary> /// <summary>
/// Logs a warning when <see cref="ChatClientAgentOptions.UseProvidedChatClientAsIs"/> is <see langword="true"/> /// Logs a warning when <see cref="ChatClientAgentOptions.UseProvidedChatClientAsIs"/> is <see langword="true"/>
/// and <see cref="ChatClientAgentOptions.SimulateServiceStoredChatHistory"/> is <see langword="true"/>, /// and <see cref="ChatClientAgentOptions.PersistChatHistoryAtEndOfRun"/> is <see langword="true"/>,
/// but no <see cref="ServiceStoredSimulatingChatClient"/> is found in the custom chat client stack. /// but no <see cref="ChatHistoryPersistingChatClient"/> is found in the custom chat client stack.
/// </summary> /// </summary>
[LoggerMessage( [LoggerMessage(
Level = LogLevel.Warning, Level = LogLevel.Warning,
Message = "Agent {AgentId}/{AgentName}: SimulateServiceStoredChatHistory is enabled with a custom chat client stack (UseProvidedChatClientAsIs), but no ServiceStoredSimulatingChatClient was found in the pipeline. Chat history will not be persisted by ChatClientAgent. Consider adding a ServiceStoredSimulatingChatClient to the pipeline using the UseServiceStoredChatHistorySimulation extension method if you have not added your own persistence mechanism.")] Message = "Agent {AgentId}/{AgentName}: PersistChatHistoryAtEndOfRun is enabled with a custom chat client stack (UseProvidedChatClientAsIs), but no ChatHistoryPersistingChatClient was found in the pipeline. All messages will be persisted at the end of the run without marking. This setup is not supported with some other features, e.g. handoffs. Consider adding a ChatHistoryPersistingChatClient to the pipeline using the UseChatHistoryPersisting extension method.")]
public static partial void LogAgentChatClientMissingPersistingClient( public static partial void LogAgentChatClientMissingPersistingClient(
this ILogger logger, this ILogger logger,
string agentId, string agentId,
@@ -92,7 +92,7 @@ internal static partial class ChatClientAgentLogMessages
/// </summary> /// </summary>
[LoggerMessage( [LoggerMessage(
Level = LogLevel.Warning, Level = LogLevel.Warning,
Message = "Agent {AgentId}/{AgentName}: SimulateServiceStoredChatHistory is enabled but we have to fall back to end-of-run persistence because the run involves background responses.")] Message = "Agent {AgentId}/{AgentName}: Per-service-call persistence is falling back to end-of-run persistence because the run involves background responses. Messages will be marked during the run and persisted at the end.")]
public static partial void LogAgentChatClientBackgroundResponseFallback( public static partial void LogAgentChatClientBackgroundResponseFallback(
this ILogger logger, this ILogger logger,
string agentId, string agentId,
@@ -92,46 +92,54 @@ public sealed class ChatClientAgentOptions
public bool ThrowOnChatHistoryProviderConflict { get; set; } = true; public bool ThrowOnChatHistoryProviderConflict { get; set; } = true;
/// <summary> /// <summary>
/// Gets or sets a value indicating whether the <see cref="ChatClientAgent"/> should simulate /// Gets or sets a value indicating whether to persist chat history only at the end of the full agent run
/// service-stored chat history behavior using its configured <see cref="ChatHistoryProvider"/>. /// rather than after each individual service call.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// <para> /// <para>
/// When set to <see langword="true"/>, a <see cref="ServiceStoredSimulatingChatClient"/> decorator is /// By default, <see cref="ChatClientAgent"/> persists request and response messages either via
/// injected between the <see cref="FunctionInvokingChatClient"/> and the leaf <see cref="IChatClient"/> /// a <see cref="ChatHistoryProvider"/>, or the underlying AI service's chat history storage.
/// in the chat client pipeline. This decorator takes full ownership of the chat history lifecycle: /// Persistence is done immediately after each call to the AI service within the function invocation loop.
/// it loads history from the <see cref="ChatHistoryProvider"/> before each service call and persists /// When storing in the underlying AI service, the session's <see cref="ChatClientAgentSession.ConversationId"/>
/// new messages after each service call. It also returns a sentinel <see cref="ChatOptions.ConversationId"/> /// is also updated after each service call, keeping it in sync with the service-side conversation state.
/// on the response, causing the <see cref="FunctionInvokingChatClient"/> to treat the conversation
/// as service-managed — clearing accumulated history and not injecting duplicate
/// <see cref="FunctionCallContent"/> during approval-response processing.
/// </para> /// </para>
/// <para> /// <para>
/// This mode aligns the behavior of framework-managed chat history with service-stored chat history, /// Setting this property to <see langword="true"/> causes messages to be marked during the function
/// ensuring consistency in how messages are stored and loaded, including during function calling loops /// invocation loop but persisted only at the end of the full agent run, providing atomic run semantics.
/// and tool-call termination scenarios. /// Updating the <see cref="ChatClientAgentSession.ConversationId"/> is likewise deferred and
/// updated only at the end of the run, consistent with atomic run semantics.
/// A <see cref="ChatHistoryPersistingChatClient"/> decorator is inserted into the chat client pipeline
/// in mark-only mode, and the <see cref="ChatClientAgent"/> persists only the marked messages at the
/// end of the run.
/// </para> /// </para>
/// <para> /// <para>
/// When set to <see langword="false"/> (the default), the <see cref="ChatClientAgent"/> handles /// When this option is <see langword="false"/> (the default), the <see cref="ChatHistoryPersistingChatClient"/>
/// chat history persistence at the end of the full agent run via the <see cref="ChatHistoryProvider"/> /// decorator persists messages and updates the <see cref="ChatClientAgentSession.ConversationId"/>
/// pipeline. /// immediately after each service call. This may leave chat history in a state where
/// <see cref="FunctionResultContent"/> is required to start a new run if the last successful service
/// call returned <see cref="FunctionCallContent"/>.
/// </para> /// </para>
/// <para> /// <para>
/// When setting the <see cref="UseProvidedChatClientAsIs"/> setting to <see langword="true"/> and /// This option has no effect when <see cref="UseProvidedChatClientAsIs"/> is <see langword="true"/>.
/// <see cref="SimulateServiceStoredChatHistory"/> to <see langword="true"/>, ensure that your custom chat client stack includes a /// When using a custom chat client stack, you can add a <see cref="ChatHistoryPersistingChatClient"/>
/// <see cref="ServiceStoredSimulatingChatClient"/> to enable per-service-call persistence. /// manually via the <see cref="ChatClientBuilderExtensions.UseChatHistoryPersisting"/>
/// If no <see cref="ServiceStoredSimulatingChatClient"/> is provided, and you are not storing chat history via other means,
/// no chat history may be stored.
/// When using a custom chat client stack, you can add a <see cref="ServiceStoredSimulatingChatClient"/>
/// manually via the <see cref="ChatClientBuilderExtensions.UseServiceStoredChatHistorySimulation"/>
/// extension method. /// extension method.
/// </para> /// </para>
/// <para>
/// Note that when using single threaded service stored chat history, like OpenAI Conversations,
/// there is only one id, so even if the conversation id is not updated after each service call,
/// the chat history will still contain intermediate messages. Setting this property to <see langword="true"/>
/// in this case will therefore have no real effect. Setting this property to <see langword="true"/> when using
/// OpenAI Responses with response ids on the other hand, allows atomic run semantics, since
/// each service request produces a new response id, and if the run fails mid-loop, the session will
/// still contain the pre-run respnose id, allowing the next run to start with a clean slate.
/// </para>
/// </remarks> /// </remarks>
/// <value> /// <value>
/// Default is <see langword="false"/>. /// Default is <see langword="false"/>.
/// </value> /// </value>
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] [Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
public bool SimulateServiceStoredChatHistory { get; set; } public bool PersistChatHistoryAtEndOfRun { get; set; }
/// <summary> /// <summary>
/// Creates a new instance of <see cref="ChatClientAgentOptions"/> with the same values as this instance. /// Creates a new instance of <see cref="ChatClientAgentOptions"/> with the same values as this instance.
@@ -149,6 +157,6 @@ public sealed class ChatClientAgentOptions
ClearOnChatHistoryProviderConflict = this.ClearOnChatHistoryProviderConflict, ClearOnChatHistoryProviderConflict = this.ClearOnChatHistoryProviderConflict,
WarnOnChatHistoryProviderConflict = this.WarnOnChatHistoryProviderConflict, WarnOnChatHistoryProviderConflict = this.WarnOnChatHistoryProviderConflict,
ThrowOnChatHistoryProviderConflict = this.ThrowOnChatHistoryProviderConflict, ThrowOnChatHistoryProviderConflict = this.ThrowOnChatHistoryProviderConflict,
SimulateServiceStoredChatHistory = this.SimulateServiceStoredChatHistory, PersistChatHistoryAtEndOfRun = this.PersistChatHistoryAtEndOfRun,
}; };
} }
@@ -86,21 +86,25 @@ public static class ChatClientBuilderExtensions
services: services); services: services);
/// <summary> /// <summary>
/// Adds a <see cref="ServiceStoredSimulatingChatClient"/> to the chat client pipeline. /// Adds a <see cref="ChatHistoryPersistingChatClient"/> to the chat client pipeline.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// <para> /// <para>
/// This decorator should be positioned between the <see cref="FunctionInvokingChatClient"/> and the leaf /// This decorator should be positioned between the <see cref="FunctionInvokingChatClient"/> and the leaf
/// <see cref="IChatClient"/> in the pipeline. It simulates service-stored chat history behavior by /// <see cref="IChatClient"/> in the pipeline. It intercepts service calls to either persist messages
/// loading history before each service call, persisting after each call, and returning a sentinel /// immediately or mark them for later persistence, depending on the <paramref name="markOnly"/> parameter.
/// <see cref="ChatOptions.ConversationId"/> on the response. /// </para>
/// <para>
/// If <paramref name="markOnly"/> is set to <see langword="true"/>, the <see cref="ChatClientAgent"/>
/// should be configured with <see cref="ChatClientAgentOptions.PersistChatHistoryAtEndOfRun"/> set to <see langword="true"/>
/// as without this combination, messages will never be persisted when using a <see cref="ChatHistoryProvider"/> for
/// chat history persistence.
/// </para> /// </para>
/// <para> /// <para>
/// This extension method is intended for use with custom chat client stacks when /// This extension method is intended for use with custom chat client stacks when
/// <see cref="ChatClientAgentOptions.UseProvidedChatClientAsIs"/> is <see langword="true"/>. /// <see cref="ChatClientAgentOptions.UseProvidedChatClientAsIs"/> is <see langword="true"/>.
/// When <see cref="ChatClientAgentOptions.UseProvidedChatClientAsIs"/> is <see langword="false"/> (the default), /// When <see cref="ChatClientAgentOptions.UseProvidedChatClientAsIs"/> is <see langword="false"/> (the default),
/// the <see cref="ChatClientAgent"/> automatically injects this decorator when /// the <see cref="ChatClientAgent"/> automatically injects this decorator.
/// <see cref="ChatClientAgentOptions.SimulateServiceStoredChatHistory"/> is <see langword="true"/>.
/// </para> /// </para>
/// <para> /// <para>
/// This decorator only works within the context of a running <see cref="ChatClientAgent"/> and will throw an /// This decorator only works within the context of a running <see cref="ChatClientAgent"/> and will throw an
@@ -108,10 +112,18 @@ public static class ChatClientBuilderExtensions
/// </para> /// </para>
/// </remarks> /// </remarks>
/// <param name="builder">The <see cref="ChatClientBuilder"/> to add the decorator to.</param> /// <param name="builder">The <see cref="ChatClientBuilder"/> to add the decorator to.</param>
/// <param name="markOnly">
/// When <see langword="true"/>, messages are marked with metadata but not persisted immediately,
/// and the session's <see cref="ChatClientAgentSession.ConversationId"/> is not updated.
/// The <see cref="ChatClientAgent"/> will persist only the marked messages and update the
/// conversation ID at the end of the run.
/// When <see langword="false"/> (the default), messages are persisted and the conversation ID
/// is updated immediately after each service call.
/// </param>
/// <returns>The <paramref name="builder"/> for chaining.</returns> /// <returns>The <paramref name="builder"/> for chaining.</returns>
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] [Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
public static ChatClientBuilder UseServiceStoredChatHistorySimulation(this ChatClientBuilder builder) public static ChatClientBuilder UseChatHistoryPersisting(this ChatClientBuilder builder, bool markOnly = false)
{ {
return builder.Use(innerClient => new ServiceStoredSimulatingChatClient(innerClient)); return builder.Use(innerClient => new ChatHistoryPersistingChatClient(innerClient, markOnly));
} }
} }
@@ -63,17 +63,14 @@ public static class ChatClientExtensions
}); });
} }
// ServiceStoredSimulatingChatClient is only injected when SimulateServiceStoredChatHistory is enabled. // ChatHistoryPersistingChatClient is registered after FunctionInvokingChatClient so that it sits
// It is registered after FunctionInvokingChatClient so that it sits between FIC and the leaf client. // between FIC and the leaf client. ChatClientBuilder.Build applies factories in reverse order,
// ChatClientBuilder.Build applies factories in reverse order, making the first Use() call outermost. // making the first Use() call outermost. By adding our decorator second, the resulting pipeline is:
// By adding our decorator second, the resulting pipeline is: // FunctionInvokingChatClient → ChatHistoryPersistingChatClient → leaf IChatClient
// FunctionInvokingChatClient → ServiceStoredSimulatingChatClient → leaf IChatClient // This allows the decorator to persist messages after each individual service call within
// This allows the decorator to simulate service-stored chat history by loading history before // FIC's function invocation loop, or to mark them for later persistence at the end of the run.
// each service call, persisting after each call, and returning a sentinel ConversationId. bool markOnly = options?.PersistChatHistoryAtEndOfRun is true;
if (options?.SimulateServiceStoredChatHistory is true) chatBuilder.Use(innerClient => new ChatHistoryPersistingChatClient(innerClient, markOnly));
{
chatBuilder.Use(innerClient => new ServiceStoredSimulatingChatClient(innerClient));
}
var agentChatClient = chatBuilder.Build(services); var agentChatClient = chatBuilder.Build(services);
@@ -0,0 +1,351 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI;
/// <summary>
/// A delegating chat client that notifies <see cref="ChatHistoryProvider"/> and <see cref="AIContextProvider"/>
/// instances of request and response messages after each individual call to the inner chat client,
/// or marks messages for later persistence depending on the configured mode.
/// </summary>
/// <remarks>
/// <para>
/// This decorator is intended to operate between the <see cref="FunctionInvokingChatClient"/> and the leaf
/// <see cref="IChatClient"/> in a <see cref="ChatClientAgent"/> pipeline.
/// </para>
/// <para>
/// In persist mode (the default), it ensures that providers are notified and the session's
/// <see cref="ChatClientAgentSession.ConversationId"/> is updated after each service call, so that
/// intermediate messages (e.g., tool calls and results) are saved even if the process is interrupted
/// mid-loop.
/// </para>
/// <para>
/// In mark-only mode (<see cref="MarkOnly"/> is <see langword="true"/>), it marks messages with metadata
/// but does not notify providers or update the <see cref="ChatClientAgentSession.ConversationId"/>.
/// Both are deferred to the <see cref="ChatClientAgent"/> at the end of the run, providing atomic
/// run semantics.
/// </para>
/// <para>
/// This chat client must be used within the context of a running <see cref="ChatClientAgent"/>. It retrieves the
/// current agent and session from <see cref="AIAgent.CurrentRunContext"/>, which is set automatically when an agent's
/// <see cref="AIAgent.RunAsync(IEnumerable{ChatMessage}, AgentSession?, AgentRunOptions?, CancellationToken)"/> or
/// <see cref="AIAgent.RunStreamingAsync(IEnumerable{ChatMessage}, AgentSession?, AgentRunOptions?, CancellationToken)"/>
/// method is called. The <see cref="ChatClientAgent"/> ensures the run context always contains a resolved session,
/// even when the caller passes null. An <see cref="InvalidOperationException"/> is thrown if no run context is
/// available or if the agent is not a <see cref="ChatClientAgent"/>.
/// </para>
/// </remarks>
internal sealed class ChatHistoryPersistingChatClient : DelegatingChatClient
{
/// <summary>
/// The key used in <see cref="ChatMessage.AdditionalProperties"/> and <see cref="AIContent.AdditionalProperties"/>
/// to mark messages and their content as already persisted to chat history.
/// </summary>
internal const string PersistedMarkerKey = "_chatHistoryPersisted";
/// <summary>
/// A sentinel value set on <see cref="ChatOptions.ConversationId"/> by <see cref="ChatClientAgent"/>
/// when per-service-call persistence is active and no real conversation ID exists.
/// </summary>
/// <remarks>
/// <para>
/// This signals to <see cref="FunctionInvokingChatClient"/> that the chat history is being managed
/// externally (by this decorator), which prevents it from adding duplicate <see cref="FunctionCallContent"/>
/// messages into the request during approval-response processing. Without this sentinel,
/// <see cref="FunctionInvokingChatClient"/> would reconstruct function-call messages from approval
/// responses and append them to the original messages — but the loaded history already contains
/// those same function calls, causing duplicate tool-call entries that the model rejects.
/// </para>
/// <para>
/// This decorator strips the sentinel before forwarding requests to the inner client, so the
/// underlying model never sees it.
/// </para>
/// </remarks>
internal const string LocalHistoryConversationId = "_agent_local_history";
/// <summary>
/// Initializes a new instance of the <see cref="ChatHistoryPersistingChatClient"/> class.
/// </summary>
/// <param name="innerClient">The underlying chat client that will handle the core operations.</param>
/// <param name="markOnly">
/// When <see langword="true"/>, messages are marked with metadata but not persisted immediately,
/// and the session's <see cref="ChatClientAgentSession.ConversationId"/> is not updated.
/// The <see cref="ChatClientAgent"/> will persist only the marked messages and update the
/// conversation ID at the end of the run.
/// When <see langword="false"/> (the default), messages are persisted and the conversation ID
/// is updated immediately after each service call.
/// </param>
public ChatHistoryPersistingChatClient(IChatClient innerClient, bool markOnly = false)
: base(innerClient)
{
this.MarkOnly = markOnly;
}
/// <summary>
/// Gets a value indicating whether this decorator is in mark-only mode.
/// </summary>
/// <remarks>
/// When <see langword="true"/>, messages are marked with metadata but not persisted immediately,
/// and the session's <see cref="ChatClientAgentSession.ConversationId"/> is not updated.
/// Both are deferred to the <see cref="ChatClientAgent"/> at the end of the run.
/// When <see langword="false"/>, messages are persisted and the conversation ID is updated
/// after each service call.
/// </remarks>
public bool MarkOnly { get; }
/// <inheritdoc/>
public override async Task<ChatResponse> GetResponseAsync(
IEnumerable<ChatMessage> messages,
ChatOptions? options = null,
CancellationToken cancellationToken = default)
{
var (agent, session) = GetRequiredAgentAndSession();
options = StripLocalHistoryConversationId(options);
ChatResponse response;
try
{
response = await base.GetResponseAsync(messages, options, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
var newRequestMessagesOnFailure = GetNewRequestMessages(messages);
await agent.NotifyProvidersOfFailureAsync(session, ex, newRequestMessagesOnFailure, options, cancellationToken).ConfigureAwait(false);
throw;
}
var newRequestMessages = GetNewRequestMessages(messages);
if (this.ShouldDeferPersistence(options))
{
// In mark-only mode or when resuming from a continuation token, just mark messages
// for later persistence by ChatClientAgent. Conversation ID and provider notification
// are deferred to end-of-run. For continuation tokens, the end-of-run handler needs
// to send the combined data from both the previous and current runs.
MarkAsPersisted(newRequestMessages);
MarkAsPersisted(response.Messages);
}
else
{
// In persist mode, persist immediately and update conversation ID.
agent.UpdateSessionConversationId(session, response.ConversationId, cancellationToken);
await agent.NotifyProvidersOfNewMessagesAsync(session, newRequestMessages, response.Messages, options, cancellationToken).ConfigureAwait(false);
MarkAsPersisted(newRequestMessages);
MarkAsPersisted(response.Messages);
}
return response;
}
/// <inheritdoc/>
public override async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
IEnumerable<ChatMessage> messages,
ChatOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
var (agent, session) = GetRequiredAgentAndSession();
options = StripLocalHistoryConversationId(options);
List<ChatResponseUpdate> responseUpdates = [];
IAsyncEnumerator<ChatResponseUpdate> enumerator;
try
{
enumerator = base.GetStreamingResponseAsync(messages, options, cancellationToken).GetAsyncEnumerator(cancellationToken);
}
catch (Exception ex)
{
var newRequestMessagesOnFailure = GetNewRequestMessages(messages);
await agent.NotifyProvidersOfFailureAsync(session, ex, newRequestMessagesOnFailure, options, cancellationToken).ConfigureAwait(false);
throw;
}
bool hasUpdates;
try
{
hasUpdates = await enumerator.MoveNextAsync().ConfigureAwait(false);
}
catch (Exception ex)
{
var newRequestMessagesOnFailure = GetNewRequestMessages(messages);
await agent.NotifyProvidersOfFailureAsync(session, ex, newRequestMessagesOnFailure, options, cancellationToken).ConfigureAwait(false);
throw;
}
while (hasUpdates)
{
var update = enumerator.Current;
responseUpdates.Add(update);
yield return update;
try
{
hasUpdates = await enumerator.MoveNextAsync().ConfigureAwait(false);
}
catch (Exception ex)
{
var newRequestMessagesOnFailure = GetNewRequestMessages(messages);
await agent.NotifyProvidersOfFailureAsync(session, ex, newRequestMessagesOnFailure, options, cancellationToken).ConfigureAwait(false);
throw;
}
}
var chatResponse = responseUpdates.ToChatResponse();
var newRequestMessages = GetNewRequestMessages(messages);
if (this.ShouldDeferPersistence(options))
{
// In mark-only mode or when resuming from a continuation token, just mark messages
// for later persistence by ChatClientAgent. Conversation ID and provider notification
// are deferred to end-of-run. For continuation tokens, the end-of-run handler needs
// to send the combined data from both the previous and current runs.
MarkAsPersisted(newRequestMessages);
MarkAsPersisted(chatResponse.Messages);
}
else
{
// In persist mode, persist immediately and update conversation ID.
agent.UpdateSessionConversationId(session, chatResponse.ConversationId, cancellationToken);
await agent.NotifyProvidersOfNewMessagesAsync(session, newRequestMessages, chatResponse.Messages, options, cancellationToken).ConfigureAwait(false);
MarkAsPersisted(newRequestMessages);
MarkAsPersisted(chatResponse.Messages);
}
}
/// <summary>
/// Gets the current <see cref="ChatClientAgent"/> and <see cref="ChatClientAgentSession"/> from the run context.
/// </summary>
private static (ChatClientAgent Agent, ChatClientAgentSession Session) GetRequiredAgentAndSession()
{
var runContext = AIAgent.CurrentRunContext
?? throw new InvalidOperationException(
$"{nameof(ChatHistoryPersistingChatClient)} can only be used within the context of a running AIAgent. " +
"Ensure that the chat client is being invoked as part of an AIAgent.RunAsync or AIAgent.RunStreamingAsync call.");
var chatClientAgent = runContext.Agent.GetService<ChatClientAgent>()
?? throw new InvalidOperationException(
$"{nameof(ChatHistoryPersistingChatClient)} can only be used with a {nameof(ChatClientAgent)}. " +
$"The current agent is of type '{runContext.Agent.GetType().Name}'.");
if (runContext.Session is not ChatClientAgentSession chatClientAgentSession)
{
throw new InvalidOperationException(
$"{nameof(ChatHistoryPersistingChatClient)} requires a {nameof(ChatClientAgentSession)}. " +
$"The current session is of type '{runContext.Session?.GetType().Name ?? "null"}'.");
}
return (chatClientAgent, chatClientAgentSession);
}
/// <summary>
/// Determines whether persistence should be deferred to end-of-run instead of happening immediately.
/// </summary>
/// <returns>
/// <see langword="true"/> when in <see cref="MarkOnly"/> mode, when the call is resuming from
/// a continuation token (since the end-of-run handler needs to combine data from the previous
/// and current runs), or when background responses are allowed (since the caller may stop
/// consuming the stream mid-run, preventing the post-stream persistence code from executing).
/// </returns>
private bool ShouldDeferPersistence(ChatOptions? options)
{
return this.MarkOnly || options?.ContinuationToken is not null || options?.AllowBackgroundResponses is true;
}
/// <summary>
/// Returns only the request messages that have not yet been persisted to chat history.
/// </summary>
/// <remarks>
/// A message is considered already persisted if any of the following is true:
/// <list type="bullet">
/// <item>It has the <see cref="PersistedMarkerKey"/> in its <see cref="ChatMessage.AdditionalProperties"/>.</item>
/// <item>It has an <see cref="AgentRequestMessageSourceType"/> of <see cref="AgentRequestMessageSourceType.ChatHistory"/>
/// (indicating it was loaded from chat history and does not need to be re-persisted).</item>
/// <item>It has <see cref="ChatMessage.Contents"/> and all of its <see cref="AIContent"/> items have the
/// <see cref="PersistedMarkerKey"/> in their <see cref="AIContent.AdditionalProperties"/>. This handles the
/// streaming case where <see cref="FunctionInvokingChatClient"/> reconstructs <see cref="ChatMessage"/> objects
/// independently via <c>ToChatResponse()</c>, producing different object references that share the same
/// underlying <see cref="AIContent"/> instances.</item>
/// </list>
/// </remarks>
/// <returns>A list of request messages that have not yet been persisted.</returns>
/// <param name="messages">The full set of request messages to filter.</param>
private static List<ChatMessage> GetNewRequestMessages(IEnumerable<ChatMessage> messages)
{
return messages.Where(m => !IsAlreadyPersisted(m)).ToList();
}
/// <summary>
/// Determines whether a message has already been persisted to chat history by this decorator.
/// </summary>
private static bool IsAlreadyPersisted(ChatMessage message)
{
if (message.AdditionalProperties?.TryGetValue(PersistedMarkerKey, out var value) == true && value is true)
{
return true;
}
if (message.GetAgentRequestMessageSourceType() == AgentRequestMessageSourceType.ChatHistory)
{
return true;
}
// In streaming mode, FunctionInvokingChatClient reconstructs ChatMessage objects via ToChatResponse()
// independently, producing different ChatMessage instances. However, the underlying AIContent objects
// (e.g., FunctionCallContent, FunctionResultContent) are shared references. Checking for markers on
// AIContent handles dedup in this case.
if (message.Contents.Count > 0 && message.Contents.All(c => c.AdditionalProperties?.TryGetValue(PersistedMarkerKey, out var value) == true && value is true))
{
return true;
}
return false;
}
/// <summary>
/// Marks the given messages as persisted by setting a marker on both the <see cref="ChatMessage"/>
/// and each of its <see cref="AIContent"/> items.
/// </summary>
/// <remarks>
/// Both levels are marked because <see cref="FunctionInvokingChatClient"/> may reconstruct
/// <see cref="ChatMessage"/> objects in streaming mode (losing the message-level marker),
/// but the <see cref="AIContent"/> references are shared and retain their markers.
/// </remarks>
/// <param name="messages">The messages to mark as persisted.</param>
private static void MarkAsPersisted(IEnumerable<ChatMessage> messages)
{
foreach (var message in messages)
{
message.AdditionalProperties ??= new();
message.AdditionalProperties[PersistedMarkerKey] = true;
foreach (var content in message.Contents)
{
content.AdditionalProperties ??= new();
content.AdditionalProperties[PersistedMarkerKey] = true;
}
}
}
/// <summary>
/// If the <paramref name="options"/> carry the <see cref="LocalHistoryConversationId"/> sentinel,
/// returns a clone with the conversation ID cleared so the inner client never sees it.
/// Otherwise returns the original <paramref name="options"/> unchanged.
/// </summary>
private static ChatOptions? StripLocalHistoryConversationId(ChatOptions? options)
{
if (options?.ConversationId == LocalHistoryConversationId)
{
options = options.Clone();
options.ConversationId = null;
}
return options;
}
}
@@ -1,273 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI;
/// <summary>
/// A delegating chat client that simulates service-stored chat history behavior using
/// framework-managed <see cref="ChatHistoryProvider"/> instances.
/// </summary>
/// <remarks>
/// <para>
/// This decorator is intended to operate between the <see cref="FunctionInvokingChatClient"/> and the leaf
/// <see cref="IChatClient"/> in a <see cref="ChatClientAgent"/> pipeline.
/// </para>
/// <para>
/// Before each service call, it loads chat history from the agent's <see cref="ChatHistoryProvider"/>
/// and prepends it to the request messages. After each successful service call, it persists
/// new request and response messages to the provider. It also returns a sentinel
/// <see cref="ChatOptions.ConversationId"/> on the response so that the
/// <see cref="FunctionInvokingChatClient"/> treats the conversation as service-managed —
/// clearing accumulated history between iterations and not injecting duplicate
/// <see cref="FunctionCallContent"/> during approval-response processing.
/// </para>
/// <para>
/// This chat client must be used within the context of a running <see cref="ChatClientAgent"/>. It retrieves the
/// current agent and session from <see cref="AIAgent.CurrentRunContext"/>, which is set automatically when an agent's
/// <see cref="AIAgent.RunAsync(IEnumerable{ChatMessage}, AgentSession?, AgentRunOptions?, CancellationToken)"/> or
/// <see cref="AIAgent.RunStreamingAsync(IEnumerable{ChatMessage}, AgentSession?, AgentRunOptions?, CancellationToken)"/>
/// method is called. The <see cref="ChatClientAgent"/> ensures the run context always contains a resolved session,
/// even when the caller passes null. An <see cref="InvalidOperationException"/> is thrown if no run context is
/// available or if the agent is not a <see cref="ChatClientAgent"/>.
/// </para>
/// </remarks>
internal sealed class ServiceStoredSimulatingChatClient : DelegatingChatClient
{
/// <summary>
/// A sentinel value returned on <see cref="ChatResponse.ConversationId"/> to signal
/// <see cref="FunctionInvokingChatClient"/> that chat history is being managed downstream.
/// </summary>
/// <remarks>
/// <para>
/// When <see cref="FunctionInvokingChatClient"/> sees a non-null <see cref="ChatResponse.ConversationId"/>,
/// it treats the conversation as service-managed: it clears accumulated history between
/// iterations (via <c>FixupHistories</c>) and does not inject <see cref="FunctionCallContent"/>
/// into the request during approval-response processing (via <c>ProcessFunctionApprovalResponses</c>).
/// </para>
/// <para>
/// This decorator strips the sentinel from <see cref="ChatOptions.ConversationId"/> on incoming
/// requests before forwarding to the inner client, so the underlying model never sees it.
/// </para>
/// </remarks>
internal const string LocalHistoryConversationId = "_agent_local_chat_history";
/// <summary>
/// Initializes a new instance of the <see cref="ServiceStoredSimulatingChatClient"/> class.
/// </summary>
/// <param name="innerClient">The underlying chat client that will handle the core operations.</param>
public ServiceStoredSimulatingChatClient(IChatClient innerClient)
: base(innerClient)
{
}
/// <inheritdoc/>
public override async Task<ChatResponse> GetResponseAsync(
IEnumerable<ChatMessage> messages,
ChatOptions? options = null,
CancellationToken cancellationToken = default)
{
var (agent, session) = GetRequiredAgentAndSession();
options = StripLocalHistoryConversationId(options);
bool isServiceManaged = !string.IsNullOrEmpty(options?.ConversationId);
bool isContinuationOrBackground = options?.ContinuationToken is not null
|| options?.AllowBackgroundResponses is true;
bool skipSimulation = isServiceManaged || isContinuationOrBackground;
var newMessages = messages as IList<ChatMessage> ?? messages.ToList();
// When simulating, load history and prepend it. When the service manages
// history (real ConversationId) or this is a continuation/background run,
// just forward the input messages as-is.
var messagesForService = skipSimulation
? newMessages
: await agent.LoadChatHistoryAsync(session, newMessages, options, cancellationToken).ConfigureAwait(false);
ChatResponse response;
try
{
response = await base.GetResponseAsync(messagesForService, options, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
await agent.NotifyProvidersOfFailureAsync(session, ex, newMessages, options, cancellationToken).ConfigureAwait(false);
throw;
}
await agent.NotifyProvidersOfNewMessagesAsync(session, newMessages, response.Messages, options, cancellationToken).ConfigureAwait(false);
if (isContinuationOrBackground)
{
// Continuation/background run — the agent's forced end-of-run handles
// session ConversationId and persistence; the decorator is a no-op.
}
else if (isServiceManaged || !string.IsNullOrEmpty(response.ConversationId))
{
// Service manages history — update session with the real ConversationId.
agent.UpdateSessionConversationId(session, response.ConversationId, cancellationToken);
}
else
{
// Normal simulated path — set sentinel so FICC treats this as service-managed.
SetSentinelConversationId(response, session);
}
return response;
}
/// <inheritdoc/>
public override async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
IEnumerable<ChatMessage> messages,
ChatOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
var (agent, session) = GetRequiredAgentAndSession();
options = StripLocalHistoryConversationId(options);
bool isServiceManaged = !string.IsNullOrEmpty(options?.ConversationId);
bool isContinuationOrBackground = options?.ContinuationToken is not null
|| options?.AllowBackgroundResponses is true;
bool skipSimulation = isServiceManaged || isContinuationOrBackground;
var newMessages = messages as IList<ChatMessage> ?? messages.ToList();
// When simulating, load history and prepend it. When the service manages
// history (real ConversationId) or this is a continuation/background run,
// just forward the input messages as-is.
var messagesForService = skipSimulation
? newMessages
: await agent.LoadChatHistoryAsync(session, newMessages, options, cancellationToken).ConfigureAwait(false);
List<ChatResponseUpdate> responseUpdates = [];
IAsyncEnumerator<ChatResponseUpdate> enumerator;
try
{
enumerator = base.GetStreamingResponseAsync(messagesForService, options, cancellationToken).GetAsyncEnumerator(cancellationToken);
}
catch (Exception ex)
{
await agent.NotifyProvidersOfFailureAsync(session, ex, newMessages, options, cancellationToken).ConfigureAwait(false);
throw;
}
bool hasUpdates;
try
{
hasUpdates = await enumerator.MoveNextAsync().ConfigureAwait(false);
}
catch (Exception ex)
{
await agent.NotifyProvidersOfFailureAsync(session, ex, newMessages, options, cancellationToken).ConfigureAwait(false);
throw;
}
while (hasUpdates)
{
var update = enumerator.Current;
responseUpdates.Add(update);
// If the service returned a real ConversationId on any update, remember that.
// Otherwise stamp our sentinel so FICC treats this as service-managed —
// unless this is a continuation/background run where the agent handles everything.
if (!string.IsNullOrEmpty(update.ConversationId))
{
isServiceManaged = true;
}
else if (!skipSimulation)
{
update.ConversationId = LocalHistoryConversationId;
}
yield return update;
try
{
hasUpdates = await enumerator.MoveNextAsync().ConfigureAwait(false);
}
catch (Exception ex)
{
await agent.NotifyProvidersOfFailureAsync(session, ex, newMessages, options, cancellationToken).ConfigureAwait(false);
throw;
}
}
var chatResponse = responseUpdates.ToChatResponse();
await agent.NotifyProvidersOfNewMessagesAsync(session, newMessages, chatResponse.Messages, options, cancellationToken).ConfigureAwait(false);
if (isContinuationOrBackground)
{
// Continuation/background run — the agent's forced end-of-run handles
// session ConversationId and persistence; the decorator is a no-op.
}
else if (isServiceManaged)
{
// Service manages history — update session with the real ConversationId.
agent.UpdateSessionConversationId(session, chatResponse.ConversationId, cancellationToken);
}
else
{
// Normal simulated path — set sentinel on session.
session.ConversationId = LocalHistoryConversationId;
}
}
/// <summary>
/// Sets the sentinel <see cref="LocalHistoryConversationId"/> on the response and session
/// so that <see cref="FunctionInvokingChatClient"/> treats the conversation as service-managed.
/// </summary>
private static void SetSentinelConversationId(ChatResponse response, ChatClientAgentSession session)
{
response.ConversationId = LocalHistoryConversationId;
session.ConversationId = LocalHistoryConversationId;
}
/// <summary>
/// Gets the current <see cref="ChatClientAgent"/> and <see cref="ChatClientAgentSession"/> from the run context.
/// </summary>
private static (ChatClientAgent Agent, ChatClientAgentSession Session) GetRequiredAgentAndSession()
{
var runContext = AIAgent.CurrentRunContext
?? throw new InvalidOperationException(
$"{nameof(ServiceStoredSimulatingChatClient)} can only be used within the context of a running AIAgent. " +
"Ensure that the chat client is being invoked as part of an AIAgent.RunAsync or AIAgent.RunStreamingAsync call.");
var chatClientAgent = runContext.Agent.GetService<ChatClientAgent>()
?? throw new InvalidOperationException(
$"{nameof(ServiceStoredSimulatingChatClient)} can only be used with a {nameof(ChatClientAgent)}. " +
$"The current agent is of type '{runContext.Agent.GetType().Name}'.");
if (runContext.Session is not ChatClientAgentSession chatClientAgentSession)
{
throw new InvalidOperationException(
$"{nameof(ServiceStoredSimulatingChatClient)} requires a {nameof(ChatClientAgentSession)}. " +
$"The current session is of type '{runContext.Session?.GetType().Name ?? "null"}'.");
}
return (chatClientAgent, chatClientAgentSession);
}
/// <summary>
/// If the <paramref name="options"/> carry the <see cref="LocalHistoryConversationId"/> sentinel,
/// returns a clone with the conversation ID cleared so the inner client never sees it.
/// Otherwise returns the original <paramref name="options"/> unchanged.
/// </summary>
private static ChatOptions? StripLocalHistoryConversationId(ChatOptions? options)
{
if (options?.ConversationId == LocalHistoryConversationId)
{
options = options.Clone();
options.ConversationId = null;
}
return options;
}
}
@@ -1,74 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- https://learn.microsoft.com/dotnet/fundamentals/package-validation/diagnostic-ids -->
<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.FileAgentSkillsProvider</Target>
<Left>lib/net10.0/Microsoft.Agents.AI.dll</Left>
<Right>lib/net10.0/Microsoft.Agents.AI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.FileAgentSkillsProviderOptions</Target>
<Left>lib/net10.0/Microsoft.Agents.AI.dll</Left>
<Right>lib/net10.0/Microsoft.Agents.AI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.FileAgentSkillsProvider</Target>
<Left>lib/net472/Microsoft.Agents.AI.dll</Left>
<Right>lib/net472/Microsoft.Agents.AI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.FileAgentSkillsProviderOptions</Target>
<Left>lib/net472/Microsoft.Agents.AI.dll</Left>
<Right>lib/net472/Microsoft.Agents.AI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.FileAgentSkillsProvider</Target>
<Left>lib/net8.0/Microsoft.Agents.AI.dll</Left>
<Right>lib/net8.0/Microsoft.Agents.AI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.FileAgentSkillsProviderOptions</Target>
<Left>lib/net8.0/Microsoft.Agents.AI.dll</Left>
<Right>lib/net8.0/Microsoft.Agents.AI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.FileAgentSkillsProvider</Target>
<Left>lib/net9.0/Microsoft.Agents.AI.dll</Left>
<Right>lib/net9.0/Microsoft.Agents.AI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.FileAgentSkillsProviderOptions</Target>
<Left>lib/net9.0/Microsoft.Agents.AI.dll</Left>
<Right>lib/net9.0/Microsoft.Agents.AI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.FileAgentSkillsProvider</Target>
<Left>lib/netstandard2.0/Microsoft.Agents.AI.dll</Left>
<Right>lib/netstandard2.0/Microsoft.Agents.AI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Microsoft.Agents.AI.FileAgentSkillsProviderOptions</Target>
<Left>lib/netstandard2.0/Microsoft.Agents.AI.dll</Left>
<Right>lib/netstandard2.0/Microsoft.Agents.AI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
</Suppressions>
@@ -1,35 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Shared.DiagnosticIds;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI;
/// <summary>
/// A skill source that holds <see cref="AgentSkill"/> instances in memory.
/// </summary>
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
internal sealed class AgentInMemorySkillsSource : AgentSkillsSource
{
private readonly List<AgentSkill> _skills;
/// <summary>
/// Initializes a new instance of the <see cref="AgentInMemorySkillsSource"/> class.
/// </summary>
/// <param name="skills">The skills to include in this source.</param>
public AgentInMemorySkillsSource(IEnumerable<AgentSkill> skills)
{
this._skills = Throw.IfNull(skills).ToList();
}
/// <inheritdoc/>
public override Task<IList<AgentSkill>> GetSkillsAsync(CancellationToken cancellationToken = default)
{
return Task.FromResult<IList<AgentSkill>>(this._skills);
}
}
@@ -12,8 +12,7 @@ namespace Microsoft.Agents.AI;
/// <remarks> /// <remarks>
/// <para> /// <para>
/// A skill represents a domain-specific capability with instructions, resources, and scripts. /// A skill represents a domain-specific capability with instructions, resources, and scripts.
/// Concrete implementations include <see cref="AgentFileSkill"/> (filesystem-backed) /// Concrete implementations include <see cref="AgentFileSkill"/> (filesystem-backed).
/// and <see cref="AgentInlineSkill"/> (code-defined).
/// </para> /// </para>
/// <para> /// <para>
/// Skill metadata follows the <see href="https://agentskills.io/specification">Agent Skills specification</see>. /// Skill metadata follows the <see href="https://agentskills.io/specification">Agent Skills specification</see>.
@@ -36,8 +35,6 @@ public abstract class AgentSkill
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// For file-based skills this is the raw SKILL.md file content. /// For file-based skills this is the raw SKILL.md file content.
/// For code-defined skills this is a synthesized XML document
/// containing name, description, and body (instructions, resources, scripts).
/// </remarks> /// </remarks>
public abstract string Content { get; } public abstract string Content { get; }
@@ -116,38 +116,6 @@ public sealed partial class AgentSkillsProvider : AIContextProvider
{ {
} }
/// <summary>
/// Initializes a new instance of the <see cref="AgentSkillsProvider"/> class
/// with one or more inline (code-defined) skills.
/// Duplicate skill names are automatically deduplicated (first occurrence wins).
/// </summary>
/// <param name="skills">The inline skills to include.</param>
public AgentSkillsProvider(params AgentInlineSkill[] skills)
: this(skills as IEnumerable<AgentInlineSkill>)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="AgentSkillsProvider"/> class
/// with inline (code-defined) skills.
/// Duplicate skill names are automatically deduplicated (first occurrence wins).
/// </summary>
/// <param name="skills">The inline skills to include.</param>
/// <param name="options">Optional provider configuration.</param>
/// <param name="loggerFactory">Optional logger factory.</param>
public AgentSkillsProvider(
IEnumerable<AgentInlineSkill> skills,
AgentSkillsProviderOptions? options = null,
ILoggerFactory? loggerFactory = null)
: this(
new DeduplicatingAgentSkillsSource(
new AgentInMemorySkillsSource(Throw.IfNull(skills)),
loggerFactory),
options,
loggerFactory)
{
}
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="AgentSkillsProvider"/> class /// Initializes a new instance of the <see cref="AgentSkillsProvider"/> class
/// from a custom <see cref="AgentSkillsSource"/>. Unlike other constructors, this one does not /// from a custom <see cref="AgentSkillsSource"/>. Unlike other constructors, this one does not
@@ -13,13 +13,9 @@ namespace Microsoft.Agents.AI;
/// Fluent builder for constructing an <see cref="AgentSkillsProvider"/> backed by a composite source. /// Fluent builder for constructing an <see cref="AgentSkillsProvider"/> backed by a composite source.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// <para>
/// Use this builder to combine multiple skill sources into a single provider:
/// </para>
/// <code> /// <code>
/// var provider = new AgentSkillsProviderBuilder() /// var provider = new AgentSkillsProviderBuilder()
/// .UseFileSkills("/path/to/skills") /// .UseFileSkills("/path/to/skills")
/// .UseSkills(myInlineSkill1, myInlineSkill2)
/// .Build(); /// .Build();
/// </code> /// </code>
/// </remarks> /// </remarks>
@@ -69,40 +65,6 @@ public sealed class AgentSkillsProviderBuilder
return this; return this;
} }
/// <summary>
/// Adds a single skill.
/// </summary>
/// <param name="skill">The skill to add.</param>
/// <returns>This builder instance for chaining.</returns>
public AgentSkillsProviderBuilder UseSkill(AgentSkill skill)
{
return this.UseSkills(skill);
}
/// <summary>
/// Adds one or more skills.
/// </summary>
/// <param name="skills">The skills to add.</param>
/// <returns>This builder instance for chaining.</returns>
public AgentSkillsProviderBuilder UseSkills(params AgentSkill[] skills)
{
var source = new AgentInMemorySkillsSource(skills);
this._sourceFactories.Add((_, _) => source);
return this;
}
/// <summary>
/// Adds skills from the specified collection.
/// </summary>
/// <param name="skills">The skills to add.</param>
/// <returns>This builder instance for chaining.</returns>
public AgentSkillsProviderBuilder UseSkills(IEnumerable<AgentSkill> skills)
{
var source = new AgentInMemorySkillsSource(skills);
this._sourceFactories.Add((_, _) => source);
return this;
}
/// <summary> /// <summary>
/// Adds a custom skill source. /// Adds a custom skill source.
/// </summary> /// </summary>
@@ -1,215 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.AI;
using Microsoft.Shared.DiagnosticIds;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI;
/// <summary>
/// A skill defined entirely in code with resources (static values or delegates) and scripts (delegates).
/// </summary>
/// <remarks>
/// All calls to <see cref="AddResource(string, object, string?)"/>,
/// <see cref="AddResource(string, Delegate, string?)"/>, and <see cref="AddScript"/>
/// must be made before the skill's <see cref="Content"/> is first accessed.
/// Calls made after that point will not be reflected in the generated
/// <see cref="Content"/>. In typical usage, this means configuring all
/// resources and scripts before registering the skill with an
/// <see cref="AgentSkillsProvider"/> or <see cref="AgentSkillsProviderBuilder"/>.
/// </remarks>
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
public sealed class AgentInlineSkill : AgentSkill
{
private readonly string _instructions;
private List<AgentSkillResource>? _resources;
private List<AgentSkillScript>? _scripts;
private string? _cachedContent;
/// <summary>
/// Initializes a new instance of the <see cref="AgentInlineSkill"/> class
/// with a pre-built <see cref="AgentSkillFrontmatter"/>.
/// </summary>
/// <param name="frontmatter">The skill frontmatter containing name, description, and other metadata.</param>
/// <param name="instructions">Skill instructions text.</param>
public AgentInlineSkill(AgentSkillFrontmatter frontmatter, string instructions)
{
this.Frontmatter = Throw.IfNull(frontmatter);
this._instructions = Throw.IfNullOrWhitespace(instructions);
}
/// <summary>
/// Initializes a new instance of the <see cref="AgentInlineSkill"/> class
/// with all frontmatter properties specified individually.
/// </summary>
/// <param name="name">Skill name in kebab-case.</param>
/// <param name="description">Skill description for discovery.</param>
/// <param name="instructions">Skill instructions text.</param>
/// <param name="license">Optional license name or reference.</param>
/// <param name="compatibility">Optional compatibility information (max 500 chars).</param>
/// <param name="allowedTools">Optional space-delimited list of pre-approved tools.</param>
/// <param name="metadata">Optional arbitrary key-value metadata.</param>
public AgentInlineSkill(
string name,
string description,
string instructions,
string? license = null,
string? compatibility = null,
string? allowedTools = null,
AdditionalPropertiesDictionary? metadata = null)
: this(
new AgentSkillFrontmatter(name, description, compatibility)
{
License = license,
AllowedTools = allowedTools,
Metadata = metadata,
},
instructions)
{
}
/// <inheritdoc/>
public override AgentSkillFrontmatter Frontmatter { get; }
/// <inheritdoc/>
public override string Content => this._cachedContent ??= this.BuildContent();
/// <inheritdoc/>
public override IReadOnlyList<AgentSkillResource>? Resources => this._resources;
/// <inheritdoc/>
public override IReadOnlyList<AgentSkillScript>? Scripts => this._scripts;
/// <summary>
/// Registers a static resource with this skill.
/// </summary>
/// <param name="name">The resource name.</param>
/// <param name="value">The static resource value.</param>
/// <param name="description">An optional description of the resource.</param>
/// <returns>This instance, for chaining.</returns>
public AgentInlineSkill AddResource(string name, object value, string? description = null)
{
(this._resources ??= []).Add(new AgentInlineSkillResource(name, value, description));
return this;
}
/// <summary>
/// Registers a dynamic resource with this skill, backed by a C# delegate.
/// The delegate's parameters and return type are automatically marshaled via <c>AIFunctionFactory</c>.
/// </summary>
/// <param name="name">The resource name.</param>
/// <param name="method">A method that produces the resource value when requested.</param>
/// <param name="description">An optional description of the resource.</param>
/// <returns>This instance, for chaining.</returns>
public AgentInlineSkill AddResource(string name, Delegate method, string? description = null)
{
(this._resources ??= []).Add(new AgentInlineSkillResource(name, method, description));
return this;
}
/// <summary>
/// Registers a script with this skill, backed by a C# delegate.
/// The delegate's parameters and return type are automatically marshaled via <c>AIFunctionFactory</c>.
/// </summary>
/// <param name="name">The script name.</param>
/// <param name="method">A method to execute when the script is invoked.</param>
/// <param name="description">An optional description of the script.</param>
/// <returns>This instance, for chaining.</returns>
public AgentInlineSkill AddScript(string name, Delegate method, string? description = null)
{
(this._scripts ??= []).Add(new AgentInlineSkillScript(name, method, description));
return this;
}
private string BuildContent()
{
var sb = new StringBuilder();
sb.Append($"<name>{EscapeXmlString(this.Frontmatter.Name)}</name>\n")
.Append($"<description>{EscapeXmlString(this.Frontmatter.Description)}</description>\n\n")
.Append("<instructions>\n")
.Append(EscapeXmlString(this._instructions))
.Append("\n</instructions>");
if (this.Resources is { Count: > 0 })
{
sb.Append("\n\n<resources>\n");
foreach (var resource in this.Resources)
{
if (resource.Description is not null)
{
sb.Append($" <resource name=\"{EscapeXmlString(resource.Name)}\" description=\"{EscapeXmlString(resource.Description)}\"/>\n");
}
else
{
sb.Append($" <resource name=\"{EscapeXmlString(resource.Name)}\"/>\n");
}
}
sb.Append("</resources>");
}
if (this.Scripts is { Count: > 0 })
{
sb.Append("\n\n<scripts>\n");
foreach (var script in this.Scripts)
{
JsonElement? parametersSchema = ((AgentInlineSkillScript)script).ParametersSchema;
if (script.Description is null && parametersSchema is null)
{
sb.Append($" <script name=\"{EscapeXmlString(script.Name)}\"/>\n");
}
else
{
sb.Append(script.Description is not null
? $" <script name=\"{EscapeXmlString(script.Name)}\" description=\"{EscapeXmlString(script.Description)}\">\n"
: $" <script name=\"{EscapeXmlString(script.Name)}\">\n");
if (parametersSchema is not null)
{
sb.Append($" <parameters_schema>{EscapeXmlString(parametersSchema.Value.GetRawText(), preserveQuotes: true)}</parameters_schema>\n");
}
sb.Append(" </script>\n");
}
}
sb.Append("</scripts>");
}
return sb.ToString();
}
/// <summary>
/// Escapes XML special characters: always escapes <c>&amp;</c>, <c>&lt;</c>, <c>&gt;</c>,
/// <c>&quot;</c>, and <c>&apos;</c>. When <paramref name="preserveQuotes"/> is <see langword="true"/>,
/// quotes are left unescaped to preserve readability of embedded content such as JSON.
/// </summary>
/// <param name="value">The string to escape.</param>
/// <param name="preserveQuotes">
/// When <see langword="true"/>, leaves <c>"</c> and <c>'</c> unescaped for use in XML element content (e.g., JSON).
/// When <see langword="false"/> (default), escapes all XML special characters including quotes.
/// </param>
private static string EscapeXmlString(string value, bool preserveQuotes = false)
{
var result = value
.Replace("&", "&amp;")
.Replace("<", "&lt;")
.Replace(">", "&gt;");
if (!preserveQuotes)
{
result = result
.Replace("\"", "&quot;")
.Replace("'", "&apos;");
}
return result;
}
}
@@ -1,60 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Microsoft.Shared.DiagnosticIds;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI;
/// <summary>
/// A skill resource defined in code, backed by either a static value or a delegate.
/// </summary>
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
internal sealed class AgentInlineSkillResource : AgentSkillResource
{
private readonly object? _value;
private readonly AIFunction? _function;
/// <summary>
/// Initializes a new instance of the <see cref="AgentInlineSkillResource"/> class with a static value.
/// The value is returned as-is when <see cref="ReadAsync"/> is called.
/// </summary>
/// <param name="name">The resource name.</param>
/// <param name="value">The static resource value.</param>
/// <param name="description">An optional description of the resource.</param>
public AgentInlineSkillResource(string name, object value, string? description = null)
: base(name, description)
{
this._value = Throw.IfNull(value);
}
/// <summary>
/// Initializes a new instance of the <see cref="AgentInlineSkillResource"/> class with a delegate.
/// The delegate is invoked via an <see cref="AIFunction"/> each time <see cref="ReadAsync"/> is called,
/// producing a dynamic (computed) value.
/// </summary>
/// <param name="name">The resource name.</param>
/// <param name="method">A method that produces the resource value when requested.</param>
/// <param name="description">An optional description of the resource.</param>
public AgentInlineSkillResource(string name, Delegate method, string? description = null)
: base(name, description)
{
Throw.IfNull(method);
this._function = AIFunctionFactory.Create(method, name: this.Name);
}
/// <inheritdoc/>
public override async Task<object?> ReadAsync(IServiceProvider? serviceProvider = null, CancellationToken cancellationToken = default)
{
if (this._function is not null)
{
return await this._function.InvokeAsync(new AIFunctionArguments() { Services = serviceProvider }, cancellationToken).ConfigureAwait(false);
}
return this._value;
}
}
@@ -1,46 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Microsoft.Shared.DiagnosticIds;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI;
/// <summary>
/// A skill script backed by a delegate.
/// </summary>
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
internal sealed class AgentInlineSkillScript : AgentSkillScript
{
private readonly AIFunction _function;
/// <summary>
/// Initializes a new instance of the <see cref="AgentInlineSkillScript"/> class from a delegate.
/// The delegate's parameters and return type are automatically marshaled via <see cref="AIFunctionFactory"/>.
/// </summary>
/// <param name="name">The script name.</param>
/// <param name="method">A method to execute when the script is invoked. Parameters are automatically deserialized from JSON.</param>
/// <param name="description">An optional description of the script.</param>
public AgentInlineSkillScript(string name, Delegate method, string? description = null)
: base(Throw.IfNullOrWhitespace(name), description)
{
Throw.IfNull(method);
this._function = AIFunctionFactory.Create(method, name: this.Name);
}
/// <summary>
/// Gets the JSON schema describing the parameters accepted by this script, or <see langword="null"/> if not available.
/// </summary>
public JsonElement? ParametersSchema => this._function.JsonSchema;
/// <inheritdoc/>
public override async Task<object?> RunAsync(AgentSkill skill, AIFunctionArguments arguments, CancellationToken cancellationToken = default)
{
return await this._function.InvokeAsync(arguments, cancellationToken).ConfigureAwait(false);
}
}
@@ -1,51 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.Agents.AI.UnitTests.AgentSkills;
/// <summary>
/// Unit tests for <see cref="AgentInMemorySkillsSource"/>.
/// </summary>
public sealed class AgentInMemorySkillsSourceTests
{
[Fact]
public async Task GetSkillsAsync_ValidSkills_ReturnsAllAsync()
{
// Arrange
var skills = new AgentSkill[]
{
new AgentInlineSkill("my-skill", "A valid skill.", "Instructions."),
new AgentInlineSkill("another", "Another valid skill.", "More instructions."),
};
var source = new AgentInMemorySkillsSource(skills);
// Act
var result = await source.GetSkillsAsync(CancellationToken.None);
// Assert
Assert.Equal(2, result.Count);
Assert.Equal("my-skill", result[0].Frontmatter.Name);
Assert.Equal("another", result[1].Frontmatter.Name);
}
[Theory]
[InlineData("INVALID-NAME")]
[InlineData("-leading")]
[InlineData("trailing-")]
public void Constructor_InvalidFrontmatter_ThrowsArgumentException(string invalidName)
{
// Act & Assert
Assert.Throws<ArgumentException>(() =>
new AgentInlineSkill(invalidName, "A skill.", "Instructions."));
}
[Fact]
public void Constructor_NullSkills_Throws()
{
// Act & Assert
Assert.Throws<ArgumentNullException>(() => new AgentInMemorySkillsSource(null!));
}
}
@@ -1,155 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.Agents.AI.UnitTests.AgentSkills;
/// <summary>
/// Unit tests for <see cref="AgentInlineSkillResource"/>.
/// </summary>
public sealed class AgentInlineSkillResourceTests
{
[Fact]
public async Task ReadAsync_StaticValue_ReturnsValueAsync()
{
// Arrange
var resource = new AgentInlineSkillResource("config", "my-value");
// Act
var result = await resource.ReadAsync();
// Assert
Assert.Equal("my-value", result);
}
[Fact]
public async Task ReadAsync_StaticObjectValue_ReturnsSameInstanceAsync()
{
// Arrange
var obj = new object();
var resource = new AgentInlineSkillResource("ref", obj);
// Act
var result = await resource.ReadAsync();
// Assert
Assert.Same(obj, result);
}
[Fact]
public async Task ReadAsync_Delegate_InvokesFunctionAsync()
{
// Arrange
int callCount = 0;
var resource = new AgentInlineSkillResource("dynamic", () =>
{
callCount++;
return "computed";
});
// Act
var result = await resource.ReadAsync();
// Assert
Assert.Equal("computed", result?.ToString());
Assert.Equal(1, callCount);
}
[Fact]
public async Task ReadAsync_Delegate_InvokesEachTimeAsync()
{
// Arrange
int callCount = 0;
var resource = new AgentInlineSkillResource("counter", () => ++callCount);
// Act
await resource.ReadAsync();
await resource.ReadAsync();
var result = await resource.ReadAsync();
// Assert
Assert.Equal(3, callCount);
}
[Fact]
public void Constructor_StaticValue_SetsNameAndDescription()
{
// Arrange & Act
var resource = new AgentInlineSkillResource("my-res", "val", "A description.");
// Assert
Assert.Equal("my-res", resource.Name);
Assert.Equal("A description.", resource.Description);
}
[Fact]
public void Constructor_StaticValue_NullDescription_DescriptionIsNull()
{
// Arrange & Act
var resource = new AgentInlineSkillResource("my-res", "val");
// Assert
Assert.Null(resource.Description);
}
[Fact]
public void Constructor_StaticValue_NullValue_Throws()
{
// Act & Assert — cast needed to target the object overload
#pragma warning disable IDE0004
Assert.Throws<ArgumentNullException>(() =>
new AgentInlineSkillResource("my-res", (object)null!));
#pragma warning restore IDE0004
}
[Fact]
public void Constructor_Delegate_NullMethod_Throws()
{
// Act & Assert
Assert.Throws<ArgumentNullException>(() =>
new AgentInlineSkillResource("my-res", null!));
}
[Fact]
public void Constructor_NullName_Throws()
{
// Act & Assert
Assert.Throws<ArgumentNullException>(() =>
new AgentInlineSkillResource(null!, "val"));
}
[Fact]
public void Constructor_WhitespaceName_Throws()
{
// Act & Assert
Assert.Throws<ArgumentException>(() =>
new AgentInlineSkillResource(" ", "val"));
}
[Fact]
public void Constructor_Delegate_SetsNameAndDescription()
{
// Arrange & Act
var resource = new AgentInlineSkillResource("dyn-res", () => "hello", "Dynamic resource.");
// Assert
Assert.Equal("dyn-res", resource.Name);
Assert.Equal("Dynamic resource.", resource.Description);
}
[Fact]
public async Task ReadAsync_SupportsCancellationTokenAsync()
{
// Arrange
using var cts = new CancellationTokenSource();
var resource = new AgentInlineSkillResource("cancellable", "value");
// Act — should not throw with a non-cancelled token
var result = await resource.ReadAsync(cancellationToken: cts.Token);
// Assert
Assert.Equal("value", result);
}
}
@@ -1,132 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.UnitTests.AgentSkills;
/// <summary>
/// Unit tests for <see cref="AgentInlineSkillScript"/>.
/// </summary>
public sealed class AgentInlineSkillScriptTests
{
[Fact]
public async Task RunAsync_InvokesDelegate_ReturnsResultAsync()
{
// Arrange
var script = new AgentInlineSkillScript("greet", () => "hello");
var skill = new AgentInlineSkill("test-skill", "Test.", "Instructions.");
// Act
var result = await script.RunAsync(skill, new AIFunctionArguments(), CancellationToken.None);
// Assert
Assert.Equal("hello", result?.ToString());
}
[Fact]
public async Task RunAsync_WithParameters_PassesArgumentsAsync()
{
// Arrange
var script = new AgentInlineSkillScript("add", (int a, int b) => a + b);
var skill = new AgentInlineSkill("calc-skill", "Calc.", "Instructions.");
var args = new AIFunctionArguments { ["a"] = 3, ["b"] = 7 };
// Act
var result = await script.RunAsync(skill, args, CancellationToken.None);
// Assert
Assert.Equal(10, int.Parse(result?.ToString()!));
}
[Fact]
public void ParametersSchema_NoParameters_ReturnsSchema()
{
// Arrange
var script = new AgentInlineSkillScript("noop", () => "ok");
// Act
var schema = script.ParametersSchema;
// Assert — parameterless delegates still produce a schema
Assert.NotNull(schema);
}
[Fact]
public void ParametersSchema_WithParameters_ContainsPropertyNames()
{
// Arrange
var script = new AgentInlineSkillScript("search", (string query, int limit) => $"{query}:{limit}");
// Act
var schema = script.ParametersSchema;
// Assert
Assert.NotNull(schema);
var schemaText = schema!.Value.GetRawText();
Assert.Contains("query", schemaText);
Assert.Contains("limit", schemaText);
}
[Fact]
public void Constructor_SetsNameAndDescription()
{
// Arrange & Act
var script = new AgentInlineSkillScript("my-script", () => "ok", "Does something.");
// Assert
Assert.Equal("my-script", script.Name);
Assert.Equal("Does something.", script.Description);
}
[Fact]
public void Constructor_NullDescription_DescriptionIsNull()
{
// Arrange & Act
var script = new AgentInlineSkillScript("my-script", () => "ok");
// Assert
Assert.Null(script.Description);
}
[Fact]
public void Constructor_NullName_Throws()
{
// Act & Assert
Assert.Throws<ArgumentNullException>(() =>
new AgentInlineSkillScript(null!, () => "ok"));
}
[Fact]
public void Constructor_WhitespaceName_Throws()
{
// Act & Assert
Assert.Throws<ArgumentException>(() =>
new AgentInlineSkillScript(" ", () => "ok"));
}
[Fact]
public void Constructor_NullMethod_Throws()
{
// Act & Assert
Assert.Throws<ArgumentNullException>(() =>
new AgentInlineSkillScript("my-script", null!));
}
[Fact]
public async Task RunAsync_StringParameter_WorksAsync()
{
// Arrange
var script = new AgentInlineSkillScript("echo", (string message) => message);
var skill = new AgentInlineSkill("test-skill", "Test.", "Instructions.");
var args = new AIFunctionArguments { ["message"] = "hello world" };
// Act
var result = await script.RunAsync(skill, args, CancellationToken.None);
// Assert
Assert.Equal("hello world", result?.ToString());
}
}
@@ -1,420 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.UnitTests.AgentSkills;
/// <summary>
/// Unit tests for <see cref="AgentInlineSkill"/>.
/// </summary>
public sealed class AgentInlineSkillTests
{
[Fact]
public void Constructor_WithNameAndDescription_SetsFrontmatter()
{
// Arrange & Act
var skill = new AgentInlineSkill("my-skill", "A valid skill.", "Instructions.");
// Assert
Assert.Equal("my-skill", skill.Frontmatter.Name);
Assert.Equal("A valid skill.", skill.Frontmatter.Description);
Assert.Null(skill.Frontmatter.License);
Assert.Null(skill.Frontmatter.Compatibility);
Assert.Null(skill.Frontmatter.AllowedTools);
Assert.Null(skill.Frontmatter.Metadata);
}
[Fact]
public void Constructor_WithAllProps_SetsFrontmatter()
{
// Arrange
var metadata = new AdditionalPropertiesDictionary { ["key"] = "value" };
// Act
var skill = new AgentInlineSkill(
"my-skill",
"A valid skill.",
"Instructions.",
license: "MIT",
compatibility: "gpt-4",
allowedTools: "tool-a tool-b",
metadata: metadata);
// Assert
Assert.Equal("my-skill", skill.Frontmatter.Name);
Assert.Equal("A valid skill.", skill.Frontmatter.Description);
Assert.Equal("MIT", skill.Frontmatter.License);
Assert.Equal("gpt-4", skill.Frontmatter.Compatibility);
Assert.Equal("tool-a tool-b", skill.Frontmatter.AllowedTools);
Assert.NotNull(skill.Frontmatter.Metadata);
Assert.Equal("value", skill.Frontmatter.Metadata["key"]);
}
[Fact]
public void Constructor_WithFrontmatter_UsesFrontmatterDirectly()
{
// Arrange
var frontmatter = new AgentSkillFrontmatter("my-skill", "A valid skill.")
{
License = "Apache-2.0",
Compatibility = "gpt-4",
AllowedTools = "tool-a",
Metadata = new AdditionalPropertiesDictionary { ["env"] = "prod" },
};
// Act
var skill = new AgentInlineSkill(frontmatter, "Instructions.");
// Assert
Assert.Same(frontmatter, skill.Frontmatter);
Assert.Equal("Apache-2.0", skill.Frontmatter.License);
Assert.Equal("gpt-4", skill.Frontmatter.Compatibility);
Assert.Equal("tool-a", skill.Frontmatter.AllowedTools);
Assert.Equal("prod", skill.Frontmatter.Metadata!["env"]);
}
[Fact]
public void Constructor_WithFrontmatter_NullFrontmatter_Throws()
{
// Act & Assert
Assert.Throws<ArgumentNullException>(() =>
new AgentInlineSkill(null!, "Instructions."));
}
[Fact]
public void Constructor_WithFrontmatter_NullInstructions_Throws()
{
// Arrange
var frontmatter = new AgentSkillFrontmatter("my-skill", "A valid skill.");
// Act & Assert
Assert.Throws<ArgumentNullException>(() =>
new AgentInlineSkill(frontmatter, null!));
}
[Fact]
public void Constructor_WithAllProps_NullInstructions_Throws()
{
// Act & Assert
Assert.Throws<ArgumentNullException>(() =>
new AgentInlineSkill("my-skill", "A valid skill.", null!));
}
[Fact]
public void Content_ContainsNameDescriptionAndInstructions()
{
// Arrange
var skill = new AgentInlineSkill("my-skill", "A valid skill.", "Do the thing.");
// Act
var content = skill.Content;
// Assert
Assert.Contains("<name>my-skill</name>", content);
Assert.Contains("<description>A valid skill.</description>", content);
Assert.Contains("<instructions>\nDo the thing.\n</instructions>", content);
}
[Fact]
public void Content_EscapesXmlCharacters()
{
// Arrange
var skill = new AgentInlineSkill("my-skill", "x<y>z\"w & it's more", "1 & 2 < 3");
// Act
var content = skill.Content;
// Assert
Assert.Contains("<name>my-skill</name>", content);
Assert.Contains("<description>x&lt;y&gt;z&quot;w &amp; it&apos;s more</description>", content);
Assert.Contains("1 &amp; 2 &lt; 3", content); // instructions are escaped
}
[Fact]
public void Content_IsCachedAcrossAccesses()
{
// Arrange
var skill = new AgentInlineSkill("my-skill", "A valid skill.", "Instructions.");
// Act
var first = skill.Content;
var second = skill.Content;
// Assert
Assert.Same(first, second);
}
[Fact]
public void Content_IncludesResourcesAddedBeforeFirstAccess()
{
// Arrange
var skill = new AgentInlineSkill("my-skill", "A valid skill.", "Instructions.");
skill.AddResource("config", "value1", "A config resource.");
// Act
var content = skill.Content;
// Assert
Assert.Contains("<resources>", content);
Assert.Contains("config", content);
}
[Fact]
public void Content_IncludesDelegateResourcesAddedBeforeFirstAccess()
{
// Arrange
var skill = new AgentInlineSkill("my-skill", "A valid skill.", "Instructions.");
skill.AddResource("dynamic", () => "hello");
// Act
var content = skill.Content;
// Assert
Assert.Contains("<resources>", content);
Assert.Contains("dynamic", content);
}
[Fact]
public void Content_IncludesScriptsAddedBeforeFirstAccess()
{
// Arrange
var skill = new AgentInlineSkill("my-skill", "A valid skill.", "Instructions.");
skill.AddScript("run", () => "result", "Runs something.");
// Act
var content = skill.Content;
// Assert
Assert.Contains("<scripts>", content);
Assert.Contains("run", content);
}
[Fact]
public void Content_IsCachedAndNotRebuilt()
{
// Arrange
var skill = new AgentInlineSkill("my-skill", "A valid skill.", "Instructions.");
skill.AddResource("r1", "v1");
// Act
var first = skill.Content;
var second = skill.Content;
// Assert
Assert.Same(first, second);
}
[Fact]
public void Content_IncludesResourcesAndScriptsAddedBeforeFirstAccess()
{
// Arrange
var skill = new AgentInlineSkill("my-skill", "A valid skill.", "Instructions.");
skill.AddResource("r1", "v1");
skill.AddScript("s1", () => "ok");
// Act
var content = skill.Content;
// Assert
Assert.Contains("<resources>", content);
Assert.Contains("r1", content);
Assert.Contains("<scripts>", content);
Assert.Contains("s1", content);
}
[Fact]
public void Content_ParametersSchema_IsXmlEscaped()
{
// Arrange
var skill = new AgentInlineSkill("my-skill", "A valid skill.", "Instructions.");
skill.AddScript("search", (string query, int limit) => $"found {limit} results for {query}");
// Act
var content = skill.Content;
// Assert — JSON schema should be present and XML content chars escaped
Assert.Contains("parameters_schema", content);
Assert.DoesNotContain("<![CDATA[", content);
}
[Fact]
public void AddResource_NullValue_Throws()
{
// Arrange
var skill = new AgentInlineSkill("my-skill", "A valid skill.", "Instructions.");
// Act & Assert — cast needed to target the object overload
#pragma warning disable IDE0004
Assert.Throws<ArgumentNullException>(() => skill.AddResource("config", (object)null!));
#pragma warning restore IDE0004
}
[Fact]
public void AddResource_NullDelegate_Throws()
{
// Arrange
var skill = new AgentInlineSkill("my-skill", "A valid skill.", "Instructions.");
// Act & Assert
Assert.Throws<ArgumentNullException>(() => skill.AddResource("config", null!));
}
[Fact]
public void AddScript_NullDelegate_Throws()
{
// Arrange
var skill = new AgentInlineSkill("my-skill", "A valid skill.", "Instructions.");
// Act & Assert
Assert.Throws<ArgumentNullException>(() => skill.AddScript("run", null!));
}
[Fact]
public void Resources_WhenNoneAdded_ReturnsNull()
{
// Arrange
var skill = new AgentInlineSkill("my-skill", "A valid skill.", "Instructions.");
// Act & Assert
Assert.Null(skill.Resources);
}
[Fact]
public void Scripts_WhenNoneAdded_ReturnsNull()
{
// Arrange
var skill = new AgentInlineSkill("my-skill", "A valid skill.", "Instructions.");
// Act & Assert
Assert.Null(skill.Scripts);
}
[Fact]
public void AddResource_ReturnsSameInstance_ForChaining()
{
// Arrange
var skill = new AgentInlineSkill("my-skill", "A valid skill.", "Instructions.");
// Act
var returned = skill.AddResource("r1", "v1");
// Assert
Assert.Same(skill, returned);
}
[Fact]
public void AddResource_Delegate_ReturnsSameInstance_ForChaining()
{
// Arrange
var skill = new AgentInlineSkill("my-skill", "A valid skill.", "Instructions.");
// Act
var returned = skill.AddResource("r1", () => "v1");
// Assert
Assert.Same(skill, returned);
}
[Fact]
public void AddScript_ReturnsSameInstance_ForChaining()
{
// Arrange
var skill = new AgentInlineSkill("my-skill", "A valid skill.", "Instructions.");
// Act
var returned = skill.AddScript("s1", () => "ok");
// Assert
Assert.Same(skill, returned);
}
[Fact]
public void Content_NoResourcesOrScripts_DoesNotContainResourcesOrScriptsTags()
{
// Arrange
var skill = new AgentInlineSkill("my-skill", "A valid skill.", "Instructions.");
// Act
var content = skill.Content;
// Assert
Assert.DoesNotContain("<resources>", content);
Assert.DoesNotContain("<scripts>", content);
}
[Fact]
public void Content_ResourcesAddedAfterCaching_AreNotIncluded()
{
// Arrange
var skill = new AgentInlineSkill("my-skill", "A valid skill.", "Instructions.");
_ = skill.Content; // trigger caching
skill.AddResource("late-resource", "late-value");
// Act
var content = skill.Content;
// Assert — the late resource should not appear because content was cached
Assert.DoesNotContain("late-resource", content);
}
[Fact]
public void Content_ScriptsAddedAfterCaching_AreNotIncluded()
{
// Arrange
var skill = new AgentInlineSkill("my-skill", "A valid skill.", "Instructions.");
_ = skill.Content; // trigger caching
skill.AddScript("late-script", () => "late");
// Act
var content = skill.Content;
// Assert — the late script should not appear because content was cached
Assert.DoesNotContain("late-script", content);
}
[Fact]
public void Content_ScriptWithDescription_IncludesDescriptionAttribute()
{
// Arrange
var skill = new AgentInlineSkill("my-skill", "A valid skill.", "Instructions.");
skill.AddScript("my-script", () => "ok", "Runs something.");
// Act
var content = skill.Content;
// Assert
Assert.Contains("description=\"Runs something.\"", content);
}
[Fact]
public void Content_ScriptWithoutParametersOrDescription_UsesSelfClosingTag()
{
// Arrange
var skill = new AgentInlineSkill("my-skill", "A valid skill.", "Instructions.");
skill.AddScript("simple", () => "ok");
// Act
var content = skill.Content;
// Assert — parameterless Action delegates still produce a schema, so this
// verifies the script is at least included in the output
Assert.Contains("simple", content);
}
[Fact]
public void Content_ResourceWithDescription_IncludesDescriptionAttribute()
{
// Arrange
var skill = new AgentInlineSkill("my-skill", "A valid skill.", "Instructions.");
skill.AddResource("with-desc", "value", "A described resource.");
skill.AddResource("no-desc", "value");
// Act
var content = skill.Content;
// Assert
Assert.Contains("description=\"A described resource.\"", content);
Assert.DoesNotContain("no-desc\" description", content);
}
}
@@ -270,7 +270,7 @@ public sealed class AgentSkillsProviderTests : IDisposable
// Arrange // Arrange
var source = new CountingAgentSkillsSource( var source = new CountingAgentSkillsSource(
[ [
new AgentInlineSkill("concurrent-skill", "Concurrent test", "Body.") new TestAgentSkill("concurrent-skill", "Concurrent test", "Body.")
]); ]);
var provider = new AgentSkillsProvider(source); var provider = new AgentSkillsProvider(source);
@@ -502,7 +502,7 @@ public sealed class AgentSkillsProviderTests : IDisposable
// Arrange // Arrange
var source = new CountingAgentSkillsSource( var source = new CountingAgentSkillsSource(
[ [
new AgentInlineSkill("no-cache-skill", "No cache test", "Body.") new TestAgentSkill("no-cache-skill", "No cache test", "Body.")
]); ]);
var provider = new AgentSkillsProviderBuilder() var provider = new AgentSkillsProviderBuilder()
.UseSource(source) .UseSource(source)
@@ -525,7 +525,7 @@ public sealed class AgentSkillsProviderTests : IDisposable
// Arrange // Arrange
var source = new CountingAgentSkillsSource( var source = new CountingAgentSkillsSource(
[ [
new AgentInlineSkill("cached-skill", "Cached test", "Body.") new TestAgentSkill("cached-skill", "Cached test", "Body.")
]); ]);
var provider = new AgentSkillsProviderBuilder() var provider = new AgentSkillsProviderBuilder()
.UseSource(source) .UseSource(source)
@@ -547,7 +547,7 @@ public sealed class AgentSkillsProviderTests : IDisposable
// Arrange // Arrange
var source = new CountingAgentSkillsSource( var source = new CountingAgentSkillsSource(
[ [
new AgentInlineSkill("default-skill", "Default test", "Body.") new TestAgentSkill("default-skill", "Default test", "Body.")
]); ]);
var provider = new AgentSkillsProviderBuilder() var provider = new AgentSkillsProviderBuilder()
.UseSource(source) .UseSource(source)
@@ -563,78 +563,6 @@ public sealed class AgentSkillsProviderTests : IDisposable
Assert.Equal(1, source.GetSkillsCallCount); Assert.Equal(1, source.GetSkillsCallCount);
} }
[Fact]
public async Task Build_PreservesSourceRegistrationOrderAsync()
{
// Arrange — register file, inline, file in that order
string dir1 = Path.Combine(this._testRoot, "dir1");
string dir2 = Path.Combine(this._testRoot, "dir2");
CreateSkillIn(dir1, "file-skill-1", "First file skill", "Body 1.");
CreateSkillIn(dir2, "file-skill-2", "Second file skill", "Body 2.");
var inlineSkill = new AgentInlineSkill("inline-skill", "Inline skill", "Body inline.");
var provider = new AgentSkillsProviderBuilder()
.UseFileSkill(dir1)
.UseSkills(inlineSkill)
.UseFileSkill(dir2)
.UseFileScriptRunner(s_noOpExecutor)
.UseOptions(o => o.DisableCaching = true)
.Build();
var invokingContext = new AIContextProvider.InvokingContext(this._agent, session: null, new AIContext());
// Act
var result = await provider.InvokingAsync(invokingContext, CancellationToken.None);
// Assert — all three skills should be present in alphabetical order in the prompt
Assert.NotNull(result.Instructions);
var instructions = result.Instructions!;
var indexFileSkill1 = instructions.IndexOf("file-skill-1", StringComparison.Ordinal);
var indexFileSkill2 = instructions.IndexOf("file-skill-2", StringComparison.Ordinal);
var indexInlineSkill = instructions.IndexOf("inline-skill", StringComparison.Ordinal);
Assert.True(indexFileSkill1 >= 0, "file-skill-1 should be present in the instructions.");
Assert.True(indexFileSkill2 >= 0, "file-skill-2 should be present in the instructions.");
Assert.True(indexInlineSkill >= 0, "inline-skill should be present in the instructions.");
Assert.True(indexFileSkill1 < indexFileSkill2, "file-skill-1 should appear before file-skill-2.");
Assert.True(indexFileSkill2 < indexInlineSkill, "file-skill-2 should appear before inline-skill.");
}
[Fact]
public async Task Build_MixedSources_AllSkillsDiscoveredAsync()
{
// Arrange — use UseSource, UseSkill, and UseFileSkill in mixed order
string dir = Path.Combine(this._testRoot, "mixed-dir");
CreateSkillIn(dir, "file-skill", "File skill", "Body file.");
var inlineSkill = new AgentInlineSkill("inline-skill", "Inline skill", "Body inline.");
var customSource = new CountingAgentSkillsSource(
[
new AgentInlineSkill("custom-skill", "Custom source skill", "Body custom.")
]);
var provider = new AgentSkillsProviderBuilder()
.UseSource(customSource)
.UseSkills(inlineSkill)
.UseFileSkill(dir)
.UseFileScriptRunner(s_noOpExecutor)
.UseOptions(o => o.DisableCaching = true)
.Build();
var invokingContext = new AIContextProvider.InvokingContext(this._agent, session: null, new AIContext());
// Act
var result = await provider.InvokingAsync(invokingContext, CancellationToken.None);
// Assert — all skills from all sources are present
Assert.NotNull(result.Instructions);
Assert.Contains("custom-skill", result.Instructions);
Assert.Contains("inline-skill", result.Instructions);
Assert.Contains("file-skill", result.Instructions);
}
[Fact] [Fact]
public async Task InvokingCoreAsync_WithScriptsAndScriptApproval_WrapsRunScriptToolAsync() public async Task InvokingCoreAsync_WithScriptsAndScriptApproval_WrapsRunScriptToolAsync()
{ {
@@ -794,63 +722,6 @@ public sealed class AgentSkillsProviderTests : IDisposable
Assert.Contains("Body 1.", content!.ToString()!); Assert.Contains("Body 1.", content!.ToString()!);
} }
[Fact]
public async Task Constructor_InlineSkillsParams_ProvidesSkillsAsync()
{
// Arrange
var skill1 = new AgentInlineSkill("inline-a", "Inline A", "Instructions A.");
var skill2 = new AgentInlineSkill("inline-b", "Inline B", "Instructions B.");
var provider = new AgentSkillsProvider(skill1, skill2);
var invokingContext = new AIContextProvider.InvokingContext(this._agent, session: null, new AIContext());
// Act
var result = await provider.InvokingAsync(invokingContext, CancellationToken.None);
// Assert
Assert.NotNull(result.Instructions);
Assert.Contains("inline-a", result.Instructions);
Assert.Contains("inline-b", result.Instructions);
}
[Fact]
public async Task Constructor_InlineSkillsEnumerable_ProvidesSkillsAsync()
{
// Arrange
var skills = new List<AgentInlineSkill>
{
new("enum-inline-a", "Inline A", "Instructions A."),
new("enum-inline-b", "Inline B", "Instructions B."),
};
var provider = new AgentSkillsProvider(skills);
var invokingContext = new AIContextProvider.InvokingContext(this._agent, session: null, new AIContext());
// Act
var result = await provider.InvokingAsync(invokingContext, CancellationToken.None);
// Assert
Assert.NotNull(result.Instructions);
Assert.Contains("enum-inline-a", result.Instructions);
Assert.Contains("enum-inline-b", result.Instructions);
}
[Fact]
public async Task Constructor_InlineSkills_DeduplicatesAsync()
{
// Arrange — two inline skills with the same name
var skill1 = new AgentInlineSkill("dup-inline", "First", "First instructions.");
var skill2 = new AgentInlineSkill("dup-inline", "Second", "Second instructions.");
var provider = new AgentSkillsProvider(skill1, skill2);
var invokingContext = new AIContextProvider.InvokingContext(this._agent, session: null, new AIContext());
// Act
var result = await provider.InvokingAsync(invokingContext, CancellationToken.None);
var loadSkillTool = result.Tools!.First(t => t.Name == "load_skill") as AIFunction;
var content = await loadSkillTool!.InvokeAsync(new AIFunctionArguments(new Dictionary<string, object?> { ["skillName"] = "dup-inline" }));
// Assert — only one occurrence (first)
Assert.Contains("First instructions.", content!.ToString()!);
}
/// <summary> /// <summary>
/// A test skill source that counts how many times <see cref="GetSkillsAsync"/> is called. /// A test skill source that counts how many times <see cref="GetSkillsAsync"/> is called.
/// </summary> /// </summary>
@@ -872,4 +743,23 @@ public sealed class AgentSkillsProviderTests : IDisposable
return Task.FromResult(this._skills); return Task.FromResult(this._skills);
} }
} }
private sealed class TestAgentSkill : AgentSkill
{
private readonly string _content;
public TestAgentSkill(string name, string description, string content)
{
this.Frontmatter = new AgentSkillFrontmatter(name, description);
this._content = content;
}
public override AgentSkillFrontmatter Frontmatter { get; }
public override string Content => this._content;
public override IReadOnlyList<AgentSkillResource>? Resources => null;
public override IReadOnlyList<AgentSkillScript>? Scripts => null;
}
} }
@@ -16,11 +16,9 @@ public sealed class DeduplicatingAgentSkillsSourceTests
public async Task GetSkillsAsync_NoDuplicates_ReturnsAllSkillsAsync() public async Task GetSkillsAsync_NoDuplicates_ReturnsAllSkillsAsync()
{ {
// Arrange // Arrange
var inner = new AgentInMemorySkillsSource(new AgentSkill[] var inner = new TestAgentSkillsSource(
{ new TestAgentSkill("skill-a", "A", "Instructions A."),
new AgentInlineSkill("skill-a", "A", "Instructions A."), new TestAgentSkill("skill-b", "B", "Instructions B."));
new AgentInlineSkill("skill-b", "B", "Instructions B."),
});
var source = new DeduplicatingAgentSkillsSource(inner); var source = new DeduplicatingAgentSkillsSource(inner);
// Act // Act
@@ -36,11 +34,11 @@ public sealed class DeduplicatingAgentSkillsSourceTests
// Arrange // Arrange
var skills = new AgentSkill[] var skills = new AgentSkill[]
{ {
new AgentInlineSkill("dupe", "First", "Instructions 1."), new TestAgentSkill("dupe", "First", "Instructions 1."),
new AgentInlineSkill("dupe", "Second", "Instructions 2."), new TestAgentSkill("dupe", "Second", "Instructions 2."),
new AgentInlineSkill("unique", "Unique", "Instructions 3."), new TestAgentSkill("unique", "Unique", "Instructions 3."),
}; };
var inner = new AgentInMemorySkillsSource(skills); var inner = new TestAgentSkillsSource(skills);
var source = new DeduplicatingAgentSkillsSource(inner); var source = new DeduplicatingAgentSkillsSource(inner);
// Act // Act
@@ -55,7 +53,7 @@ public sealed class DeduplicatingAgentSkillsSourceTests
[Fact] [Fact]
public async Task GetSkillsAsync_CaseInsensitiveDuplication_KeepsFirstAsync() public async Task GetSkillsAsync_CaseInsensitiveDuplication_KeepsFirstAsync()
{ {
// Arrange - Use a custom source that returns skills with same name but different casing // Arrange — use a custom source that returns skills with same name but different casing
var inner = new FakeDuplicateCaseSource(); var inner = new FakeDuplicateCaseSource();
var source = new DeduplicatingAgentSkillsSource(inner); var source = new DeduplicatingAgentSkillsSource(inner);
@@ -71,7 +69,7 @@ public sealed class DeduplicatingAgentSkillsSourceTests
public async Task GetSkillsAsync_EmptySource_ReturnsEmptyAsync() public async Task GetSkillsAsync_EmptySource_ReturnsEmptyAsync()
{ {
// Arrange // Arrange
var inner = new AgentInMemorySkillsSource(System.Array.Empty<AgentSkill>()); var inner = new TestAgentSkillsSource(System.Array.Empty<AgentSkill>());
var source = new DeduplicatingAgentSkillsSource(inner); var source = new DeduplicatingAgentSkillsSource(inner);
// Act // Act
@@ -92,8 +90,8 @@ public sealed class DeduplicatingAgentSkillsSourceTests
// two skills with the same lowercase name to test case-insensitive dedup. // two skills with the same lowercase name to test case-insensitive dedup.
var skills = new List<AgentSkill> var skills = new List<AgentSkill>
{ {
new AgentInlineSkill("my-skill", "First", "Instructions 1."), new TestAgentSkill("my-skill", "First", "Instructions 1."),
new AgentInlineSkill("my-skill", "Second", "Instructions 2."), new TestAgentSkill("my-skill", "Second", "Instructions 2."),
}; };
return Task.FromResult<IList<AgentSkill>>(skills); return Task.FromResult<IList<AgentSkill>>(skills);
} }
@@ -15,11 +15,9 @@ public sealed class FilteringAgentSkillsSourceTests
public async Task GetSkillsAsync_PredicateIncludesAll_ReturnsAllSkillsAsync() public async Task GetSkillsAsync_PredicateIncludesAll_ReturnsAllSkillsAsync()
{ {
// Arrange // Arrange
var inner = new AgentInMemorySkillsSource(new AgentSkill[] var inner = new TestAgentSkillsSource(
{ new TestAgentSkill("skill-a", "A", "Instructions A."),
new AgentInlineSkill("skill-a", "A", "Instructions A."), new TestAgentSkill("skill-b", "B", "Instructions B."));
new AgentInlineSkill("skill-b", "B", "Instructions B."),
});
var source = new FilteringAgentSkillsSource(inner, _ => true); var source = new FilteringAgentSkillsSource(inner, _ => true);
// Act // Act
@@ -33,11 +31,9 @@ public sealed class FilteringAgentSkillsSourceTests
public async Task GetSkillsAsync_PredicateExcludesAll_ReturnsEmptyAsync() public async Task GetSkillsAsync_PredicateExcludesAll_ReturnsEmptyAsync()
{ {
// Arrange // Arrange
var inner = new AgentInMemorySkillsSource(new AgentSkill[] var inner = new TestAgentSkillsSource(
{ new TestAgentSkill("skill-a", "A", "Instructions A."),
new AgentInlineSkill("skill-a", "A", "Instructions A."), new TestAgentSkill("skill-b", "B", "Instructions B."));
new AgentInlineSkill("skill-b", "B", "Instructions B."),
});
var source = new FilteringAgentSkillsSource(inner, _ => false); var source = new FilteringAgentSkillsSource(inner, _ => false);
// Act // Act
@@ -51,12 +47,10 @@ public sealed class FilteringAgentSkillsSourceTests
public async Task GetSkillsAsync_PartialFilter_ReturnsMatchingSkillsOnlyAsync() public async Task GetSkillsAsync_PartialFilter_ReturnsMatchingSkillsOnlyAsync()
{ {
// Arrange // Arrange
var inner = new AgentInMemorySkillsSource(new AgentSkill[] var inner = new TestAgentSkillsSource(
{ new TestAgentSkill("keep-me", "Keep", "Instructions."),
new AgentInlineSkill("keep-me", "Keep", "Instructions."), new TestAgentSkill("drop-me", "Drop", "Instructions."),
new AgentInlineSkill("drop-me", "Drop", "Instructions."), new TestAgentSkill("keep-also", "KeepAlso", "Instructions."));
new AgentInlineSkill("keep-also", "KeepAlso", "Instructions."),
});
var source = new FilteringAgentSkillsSource( var source = new FilteringAgentSkillsSource(
inner, inner,
skill => skill.Frontmatter.Name.StartsWith("keep", StringComparison.OrdinalIgnoreCase)); skill => skill.Frontmatter.Name.StartsWith("keep", StringComparison.OrdinalIgnoreCase));
@@ -73,7 +67,7 @@ public sealed class FilteringAgentSkillsSourceTests
public async Task GetSkillsAsync_EmptySource_ReturnsEmptyAsync() public async Task GetSkillsAsync_EmptySource_ReturnsEmptyAsync()
{ {
// Arrange // Arrange
var inner = new AgentInMemorySkillsSource(Array.Empty<AgentSkill>()); var inner = new TestAgentSkillsSource(Array.Empty<AgentSkill>());
var source = new FilteringAgentSkillsSource(inner, _ => true); var source = new FilteringAgentSkillsSource(inner, _ => true);
// Act // Act
@@ -87,7 +81,7 @@ public sealed class FilteringAgentSkillsSourceTests
public void Constructor_NullPredicate_Throws() public void Constructor_NullPredicate_Throws()
{ {
// Arrange // Arrange
var inner = new AgentInMemorySkillsSource(Array.Empty<AgentSkill>()); var inner = new TestAgentSkillsSource(Array.Empty<AgentSkill>());
// Act & Assert // Act & Assert
Assert.Throws<ArgumentNullException>(() => new FilteringAgentSkillsSource(inner, null!)); Assert.Throws<ArgumentNullException>(() => new FilteringAgentSkillsSource(inner, null!));
@@ -104,13 +98,11 @@ public sealed class FilteringAgentSkillsSourceTests
public async Task GetSkillsAsync_PreservesOrderAsync() public async Task GetSkillsAsync_PreservesOrderAsync()
{ {
// Arrange // Arrange
var inner = new AgentInMemorySkillsSource(new AgentSkill[] var inner = new TestAgentSkillsSource(
{ new TestAgentSkill("alpha", "Alpha", "Instructions."),
new AgentInlineSkill("alpha", "Alpha", "Instructions."), new TestAgentSkill("beta", "Beta", "Instructions."),
new AgentInlineSkill("beta", "Beta", "Instructions."), new TestAgentSkill("gamma", "Gamma", "Instructions."),
new AgentInlineSkill("gamma", "Gamma", "Instructions."), new TestAgentSkill("delta", "Delta", "Instructions."));
new AgentInlineSkill("delta", "Delta", "Instructions."),
});
// Keep only alpha and gamma // Keep only alpha and gamma
var source = new FilteringAgentSkillsSource( var source = new FilteringAgentSkillsSource(
@@ -14,7 +14,7 @@ namespace Microsoft.Agents.AI.UnitTests;
/// <summary> /// <summary>
/// Shared test helper for <see cref="ChatClientAgent"/> integration tests that verify /// Shared test helper for <see cref="ChatClientAgent"/> integration tests that verify
/// end-to-end behavior with <see cref="ServiceStoredSimulatingChatClient"/> and /// end-to-end behavior with <see cref="ChatHistoryPersistingChatClient"/> and
/// <see cref="FunctionInvokingChatClient"/>. /// <see cref="FunctionInvokingChatClient"/>.
/// </summary> /// </summary>
internal static class ChatClientAgentTestHelper internal static class ChatClientAgentTestHelper
@@ -379,10 +379,12 @@ public partial class ChatClientAgentTests
} }
/// <summary> /// <summary>
/// Verify that RunAsync passes null ChatOptions when using regular AgentRunOptions. /// Verify that RunAsync passes ChatOptions with null ConversationId when using regular AgentRunOptions.
/// When per-service-call persistence is active (default), the sentinel conversation ID is set on ChatOptions
/// and then stripped by ChatHistoryPersistingChatClient before reaching the inner client.
/// </summary> /// </summary>
[Fact] [Fact]
public async Task RunAsyncPassesNullChatOptionsWhenUsingRegularAgentRunOptionsAsync() public async Task RunAsyncPassesChatOptionsWithNullConversationIdWhenUsingRegularAgentRunOptionsAsync()
{ {
// Arrange // Arrange
ChatOptions? capturedOptions = null; ChatOptions? capturedOptions = null;
@@ -401,8 +403,9 @@ public partial class ChatClientAgentTests
// Act // Act
await agent.RunAsync([new(ChatRole.User, "test")], options: runOptions); await agent.RunAsync([new(ChatRole.User, "test")], options: runOptions);
// Assert // Assert — the inner client receives ChatOptions with null ConversationId (sentinel was stripped)
Assert.Null(capturedOptions); Assert.NotNull(capturedOptions);
Assert.Null(capturedOptions!.ConversationId);
} }
/// <summary> /// <summary>
@@ -9,7 +9,7 @@ namespace Microsoft.Agents.AI.UnitTests;
/// <summary> /// <summary>
/// Contains unit tests that verify the end-to-end approval flow behavior of the /// Contains unit tests that verify the end-to-end approval flow behavior of the
/// <see cref="ChatClientAgent"/> class with <see cref="ServiceStoredSimulatingChatClient"/>, /// <see cref="ChatClientAgent"/> class with <see cref="ChatHistoryPersistingChatClient"/>,
/// ensuring that chat history is correctly persisted across multi-turn approval interactions. /// ensuring that chat history is correctly persisted across multi-turn approval interactions.
/// </summary> /// </summary>
public class ChatClientAgent_ApprovalsTests public class ChatClientAgent_ApprovalsTests
@@ -48,7 +48,7 @@ public class ChatClientAgent_ApprovalsTests
agentOptions: new() agentOptions: new()
{ {
ChatOptions = new() { Tools = [approvalTool] }, ChatOptions = new() { Tools = [approvalTool] },
SimulateServiceStoredChatHistory = true, PersistChatHistoryAtEndOfRun = false,
}, },
callIndex: callIndex, callIndex: callIndex,
capturedInputs: capturedInputs); capturedInputs: capturedInputs);
@@ -123,6 +123,7 @@ public class ChatClientAgent_ApprovalsTests
agentOptions: new() agentOptions: new()
{ {
ChatOptions = new() { Tools = [approvalTool] }, ChatOptions = new() { Tools = [approvalTool] },
PersistChatHistoryAtEndOfRun = true,
}, },
callIndex: callIndex, callIndex: callIndex,
capturedInputs: capturedInputs); capturedInputs: capturedInputs);
@@ -149,10 +150,8 @@ public class ChatClientAgent_ApprovalsTests
expectedHistory: expectedHistory:
[ [
// End-of-run persistence retains the approval request from Turn 1 // End-of-run persistence retains the approval request from Turn 1
// and the approval response from Turn 2
new(ChatRole.User, TextContains: "What's the weather?"), new(ChatRole.User, TextContains: "What's the weather?"),
new(ChatRole.Assistant, ContentTypes: [typeof(ToolApprovalRequestContent)]), new(ChatRole.Assistant, ContentTypes: [typeof(ToolApprovalRequestContent)]),
new(ChatRole.User, ContentTypes: [typeof(ToolApprovalResponseContent)]),
new(ChatRole.Assistant, ContentTypes: [typeof(FunctionCallContent)]), new(ChatRole.Assistant, ContentTypes: [typeof(FunctionCallContent)]),
new(ChatRole.Tool, ContentTypes: [typeof(FunctionResultContent)]), new(ChatRole.Tool, ContentTypes: [typeof(FunctionResultContent)]),
new(ChatRole.Assistant, TextContains: "sunny and 22°C"), new(ChatRole.Assistant, TextContains: "sunny and 22°C"),
@@ -197,6 +196,7 @@ public class ChatClientAgent_ApprovalsTests
agentOptions: new() agentOptions: new()
{ {
ChatOptions = new() { Tools = [approvalTool] }, ChatOptions = new() { Tools = [approvalTool] },
PersistChatHistoryAtEndOfRun = false,
}, },
callIndex: callIndex, callIndex: callIndex,
capturedInputs: capturedInputs); capturedInputs: capturedInputs);
@@ -260,7 +260,7 @@ public class ChatClientAgent_ApprovalsTests
agentOptions: new() agentOptions: new()
{ {
ChatOptions = new() { Tools = [approvalTool] }, ChatOptions = new() { Tools = [approvalTool] },
SimulateServiceStoredChatHistory = true, PersistChatHistoryAtEndOfRun = false,
}, },
callIndex: callIndex, callIndex: callIndex,
capturedInputs: capturedInputs); capturedInputs: capturedInputs);
@@ -520,7 +520,7 @@ public class ChatClientAgent_ChatHistoryManagementTests
agentOptions: new() agentOptions: new()
{ {
ChatOptions = new() { Instructions = "Be helpful" }, ChatOptions = new() { Instructions = "Be helpful" },
SimulateServiceStoredChatHistory = true, PersistChatHistoryAtEndOfRun = false,
}, },
expectedServiceCallCount: 1, expectedServiceCallCount: 1,
expectedHistory: expectedHistory:
@@ -554,7 +554,7 @@ public class ChatClientAgent_ChatHistoryManagementTests
agentOptions: new() agentOptions: new()
{ {
ChatOptions = new() { Tools = [tool] }, ChatOptions = new() { Tools = [tool] },
SimulateServiceStoredChatHistory = true, PersistChatHistoryAtEndOfRun = false,
}, },
expectedServiceCallCount: 2, expectedServiceCallCount: 2,
expectedHistory: expectedHistory:
@@ -583,6 +583,7 @@ public class ChatClientAgent_ChatHistoryManagementTests
agentOptions: new() agentOptions: new()
{ {
ChatOptions = new() { Instructions = "Be helpful" }, ChatOptions = new() { Instructions = "Be helpful" },
PersistChatHistoryAtEndOfRun = true,
}, },
expectedServiceCallCount: 1, expectedServiceCallCount: 1,
expectedHistory: expectedHistory:
@@ -614,6 +615,7 @@ public class ChatClientAgent_ChatHistoryManagementTests
agentOptions: new() agentOptions: new()
{ {
ChatOptions = new() { Tools = [tool] }, ChatOptions = new() { Tools = [tool] },
PersistChatHistoryAtEndOfRun = true,
}, },
expectedServiceCallCount: 2, expectedServiceCallCount: 2,
expectedHistory: expectedHistory:
@@ -642,6 +644,7 @@ public class ChatClientAgent_ChatHistoryManagementTests
agentOptions: new() agentOptions: new()
{ {
ChatOptions = new() { Instructions = "Be helpful" }, ChatOptions = new() { Instructions = "Be helpful" },
PersistChatHistoryAtEndOfRun = false,
}, },
expectedServiceCallCount: 1); expectedServiceCallCount: 1);
@@ -176,11 +176,12 @@ public class ChatClientAgent_ChatOptionsMergingTests
} }
/// <summary> /// <summary>
/// Verify that when both agent and request have no ChatOptions, the inner client /// Verify that ChatOptions merging returns a non-null ChatOptions instance with null ConversationId
/// receives null options. /// when both agent and request have no ChatOptions. The sentinel conversation ID is set for
/// per-service-call persistence and stripped before reaching the inner client.
/// </summary> /// </summary>
[Fact] [Fact]
public async Task ChatOptionsMergingReturnsNullChatOptionsWhenBothAgentAndRequestHaveNoneAsync() public async Task ChatOptionsMergingReturnsChatOptionsWithNullConversationIdWhenBothAgentAndRequestHaveNoneAsync()
{ {
// Arrange // Arrange
Mock<IChatClient> mockService = new(); Mock<IChatClient> mockService = new();
@@ -200,8 +201,9 @@ public class ChatClientAgent_ChatOptionsMergingTests
// Act // Act
await agent.RunAsync(messages); await agent.RunAsync(messages);
// Assert // Assert — ChatOptions is non-null because the sentinel was set, but ConversationId is null (stripped)
Assert.Null(capturedChatOptions); Assert.NotNull(capturedChatOptions);
Assert.Null(capturedChatOptions!.ConversationId);
} }
/// <summary> /// <summary>
@@ -13,15 +13,15 @@ using Moq.Protected;
namespace Microsoft.Agents.AI.UnitTests; namespace Microsoft.Agents.AI.UnitTests;
/// <summary> /// <summary>
/// Contains unit tests for the <see cref="ServiceStoredSimulatingChatClient"/> decorator, /// Contains unit tests for the <see cref="ChatHistoryPersistingChatClient"/> decorator,
/// verifying that it persists messages via the <see cref="ChatHistoryProvider"/> after each /// verifying that it persists messages via the <see cref="ChatHistoryProvider"/> after each
/// individual service call by default, or marks messages for end-of-run persistence when the /// individual service call by default, or marks messages for end-of-run persistence when the
/// <see cref="ChatClientAgentOptions.SimulateServiceStoredChatHistory"/> option is enabled. /// <see cref="ChatClientAgentOptions.PersistChatHistoryAtEndOfRun"/> option is enabled.
/// </summary> /// </summary>
public class ServiceStoredSimulatingChatClientTests public class ChatHistoryPersistingChatClientTests
{ {
/// <summary> /// <summary>
/// Verifies that by default (SimulateServiceStoredChatHistory is false), /// Verifies that by default (PersistChatHistoryAtEndOfRun is false),
/// the ChatHistoryProvider receives messages after a successful non-streaming call. /// the ChatHistoryProvider receives messages after a successful non-streaming call.
/// </summary> /// </summary>
[Fact] [Fact]
@@ -50,7 +50,7 @@ public class ServiceStoredSimulatingChatClientTests
ChatClientAgent agent = new(mockService.Object, options: new() ChatClientAgent agent = new(mockService.Object, options: new()
{ {
ChatHistoryProvider = mockChatHistoryProvider.Object, ChatHistoryProvider = mockChatHistoryProvider.Object,
SimulateServiceStoredChatHistory = true, PersistChatHistoryAtEndOfRun = false,
}); });
// Act // Act
@@ -97,7 +97,7 @@ public class ServiceStoredSimulatingChatClientTests
ChatClientAgent agent = new(mockService.Object, options: new() ChatClientAgent agent = new(mockService.Object, options: new()
{ {
ChatHistoryProvider = mockChatHistoryProvider.Object, ChatHistoryProvider = mockChatHistoryProvider.Object,
SimulateServiceStoredChatHistory = true, PersistChatHistoryAtEndOfRun = true,
}); });
// Act // Act
@@ -145,7 +145,7 @@ public class ServiceStoredSimulatingChatClientTests
ChatClientAgent agent = new(mockService.Object, options: new() ChatClientAgent agent = new(mockService.Object, options: new()
{ {
ChatHistoryProvider = mockChatHistoryProvider.Object, ChatHistoryProvider = mockChatHistoryProvider.Object,
SimulateServiceStoredChatHistory = true, PersistChatHistoryAtEndOfRun = false,
}); });
// Act // Act
@@ -163,10 +163,11 @@ public class ServiceStoredSimulatingChatClientTests
} }
/// <summary> /// <summary>
/// Verifies that the decorator is NOT injected by default (SimulateServiceStoredChatHistory is false). /// Verifies that the decorator is injected in persist mode by default
/// and can be discovered via GetService.
/// </summary> /// </summary>
[Fact] [Fact]
public void ChatClient_DoesNotContainDecorator_ByDefault() public void ChatClient_ContainsDecorator_InPersistMode_ByDefault()
{ {
// Arrange // Arrange
Mock<IChatClient> mockService = new(); Mock<IChatClient> mockService = new();
@@ -175,15 +176,16 @@ public class ServiceStoredSimulatingChatClientTests
ChatClientAgent agent = new(mockService.Object, options: new()); ChatClientAgent agent = new(mockService.Object, options: new());
// Assert // Assert
var decorator = agent.ChatClient.GetService<ServiceStoredSimulatingChatClient>(); var decorator = agent.ChatClient.GetService<ChatHistoryPersistingChatClient>();
Assert.Null(decorator); Assert.NotNull(decorator);
Assert.False(decorator.MarkOnly);
} }
/// <summary> /// <summary>
/// Verifies that the decorator is injected when SimulateServiceStoredChatHistory is true. /// Verifies that the decorator is injected in mark-only mode when PersistChatHistoryAtEndOfRun is true.
/// </summary> /// </summary>
[Fact] [Fact]
public void ChatClient_ContainsDecorator_WhenSimulateServiceStoredChatHistory() public void ChatClient_ContainsDecorator_InMarkOnlyMode_WhenPersistAtEndOfRun()
{ {
// Arrange // Arrange
Mock<IChatClient> mockService = new(); Mock<IChatClient> mockService = new();
@@ -191,12 +193,13 @@ public class ServiceStoredSimulatingChatClientTests
// Act // Act
ChatClientAgent agent = new(mockService.Object, options: new() ChatClientAgent agent = new(mockService.Object, options: new()
{ {
SimulateServiceStoredChatHistory = true, PersistChatHistoryAtEndOfRun = true,
}); });
// Assert // Assert
var decorator = agent.ChatClient.GetService<ServiceStoredSimulatingChatClient>(); var decorator = agent.ChatClient.GetService<ChatHistoryPersistingChatClient>();
Assert.NotNull(decorator); Assert.NotNull(decorator);
Assert.True(decorator.MarkOnly);
} }
/// <summary> /// <summary>
@@ -215,27 +218,27 @@ public class ServiceStoredSimulatingChatClientTests
}); });
// Assert // Assert
var decorator = agent.ChatClient.GetService<ServiceStoredSimulatingChatClient>(); var decorator = agent.ChatClient.GetService<ChatHistoryPersistingChatClient>();
Assert.Null(decorator); Assert.Null(decorator);
} }
/// <summary> /// <summary>
/// Verifies that the SimulateServiceStoredChatHistory option is included in Clone(). /// Verifies that the PersistChatHistoryAtEndOfRun option is included in Clone().
/// </summary> /// </summary>
[Fact] [Fact]
public void ChatClientAgentOptions_Clone_IncludesSimulateServiceStoredChatHistory() public void ChatClientAgentOptions_Clone_IncludesPersistChatHistoryAtEndOfRun()
{ {
// Arrange // Arrange
var options = new ChatClientAgentOptions var options = new ChatClientAgentOptions
{ {
SimulateServiceStoredChatHistory = true, PersistChatHistoryAtEndOfRun = true,
}; };
// Act // Act
var cloned = options.Clone(); var cloned = options.Clone();
// Assert // Assert
Assert.True(cloned.SimulateServiceStoredChatHistory); Assert.True(cloned.PersistChatHistoryAtEndOfRun);
} }
/// <summary> /// <summary>
@@ -289,7 +292,7 @@ public class ServiceStoredSimulatingChatClientTests
{ {
ChatOptions = new() { Tools = [tool] }, ChatOptions = new() { Tools = [tool] },
ChatHistoryProvider = mockChatHistoryProvider.Object, ChatHistoryProvider = mockChatHistoryProvider.Object,
SimulateServiceStoredChatHistory = true, PersistChatHistoryAtEndOfRun = false,
}, services: new ServiceCollection().BuildServiceProvider()); }, services: new ServiceCollection().BuildServiceProvider());
// Act // Act
@@ -358,7 +361,7 @@ public class ServiceStoredSimulatingChatClientTests
ChatClientAgent agent = new(mockService.Object, options: new() ChatClientAgent agent = new(mockService.Object, options: new()
{ {
ChatHistoryProvider = mockChatHistoryProvider.Object, ChatHistoryProvider = mockChatHistoryProvider.Object,
SimulateServiceStoredChatHistory = true, PersistChatHistoryAtEndOfRun = false,
}); });
// Act // Act
@@ -407,7 +410,7 @@ public class ServiceStoredSimulatingChatClientTests
ChatClientAgent agent = new(mockService.Object, options: new() ChatClientAgent agent = new(mockService.Object, options: new()
{ {
AIContextProviders = [mockContextProvider.Object], AIContextProviders = [mockContextProvider.Object],
SimulateServiceStoredChatHistory = true, PersistChatHistoryAtEndOfRun = false,
}); });
// Act // Act
@@ -454,7 +457,7 @@ public class ServiceStoredSimulatingChatClientTests
ChatClientAgent agent = new(mockService.Object, options: new() ChatClientAgent agent = new(mockService.Object, options: new()
{ {
AIContextProviders = [mockContextProvider.Object], AIContextProviders = [mockContextProvider.Object],
SimulateServiceStoredChatHistory = true, PersistChatHistoryAtEndOfRun = false,
}); });
// Act // Act
@@ -513,7 +516,7 @@ public class ServiceStoredSimulatingChatClientTests
{ {
ChatHistoryProvider = mockChatHistoryProvider.Object, ChatHistoryProvider = mockChatHistoryProvider.Object,
AIContextProviders = [mockContextProvider.Object], AIContextProviders = [mockContextProvider.Object],
SimulateServiceStoredChatHistory = true, PersistChatHistoryAtEndOfRun = false,
}); });
// Act // Act
@@ -587,7 +590,7 @@ public class ServiceStoredSimulatingChatClientTests
{ {
ChatOptions = new() { Tools = [tool] }, ChatOptions = new() { Tools = [tool] },
ChatHistoryProvider = mockChatHistoryProvider.Object, ChatHistoryProvider = mockChatHistoryProvider.Object,
SimulateServiceStoredChatHistory = true, PersistChatHistoryAtEndOfRun = false,
}, services: new ServiceCollection().BuildServiceProvider()); }, services: new ServiceCollection().BuildServiceProvider());
// Act // Act
@@ -652,7 +655,7 @@ public class ServiceStoredSimulatingChatClientTests
{ {
ChatOptions = new() { Tools = [tool] }, ChatOptions = new() { Tools = [tool] },
ChatHistoryProvider = mockChatHistoryProvider.Object, ChatHistoryProvider = mockChatHistoryProvider.Object,
SimulateServiceStoredChatHistory = true, PersistChatHistoryAtEndOfRun = false,
}, services: new ServiceCollection().BuildServiceProvider()); }, services: new ServiceCollection().BuildServiceProvider());
// Act // Act
@@ -677,12 +680,52 @@ public class ServiceStoredSimulatingChatClientTests
/// Verifies that after a successful run with per-service-call persistence, the notified /// Verifies that after a successful run with per-service-call persistence, the notified
/// messages are stamped with the persisted marker so they are not re-notified. /// messages are stamped with the persisted marker so they are not re-notified.
/// </summary> /// </summary>
[Fact]
public async Task RunAsync_MarksNotifiedMessages_WithPersistedMarkerAsync()
{
// Arrange
Mock<IChatClient> mockService = new();
mockService.Setup(
s => s.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
Mock<ChatHistoryProvider> mockChatHistoryProvider = new(null, null, null);
mockChatHistoryProvider.SetupGet(p => p.StateKeys).Returns(["TestChatHistoryProvider"]);
mockChatHistoryProvider
.Protected()
.Setup<ValueTask<IEnumerable<ChatMessage>>>("InvokingCoreAsync", ItExpr.IsAny<ChatHistoryProvider.InvokingContext>(), ItExpr.IsAny<CancellationToken>())
.Returns((ChatHistoryProvider.InvokingContext ctx, CancellationToken _) =>
new ValueTask<IEnumerable<ChatMessage>>(ctx.RequestMessages.ToList()));
mockChatHistoryProvider
.Protected()
.Setup<ValueTask>("InvokedCoreAsync", ItExpr.IsAny<ChatHistoryProvider.InvokedContext>(), ItExpr.IsAny<CancellationToken>())
.Returns(() => new ValueTask());
ChatClientAgent agent = new(mockService.Object, options: new()
{
ChatHistoryProvider = mockChatHistoryProvider.Object,
PersistChatHistoryAtEndOfRun = false,
});
// Act
var inputMessage = new ChatMessage(ChatRole.User, "test");
var session = await agent.CreateSessionAsync() as ChatClientAgentSession;
await agent.RunAsync([inputMessage], session);
// Assert — input message should be marked as persisted
Assert.True(
inputMessage.AdditionalProperties?.ContainsKey(ChatHistoryPersistingChatClient.PersistedMarkerKey) == true,
"Input message should be marked as persisted after a successful run.");
}
/// <summary> /// <summary>
/// Verifies that when the inner client returns a real conversation ID, /// Verifies that when per-service-call persistence is enabled and the inner client returns a
/// the session's ConversationId is updated after the run. /// conversation ID, the session's ConversationId is updated after the service call.
/// </summary> /// </summary>
[Fact] [Fact]
public async Task RunAsync_UpdatesSessionConversationId_WhenServiceReturnsOneAsync() public async Task RunAsync_UpdatesSessionConversationId_WhenPerServiceCallPersistenceEnabledAsync()
{ {
// Arrange // Arrange
const string ExpectedConversationId = "conv-123"; const string ExpectedConversationId = "conv-123";
@@ -698,7 +741,10 @@ public class ServiceStoredSimulatingChatClientTests
ConversationId = ExpectedConversationId, ConversationId = ExpectedConversationId,
}); });
ChatClientAgent agent = new(mockService.Object); ChatClientAgent agent = new(mockService.Object, options: new()
{
PersistChatHistoryAtEndOfRun = false,
});
// Act // Act
var session = await agent.CreateSessionAsync() as ChatClientAgentSession; var session = await agent.CreateSessionAsync() as ChatClientAgentSession;
@@ -720,8 +766,8 @@ public class ServiceStoredSimulatingChatClientTests
/// <summary> /// <summary>
/// Verifies that when per-service-call persistence is active and no real conversation ID exists, /// Verifies that when per-service-call persistence is active and no real conversation ID exists,
/// <see cref="ChatClientAgent"/> sets the <see cref="ServiceStoredSimulatingChatClient.LocalHistoryConversationId"/> /// <see cref="ChatClientAgent"/> sets the <see cref="ChatHistoryPersistingChatClient.LocalHistoryConversationId"/>
/// sentinel on the chat options and <see cref="ServiceStoredSimulatingChatClient"/> strips it before /// sentinel on the chat options and <see cref="ChatHistoryPersistingChatClient"/> strips it before
/// forwarding to the inner client. /// forwarding to the inner client.
/// </summary> /// </summary>
[Fact] [Fact]
@@ -741,7 +787,7 @@ public class ServiceStoredSimulatingChatClientTests
ChatClientAgent agent = new(mockService.Object, options: new() ChatClientAgent agent = new(mockService.Object, options: new()
{ {
ChatOptions = new() { Instructions = "test" }, ChatOptions = new() { Instructions = "test" },
SimulateServiceStoredChatHistory = true, PersistChatHistoryAtEndOfRun = false,
}); });
// Act // Act
@@ -773,7 +819,7 @@ public class ServiceStoredSimulatingChatClientTests
ChatClientAgent agent = new(mockService.Object, options: new() ChatClientAgent agent = new(mockService.Object, options: new()
{ {
ChatOptions = new() { Instructions = "test" }, ChatOptions = new() { Instructions = "test" },
SimulateServiceStoredChatHistory = true, PersistChatHistoryAtEndOfRun = true,
}); });
// Act // Act
@@ -808,7 +854,7 @@ public class ServiceStoredSimulatingChatClientTests
ChatClientAgent agent = new(mockService.Object, options: new() ChatClientAgent agent = new(mockService.Object, options: new()
{ {
SimulateServiceStoredChatHistory = true, PersistChatHistoryAtEndOfRun = false,
}); });
// Create a session with a real conversation ID. // Create a session with a real conversation ID.
@@ -842,7 +888,7 @@ public class ServiceStoredSimulatingChatClientTests
ChatClientAgent agent = new(mockService.Object, options: new() ChatClientAgent agent = new(mockService.Object, options: new()
{ {
ChatOptions = new() { Instructions = "test" }, ChatOptions = new() { Instructions = "test" },
SimulateServiceStoredChatHistory = true, PersistChatHistoryAtEndOfRun = false,
}); });
// Act // Act
@@ -857,12 +903,11 @@ public class ServiceStoredSimulatingChatClientTests
} }
/// <summary> /// <summary>
/// Verifies that the session's conversation ID IS set to the sentinel after the run /// Verifies that the session's conversation ID is NOT set to the sentinel after the run.
/// when simulating service-stored chat history. This allows subsequent runs to /// The sentinel should only exist transiently on the ChatOptions for the pipeline.
/// skip provider resolution in the agent (the decorator handles it).
/// </summary> /// </summary>
[Fact] [Fact]
public async Task RunAsync_SetsSentinelOnSession_WhenSimulateServiceStoredChatHistoryActiveAsync() public async Task RunAsync_SentinelDoesNotLeakToSession_WhenPerServiceCallPersistenceActiveAsync()
{ {
// Arrange // Arrange
Mock<IChatClient> mockService = new(); Mock<IChatClient> mockService = new();
@@ -875,440 +920,14 @@ public class ServiceStoredSimulatingChatClientTests
ChatClientAgent agent = new(mockService.Object, options: new() ChatClientAgent agent = new(mockService.Object, options: new()
{ {
SimulateServiceStoredChatHistory = true, PersistChatHistoryAtEndOfRun = false,
}); });
// Act // Act
var session = await agent.CreateSessionAsync() as ChatClientAgentSession; var session = await agent.CreateSessionAsync() as ChatClientAgentSession;
await agent.RunAsync([new(ChatRole.User, "test")], session); await agent.RunAsync([new(ChatRole.User, "test")], session);
// Assert — session should have the sentinel conversation ID // Assert — session should NOT have the sentinel conversation ID
Assert.Equal(ServiceStoredSimulatingChatClient.LocalHistoryConversationId, session!.ConversationId); Assert.Null(session!.ConversationId);
}
/// <summary>
/// Verifies that when simulating service-stored chat history and the service returns
/// a real <see cref="ChatResponse.ConversationId"/>, the conflict detection in
/// <see cref="ChatClientAgent.UpdateSessionConversationId"/> throws because both a
/// <see cref="ChatHistoryProvider"/> and a service-managed ConversationId are present.
/// </summary>
[Fact]
public async Task RunAsync_Throws_WhenServiceReturnsRealConversationIdWithChatHistoryProviderAsync()
{
// Arrange
const string RealConversationId = "service-conv-456";
Mock<ChatHistoryProvider> mockChatHistoryProvider = new(null, null, null);
mockChatHistoryProvider.SetupGet(p => p.StateKeys).Returns(["TestChatHistoryProvider"]);
mockChatHistoryProvider
.Protected()
.Setup<ValueTask<IEnumerable<ChatMessage>>>("InvokingCoreAsync", ItExpr.IsAny<ChatHistoryProvider.InvokingContext>(), ItExpr.IsAny<CancellationToken>())
.Returns((ChatHistoryProvider.InvokingContext ctx, CancellationToken _) =>
new ValueTask<IEnumerable<ChatMessage>>(ctx.RequestMessages.ToList()));
mockChatHistoryProvider
.Protected()
.Setup<ValueTask>("InvokedCoreAsync", ItExpr.IsAny<ChatHistoryProvider.InvokedContext>(), ItExpr.IsAny<CancellationToken>())
.Returns(new ValueTask());
Mock<IChatClient> mockService = new();
mockService.Setup(
s => s.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")])
{
ConversationId = RealConversationId,
});
ChatClientAgent agent = new(mockService.Object, options: new()
{
ChatHistoryProvider = mockChatHistoryProvider.Object,
SimulateServiceStoredChatHistory = true,
});
// Act & Assert — conflict detection should throw
var session = await agent.CreateSessionAsync() as ChatClientAgentSession;
await Assert.ThrowsAsync<InvalidOperationException>(() => agent.RunAsync([new(ChatRole.User, "test")], session));
}
/// <summary>
/// Verifies that when simulating service-stored chat history and the request carries a real
/// <see cref="ChatOptions.ConversationId"/>, the decorator skips history loading but still
/// notifies <see cref="AIContextProvider"/>s on success and updates the session ConversationId.
/// </summary>
[Fact]
public async Task RunAsync_NotifiesProvidersAndUpdatesSession_WhenRequestHasRealConversationIdAsync()
{
// Arrange
const string RealConversationId = "real-conv-request";
const string ServiceConversationId = "real-conv-response";
Mock<AIContextProvider> mockContextProvider = new(null, null, null);
mockContextProvider.SetupGet(p => p.StateKeys).Returns(["TestContextProvider"]);
mockContextProvider
.Protected()
.Setup<ValueTask<AIContext>>("InvokingCoreAsync", ItExpr.IsAny<AIContextProvider.InvokingContext>(), ItExpr.IsAny<CancellationToken>())
.Returns((AIContextProvider.InvokingContext ctx, CancellationToken _) =>
new ValueTask<AIContext>(new AIContext { Messages = ctx.AIContext.Messages }));
mockContextProvider
.Protected()
.Setup<ValueTask>("InvokedCoreAsync", ItExpr.IsAny<AIContextProvider.InvokedContext>(), ItExpr.IsAny<CancellationToken>())
.Returns(new ValueTask());
Mock<IChatClient> mockService = new();
mockService.Setup(
s => s.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")])
{
ConversationId = ServiceConversationId,
});
ChatClientAgent agent = new(mockService.Object, options: new()
{
SimulateServiceStoredChatHistory = true,
AIContextProviders = [mockContextProvider.Object],
});
// Create a session with a real conversation ID so it's on chatOptions.
var session = await agent.CreateSessionAsync(RealConversationId);
// Act
await agent.RunAsync([new(ChatRole.User, "test")], session);
// Assert — AIContextProvider.InvokedAsync should have been called
mockContextProvider
.Protected()
.Verify<ValueTask>("InvokedCoreAsync", Times.Once(),
ItExpr.Is<AIContextProvider.InvokedContext>(x =>
x.RequestMessages.Any(m => m.Text == "test") &&
x.ResponseMessages!.Any(m => m.Text == "response")),
ItExpr.IsAny<CancellationToken>());
// Assert — session should have the service-returned ConversationId
Assert.Equal(ServiceConversationId, (session as ChatClientAgentSession)!.ConversationId);
}
/// <summary>
/// Verifies that when simulating service-stored chat history and the request carries a real
/// <see cref="ChatOptions.ConversationId"/>, the decorator notifies providers of failure
/// when the inner client throws.
/// </summary>
[Fact]
public async Task RunAsync_NotifiesProvidersOfFailure_WhenRequestHasRealConversationIdAsync()
{
// Arrange
const string RealConversationId = "real-conv-failure";
Mock<AIContextProvider> mockContextProvider = new(null, null, null);
mockContextProvider.SetupGet(p => p.StateKeys).Returns(["TestContextProvider"]);
mockContextProvider
.Protected()
.Setup<ValueTask<AIContext>>("InvokingCoreAsync", ItExpr.IsAny<AIContextProvider.InvokingContext>(), ItExpr.IsAny<CancellationToken>())
.Returns((AIContextProvider.InvokingContext ctx, CancellationToken _) =>
new ValueTask<AIContext>(new AIContext { Messages = ctx.AIContext.Messages }));
mockContextProvider
.Protected()
.Setup<ValueTask>("InvokedCoreAsync", ItExpr.IsAny<AIContextProvider.InvokedContext>(), ItExpr.IsAny<CancellationToken>())
.Returns(new ValueTask());
Mock<IChatClient> mockService = new();
mockService.Setup(
s => s.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.ThrowsAsync(new InvalidOperationException("Service error"));
ChatClientAgent agent = new(mockService.Object, options: new()
{
SimulateServiceStoredChatHistory = true,
AIContextProviders = [mockContextProvider.Object],
});
var session = await agent.CreateSessionAsync(RealConversationId);
// Act & Assert — should throw
await Assert.ThrowsAsync<InvalidOperationException>(() => agent.RunAsync([new(ChatRole.User, "test")], session));
// Assert — AIContextProvider.InvokedAsync should have been called with the failure
mockContextProvider
.Protected()
.Verify<ValueTask>("InvokedCoreAsync", Times.Once(),
ItExpr.Is<AIContextProvider.InvokedContext>(x => x.InvokeException != null),
ItExpr.IsAny<CancellationToken>());
}
/// <summary>
/// Verifies that in the streaming path, when the request carries a real
/// <see cref="ChatOptions.ConversationId"/>, the decorator skips history loading but still
/// notifies providers and updates the session ConversationId.
/// </summary>
[Fact]
public async Task RunStreamingAsync_NotifiesProvidersAndUpdatesSession_WhenRequestHasRealConversationIdAsync()
{
// Arrange
const string RealConversationId = "real-conv-streaming";
const string ServiceConversationId = "service-conv-streaming";
Mock<AIContextProvider> mockContextProvider = new(null, null, null);
mockContextProvider.SetupGet(p => p.StateKeys).Returns(["TestContextProvider"]);
mockContextProvider
.Protected()
.Setup<ValueTask<AIContext>>("InvokingCoreAsync", ItExpr.IsAny<AIContextProvider.InvokingContext>(), ItExpr.IsAny<CancellationToken>())
.Returns((AIContextProvider.InvokingContext ctx, CancellationToken _) =>
new ValueTask<AIContext>(new AIContext { Messages = ctx.AIContext.Messages }));
mockContextProvider
.Protected()
.Setup<ValueTask>("InvokedCoreAsync", ItExpr.IsAny<AIContextProvider.InvokedContext>(), ItExpr.IsAny<CancellationToken>())
.Returns(new ValueTask());
Mock<IChatClient> mockService = new();
mockService.Setup(
s => s.GetStreamingResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.Returns(CreateAsyncEnumerableAsync(
new ChatResponseUpdate(ChatRole.Assistant, "streamed") { ConversationId = ServiceConversationId }));
ChatClientAgent agent = new(mockService.Object, options: new()
{
SimulateServiceStoredChatHistory = true,
AIContextProviders = [mockContextProvider.Object],
});
var session = await agent.CreateSessionAsync(RealConversationId);
// Act
await foreach (var _ in agent.RunStreamingAsync([new(ChatRole.User, "test")], session))
{
// Consume all updates.
}
// Assert — AIContextProvider.InvokedAsync should have been called
mockContextProvider
.Protected()
.Verify<ValueTask>("InvokedCoreAsync", Times.Once(),
ItExpr.IsAny<AIContextProvider.InvokedContext>(),
ItExpr.IsAny<CancellationToken>());
// Assert — session should have the service-returned ConversationId
Assert.Equal(ServiceConversationId, (session as ChatClientAgentSession)!.ConversationId);
}
/// <summary>
/// Verifies that when simulating and the service unexpectedly returns a real
/// <see cref="ChatResponse.ConversationId"/> (no ConversationId on the request), the decorator
/// notifies providers and updates the session ConversationId without setting the sentinel.
/// </summary>
[Fact]
public async Task RunAsync_NotifiesProvidersAndUpdatesSession_WhenServiceReturnsUnexpectedConversationIdAsync()
{
// Arrange
const string ServiceConversationId = "unexpected-conv-id";
Mock<AIContextProvider> mockContextProvider = new(null, null, null);
mockContextProvider.SetupGet(p => p.StateKeys).Returns(["TestContextProvider"]);
mockContextProvider
.Protected()
.Setup<ValueTask<AIContext>>("InvokingCoreAsync", ItExpr.IsAny<AIContextProvider.InvokingContext>(), ItExpr.IsAny<CancellationToken>())
.Returns((AIContextProvider.InvokingContext ctx, CancellationToken _) =>
new ValueTask<AIContext>(new AIContext { Messages = ctx.AIContext.Messages }));
mockContextProvider
.Protected()
.Setup<ValueTask>("InvokedCoreAsync", ItExpr.IsAny<AIContextProvider.InvokedContext>(), ItExpr.IsAny<CancellationToken>())
.Returns(new ValueTask());
Mock<IChatClient> mockService = new();
mockService.Setup(
s => s.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")])
{
ConversationId = ServiceConversationId,
});
// No ChatHistoryProvider — so conflict detection won't throw.
ChatClientAgent agent = new(mockService.Object, options: new()
{
SimulateServiceStoredChatHistory = true,
AIContextProviders = [mockContextProvider.Object],
});
// Act
var session = await agent.CreateSessionAsync() as ChatClientAgentSession;
await agent.RunAsync([new(ChatRole.User, "test")], session);
// Assert — AIContextProvider.InvokedAsync should have been called
mockContextProvider
.Protected()
.Verify<ValueTask>("InvokedCoreAsync", Times.Once(),
ItExpr.Is<AIContextProvider.InvokedContext>(x =>
x.ResponseMessages!.Any(m => m.Text == "response")),
ItExpr.IsAny<CancellationToken>());
// Assert — session should have the service ConversationId, not the sentinel
Assert.Equal(ServiceConversationId, session!.ConversationId);
}
/// <summary>
/// Verifies that in the streaming path, when the service returns a real ConversationId mid-stream
/// (no ConversationId on the request), the decorator notifies providers and updates the session.
/// </summary>
[Fact]
public async Task RunStreamingAsync_NotifiesProvidersAndUpdatesSession_WhenServiceReturnsUnexpectedConversationIdAsync()
{
// Arrange
const string ServiceConversationId = "unexpected-stream-conv";
Mock<AIContextProvider> mockContextProvider = new(null, null, null);
mockContextProvider.SetupGet(p => p.StateKeys).Returns(["TestContextProvider"]);
mockContextProvider
.Protected()
.Setup<ValueTask<AIContext>>("InvokingCoreAsync", ItExpr.IsAny<AIContextProvider.InvokingContext>(), ItExpr.IsAny<CancellationToken>())
.Returns((AIContextProvider.InvokingContext ctx, CancellationToken _) =>
new ValueTask<AIContext>(new AIContext { Messages = ctx.AIContext.Messages }));
mockContextProvider
.Protected()
.Setup<ValueTask>("InvokedCoreAsync", ItExpr.IsAny<AIContextProvider.InvokedContext>(), ItExpr.IsAny<CancellationToken>())
.Returns(new ValueTask());
Mock<IChatClient> mockService = new();
mockService.Setup(
s => s.GetStreamingResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.Returns(CreateAsyncEnumerableAsync(
new ChatResponseUpdate(ChatRole.Assistant, "part1"),
new ChatResponseUpdate(null, "part2") { ConversationId = ServiceConversationId }));
// No ChatHistoryProvider — so conflict detection won't throw.
ChatClientAgent agent = new(mockService.Object, options: new()
{
SimulateServiceStoredChatHistory = true,
AIContextProviders = [mockContextProvider.Object],
});
// Act
var session = await agent.CreateSessionAsync() as ChatClientAgentSession;
await foreach (var _ in agent.RunStreamingAsync([new(ChatRole.User, "test")], session))
{
// Consume all updates.
}
// Assert — AIContextProvider.InvokedAsync should have been called
mockContextProvider
.Protected()
.Verify<ValueTask>("InvokedCoreAsync", Times.Once(),
ItExpr.IsAny<AIContextProvider.InvokedContext>(),
ItExpr.IsAny<CancellationToken>());
// Assert — session should have the service ConversationId, not the sentinel
Assert.Equal(ServiceConversationId, session!.ConversationId);
}
/// <summary>
/// Verifies that when <see cref="ChatOptions.AllowBackgroundResponses"/> is true,
/// the decorator skips history loading and sentinel setting, letting the agent's
/// forced end-of-run path handle persistence.
/// </summary>
[Fact]
public async Task RunAsync_SkipsSimulation_WhenAllowBackgroundResponsesAsync()
{
// Arrange
IEnumerable<ChatMessage>? capturedMessages = null;
Mock<IChatClient> mockService = new();
mockService.Setup(
s => s.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.Callback<IEnumerable<ChatMessage>, ChatOptions?, CancellationToken>((msgs, _, _) => capturedMessages = msgs)
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
Mock<ChatHistoryProvider> mockChatHistoryProvider = new(null, null, null);
mockChatHistoryProvider.SetupGet(p => p.StateKeys).Returns(["TestChatHistoryProvider"]);
mockChatHistoryProvider
.Protected()
.Setup<ValueTask<IEnumerable<ChatMessage>>>("InvokingCoreAsync", ItExpr.IsAny<ChatHistoryProvider.InvokingContext>(), ItExpr.IsAny<CancellationToken>())
.Returns((ChatHistoryProvider.InvokingContext ctx, CancellationToken _) =>
{
// Add a history message to verify it's NOT prepended in this scenario.
var result = ctx.RequestMessages.ToList();
result.Insert(0, new ChatMessage(ChatRole.Assistant, "history"));
return new ValueTask<IEnumerable<ChatMessage>>(result);
});
mockChatHistoryProvider
.Protected()
.Setup<ValueTask>("InvokedCoreAsync", ItExpr.IsAny<ChatHistoryProvider.InvokedContext>(), ItExpr.IsAny<CancellationToken>())
.Returns(new ValueTask());
ChatClientAgent agent = new(mockService.Object, options: new()
{
ChatHistoryProvider = mockChatHistoryProvider.Object,
SimulateServiceStoredChatHistory = true,
});
// Act
var session = await agent.CreateSessionAsync() as ChatClientAgentSession;
await agent.RunAsync(
[new(ChatRole.User, "test")],
session,
new AgentRunOptions { AllowBackgroundResponses = true });
// Assert — the inner client should NOT have received history messages
Assert.NotNull(capturedMessages);
var messageList = capturedMessages!.ToList();
Assert.Single(messageList);
Assert.Equal("test", messageList[0].Text);
// Assert — session should NOT have the sentinel (agent handles ConversationId at end-of-run)
Assert.NotEqual(ServiceStoredSimulatingChatClient.LocalHistoryConversationId, session!.ConversationId);
}
/// <summary>
/// Verifies that in the streaming path, when <see cref="ChatOptions.AllowBackgroundResponses"/> is true,
/// the decorator skips history loading and sentinel setting.
/// </summary>
[Fact]
public async Task RunStreamingAsync_SkipsSimulation_WhenAllowBackgroundResponsesAsync()
{
// Arrange
Mock<IChatClient> mockService = new();
mockService.Setup(
s => s.GetStreamingResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.Returns(CreateAsyncEnumerableAsync(new ChatResponseUpdate(ChatRole.Assistant, "response")));
ChatClientAgent agent = new(mockService.Object, options: new()
{
SimulateServiceStoredChatHistory = true,
});
// Act
var session = await agent.CreateSessionAsync() as ChatClientAgentSession;
List<AgentResponseUpdate> updates = [];
await foreach (var update in agent.RunStreamingAsync(
[new(ChatRole.User, "test")],
session,
new AgentRunOptions { AllowBackgroundResponses = true }))
{
updates.Add(update);
}
// Assert — updates should NOT carry the sentinel ConversationId
Assert.NotEmpty(updates);
// Assert — session should NOT have the sentinel
Assert.NotEqual(ServiceStoredSimulatingChatClient.LocalHistoryConversationId, session!.ConversationId);
} }
} }
+1 -31
View File
@@ -7,35 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## [1.0.0rc6] - 2026-03-30
### Added
- **agent-framework-openai**: New package extracted from core for OpenAI and Azure OpenAI provider support ([#4818](https://github.com/microsoft/agent-framework/pull/4818))
- **agent-framework-foundry**: New package for Azure AI Foundry integration ([#4818](https://github.com/microsoft/agent-framework/pull/4818))
- **agent-framework-core**: Support `structuredContent` in MCP tool results and fix sampling options type ([#4763](https://github.com/microsoft/agent-framework/pull/4763))
- **agent-framework-core**: Include reasoning messages in `MESSAGES_SNAPSHOT` events ([#4844](https://github.com/microsoft/agent-framework/pull/4844))
- **agent-framework-core**: [BREAKING] Add context mode to `AgentExecutor` ([#4668](https://github.com/microsoft/agent-framework/pull/4668))
### Changed
- **agent-framework-core**: [BREAKING] Remove deprecated kwargs compatibility paths ([#4858](https://github.com/microsoft/agent-framework/pull/4858))
- **agent-framework-core**: [BREAKING] Reduce core dependencies and simplify optional integrations ([#4904](https://github.com/microsoft/agent-framework/pull/4904))
- **agent-framework-openai**: [BREAKING] Provider-leading client design & OpenAI package extraction ([#4818](https://github.com/microsoft/agent-framework/pull/4818))
- **agent-framework-openai**: [BREAKING] Fix OpenAI Azure routing and provider samples ([#4925](https://github.com/microsoft/agent-framework/pull/4925))
- **agent-framework-azure-ai**: Deprecate Azure AI v1 (Persistent Agents API) helper methods ([#4804](https://github.com/microsoft/agent-framework/pull/4804))
- **agent-framework-core**: Avoid duplicate agent response telemetry ([#4685](https://github.com/microsoft/agent-framework/pull/4685))
- **agent-framework-devui**: Bump `flatted` from 3.3.3 to 3.4.2 in frontend ([#4805](https://github.com/microsoft/agent-framework/pull/4805))
- **samples**: Move `ag_ui_workflow_handoff` demo from `demos/` to `05-end-to-end/` ([#4900](https://github.com/microsoft/agent-framework/pull/4900))
### Fixed
- **agent-framework-core**: Fix streaming path to emit `mcp_server_tool_result` on `output_item.done` instead of `output_item.added` ([#4821](https://github.com/microsoft/agent-framework/pull/4821))
- **agent-framework-a2a**: Fix `A2AAgent` to surface message content from in-progress `TaskStatusUpdateEvents` ([#4798](https://github.com/microsoft/agent-framework/pull/4798))
- **agent-framework-core**: Fix `PydanticSchemaGenerationError` when using `from __future__ import annotations` with `@tool` ([#4822](https://github.com/microsoft/agent-framework/pull/4822))
- **samples**: Fix broken samples for GitHub Copilot, declarative, and Responses API ([#4915](https://github.com/microsoft/agent-framework/pull/4915))
- **repo**: Fix: update PyRIT repository link from Azure/PyRIT to microsoft/PyRIT ([#4960](https://github.com/microsoft/agent-framework/pull/4960))
## [1.0.0rc5] - 2026-03-19 ## [1.0.0rc5] - 2026-03-19
### Added ### Added
@@ -846,8 +817,7 @@ Release candidate for **agent-framework-core** and **agent-framework-azure-ai**
For more information, see the [announcement blog post](https://devblogs.microsoft.com/foundry/introducing-microsoft-agent-framework-the-open-source-engine-for-agentic-ai-apps/). For more information, see the [announcement blog post](https://devblogs.microsoft.com/foundry/introducing-microsoft-agent-framework-the-open-source-engine-for-agentic-ai-apps/).
[Unreleased]: https://github.com/microsoft/agent-framework/compare/python-1.0.0rc6...HEAD [Unreleased]: https://github.com/microsoft/agent-framework/compare/python-1.0.0rc5...HEAD
[1.0.0rc6]: https://github.com/microsoft/agent-framework/compare/python-1.0.0rc5...python-1.0.0rc6
[1.0.0rc5]: https://github.com/microsoft/agent-framework/compare/python-1.0.0rc4...python-1.0.0rc5 [1.0.0rc5]: https://github.com/microsoft/agent-framework/compare/python-1.0.0rc4...python-1.0.0rc5
[1.0.0rc4]: https://github.com/microsoft/agent-framework/compare/python-1.0.0rc3...python-1.0.0rc4 [1.0.0rc4]: https://github.com/microsoft/agent-framework/compare/python-1.0.0rc3...python-1.0.0rc4
[1.0.0rc3]: https://github.com/microsoft/agent-framework/compare/python-1.0.0rc2...python-1.0.0rc3 [1.0.0rc3]: https://github.com/microsoft/agent-framework/compare/python-1.0.0rc2...python-1.0.0rc3
+2 -2
View File
@@ -4,7 +4,7 @@ description = "A2A integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"
version = "1.0.0b260330" version = "1.0.0b260319"
license-files = ["LICENSE"] license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework" urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,7 +23,7 @@ classifiers = [
"Typing :: Typed", "Typing :: Typed",
] ]
dependencies = [ dependencies = [
"agent-framework-core>=1.0.0rc6", "agent-framework-core>=1.0.0rc5",
"a2a-sdk>=0.3.5,<0.3.24", "a2a-sdk>=0.3.5,<0.3.24",
] ]
+2 -2
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "agent-framework-ag-ui" name = "agent-framework-ag-ui"
version = "1.0.0b260330" version = "1.0.0b260319"
description = "AG-UI protocol integration for Agent Framework" description = "AG-UI protocol integration for Agent Framework"
readme = "README.md" readme = "README.md"
license-files = ["LICENSE"] license-files = ["LICENSE"]
@@ -22,7 +22,7 @@ classifiers = [
"Typing :: Typed", "Typing :: Typed",
] ]
dependencies = [ dependencies = [
"agent-framework-core>=1.0.0rc6", "agent-framework-core>=1.0.0rc5",
"ag-ui-protocol==0.1.13", "ag-ui-protocol==0.1.13",
"fastapi>=0.115.0,<0.133.1", "fastapi>=0.115.0,<0.133.1",
"uvicorn[standard]>=0.30.0,<0.42.0" "uvicorn[standard]>=0.30.0,<0.42.0"
+2 -2
View File
@@ -4,7 +4,7 @@ description = "Anthropic integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"
version = "1.0.0b260330" version = "1.0.0b260319"
license-files = ["LICENSE"] license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework" urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,7 +23,7 @@ classifiers = [
"Typing :: Typed", "Typing :: Typed",
] ]
dependencies = [ dependencies = [
"agent-framework-core>=1.0.0rc6", "agent-framework-core>=1.0.0rc5",
"anthropic>=0.80.0,<0.80.1", "anthropic>=0.80.0,<0.80.1",
] ]
@@ -4,7 +4,7 @@ description = "Azure AI Search integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"
version = "1.0.0b260330" version = "1.0.0b260319"
license-files = ["LICENSE"] license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework" urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,7 +23,7 @@ classifiers = [
"Typing :: Typed", "Typing :: Typed",
] ]
dependencies = [ dependencies = [
"agent-framework-core>=1.0.0rc6", "agent-framework-core>=1.0.0rc5",
"azure-search-documents>=11.7.0b2,<11.7.0b3", "azure-search-documents>=11.7.0b2,<11.7.0b3",
] ]
+3 -3
View File
@@ -4,7 +4,7 @@ description = "Azure AI Foundry integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"
version = "1.0.0rc6" version = "1.0.0rc5"
license-files = ["LICENSE"] license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework" urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,8 +23,8 @@ classifiers = [
"Typing :: Typed", "Typing :: Typed",
] ]
dependencies = [ dependencies = [
"agent-framework-core>=1.0.0rc6", "agent-framework-core>=1.0.0rc5",
"agent-framework-openai>=1.0.0rc6", "agent-framework-openai>=1.0.0rc5",
"azure-ai-projects>=2.0.0,<3.0", "azure-ai-projects>=2.0.0,<3.0",
"azure-ai-agents>=1.2.0b5,<1.2.0b6", "azure-ai-agents>=1.2.0b5,<1.2.0b6",
"azure-ai-inference>=1.0.0b9,<1.0.0b10", "azure-ai-inference>=1.0.0b9,<1.0.0b10",
+2 -2
View File
@@ -4,7 +4,7 @@ description = "Azure Cosmos DB history provider integration for Microsoft Agent
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"
version = "1.0.0b260330" version = "1.0.0b260319"
license-files = ["LICENSE"] license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework" urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,7 +23,7 @@ classifiers = [
"Typing :: Typed", "Typing :: Typed",
] ]
dependencies = [ dependencies = [
"agent-framework-core>=1.0.0rc6", "agent-framework-core>=1.0.0rc5",
"azure-cosmos>=4.3.0,<5", "azure-cosmos>=4.3.0,<5",
] ]
@@ -4,7 +4,7 @@ description = "Azure Functions integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"
version = "1.0.0b260330" version = "1.0.0b260319"
license-files = ["LICENSE"] license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework" urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -22,7 +22,7 @@ classifiers = [
"Typing :: Typed", "Typing :: Typed",
] ]
dependencies = [ dependencies = [
"agent-framework-core>=1.0.0rc6", "agent-framework-core>=1.0.0rc5",
"agent-framework-durabletask", "agent-framework-durabletask",
"azure-functions>=1.24.0,<2", "azure-functions>=1.24.0,<2",
"azure-functions-durable>=1.3.1,<2", "azure-functions-durable>=1.3.1,<2",
+2 -2
View File
@@ -4,7 +4,7 @@ description = "Amazon Bedrock integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"
version = "1.0.0b260330" version = "1.0.0b260319"
license-files = ["LICENSE"] license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework" urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,7 +23,7 @@ classifiers = [
"Typing :: Typed", "Typing :: Typed",
] ]
dependencies = [ dependencies = [
"agent-framework-core>=1.0.0rc6", "agent-framework-core>=1.0.0rc5",
"boto3>=1.35.0,<2.0.0", "boto3>=1.35.0,<2.0.0",
"botocore>=1.35.0,<2.0.0", "botocore>=1.35.0,<2.0.0",
] ]
+2 -2
View File
@@ -4,7 +4,7 @@ description = "OpenAI ChatKit integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"
version = "1.0.0b260330" version = "1.0.0b260319"
license-files = ["LICENSE"] license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework" urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -22,7 +22,7 @@ classifiers = [
"Typing :: Typed", "Typing :: Typed",
] ]
dependencies = [ dependencies = [
"agent-framework-core>=1.0.0rc6", "agent-framework-core>=1.0.0rc5",
"openai-chatkit>=1.4.1,<2.0.0", "openai-chatkit>=1.4.1,<2.0.0",
] ]
+2 -2
View File
@@ -4,7 +4,7 @@ description = "Claude Agent SDK integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"
version = "1.0.0b260330" version = "1.0.0b260319"
license-files = ["LICENSE"] license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework" urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,7 +23,7 @@ classifiers = [
"Typing :: Typed", "Typing :: Typed",
] ]
dependencies = [ dependencies = [
"agent-framework-core>=1.0.0rc6", "agent-framework-core>=1.0.0rc5",
"claude-agent-sdk>=0.1.36,<0.1.49", "claude-agent-sdk>=0.1.36,<0.1.49",
] ]
+2 -2
View File
@@ -4,7 +4,7 @@ description = "Copilot Studio integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"
version = "1.0.0b260330" version = "1.0.0b260319"
license-files = ["LICENSE"] license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework" urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,7 +23,7 @@ classifiers = [
"Typing :: Typed", "Typing :: Typed",
] ]
dependencies = [ dependencies = [
"agent-framework-core>=1.0.0rc6", "agent-framework-core>=1.0.0rc5",
"microsoft-agents-copilotstudio-client>=0.3.1,<0.3.2", "microsoft-agents-copilotstudio-client>=0.3.1,<0.3.2",
] ]
+1 -1
View File
@@ -4,7 +4,7 @@ description = "Microsoft Agent Framework for building AI Agents with Python. Thi
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"
version = "1.0.0rc6" version = "1.0.0rc5"
license-files = ["LICENSE"] license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework" urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
+2 -2
View File
@@ -4,7 +4,7 @@ description = "Declarative specification support for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"
version = "1.0.0b260330" version = "1.0.0b260319"
license-files = ["LICENSE"] license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework" urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -22,7 +22,7 @@ classifiers = [
"Typing :: Typed", "Typing :: Typed",
] ]
dependencies = [ dependencies = [
"agent-framework-core>=1.0.0rc6", "agent-framework-core>=1.0.0rc5",
"powerfx>=0.0.32,<0.0.35; python_version < '3.14'", "powerfx>=0.0.32,<0.0.35; python_version < '3.14'",
"pyyaml>=6.0,<7.0", "pyyaml>=6.0,<7.0",
] ]
+2 -2
View File
@@ -4,7 +4,7 @@ description = "Debug UI for Microsoft Agent Framework with OpenAI-compatible API
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"
version = "1.0.0b260330" version = "1.0.0b260319"
license-files = ["LICENSE"] license-files = ["LICENSE"]
urls.homepage = "https://github.com/microsoft/agent-framework" urls.homepage = "https://github.com/microsoft/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,7 +23,7 @@ classifiers = [
"Typing :: Typed", "Typing :: Typed",
] ]
dependencies = [ dependencies = [
"agent-framework-core>=1.0.0rc6", "agent-framework-core>=1.0.0rc5",
"openai>=1.99.0,<3", "openai>=1.99.0,<3",
"opentelemetry-sdk>=1.39.0,<2", "opentelemetry-sdk>=1.39.0,<2",
"fastapi>=0.115.0,<0.133.1", "fastapi>=0.115.0,<0.133.1",
+2 -2
View File
@@ -4,7 +4,7 @@ description = "Durable Task integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"
version = "1.0.0b260330" version = "1.0.0b260319"
license-files = ["LICENSE"] license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework" urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -22,7 +22,7 @@ classifiers = [
"Typing :: Typed", "Typing :: Typed",
] ]
dependencies = [ dependencies = [
"agent-framework-core>=1.0.0rc6", "agent-framework-core>=1.0.0rc5",
"durabletask>=1.3.0,<2", "durabletask>=1.3.0,<2",
"durabletask-azuremanaged>=1.3.0,<2", "durabletask-azuremanaged>=1.3.0,<2",
"python-dateutil>=2.8.0,<3", "python-dateutil>=2.8.0,<3",
+3 -3
View File
@@ -4,7 +4,7 @@ description = "Cloud Azure AI Foundry integration for Microsoft Agent Framework.
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"
version = "1.0.0rc6" version = "1.0.0rc5"
license-files = ["LICENSE"] license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework" urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,8 +23,8 @@ classifiers = [
"Typing :: Typed", "Typing :: Typed",
] ]
dependencies = [ dependencies = [
"agent-framework-core>=1.0.0rc6", "agent-framework-core>=1.0.0rc5",
"agent-framework-openai>=1.0.0rc6", "agent-framework-openai>=1.0.0rc5",
"azure-ai-projects>=2.0.0,<3.0", "azure-ai-projects>=2.0.0,<3.0",
] ]
+3 -3
View File
@@ -4,7 +4,7 @@ description = "Foundry Local integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"
version = "1.0.0b260330" version = "1.0.0b260319"
license-files = ["LICENSE"] license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework" urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,8 +23,8 @@ classifiers = [
"Typing :: Typed", "Typing :: Typed",
] ]
dependencies = [ dependencies = [
"agent-framework-core>=1.0.0rc6", "agent-framework-core>=1.0.0rc5",
"agent-framework-openai>=1.0.0rc6", "agent-framework-openai>=1.0.0rc5",
"foundry-local-sdk>=0.5.1,<0.5.2", "foundry-local-sdk>=0.5.1,<0.5.2",
] ]
@@ -4,7 +4,7 @@ description = "GitHub Copilot integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"
version = "1.0.0b260330" version = "1.0.0b260319"
license-files = ["LICENSE"] license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework" urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,7 +23,7 @@ classifiers = [
"Typing :: Typed", "Typing :: Typed",
] ]
dependencies = [ dependencies = [
"agent-framework-core>=1.0.0rc6", "agent-framework-core>=1.0.0rc5",
"github-copilot-sdk>=0.1.31,<0.1.33; python_version >= '3.11'", "github-copilot-sdk>=0.1.31,<0.1.33; python_version >= '3.11'",
] ]
+2 -2
View File
@@ -4,7 +4,7 @@ description = "Experimental modules for Microsoft Agent Framework"
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"
version = "1.0.0b260330" version = "1.0.0b260319"
license-files = ["LICENSE"] license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework" urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -22,7 +22,7 @@ classifiers = [
"Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3.14",
] ]
dependencies = [ dependencies = [
"agent-framework-core>=1.0.0rc6", "agent-framework-core>=1.0.0rc5",
] ]
[project.optional-dependencies] [project.optional-dependencies]
+2 -2
View File
@@ -4,7 +4,7 @@ description = "Mem0 integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"
version = "1.0.0b260330" version = "1.0.0b260319"
license-files = ["LICENSE"] license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework" urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,7 +23,7 @@ classifiers = [
"Typing :: Typed", "Typing :: Typed",
] ]
dependencies = [ dependencies = [
"agent-framework-core>=1.0.0rc6", "agent-framework-core>=1.0.0rc5",
"mem0ai>=1.0.0,<2", "mem0ai>=1.0.0,<2",
] ]
+2 -2
View File
@@ -4,7 +4,7 @@ description = "Ollama integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"
version = "1.0.0b260330" version = "1.0.0b260319"
license-files = ["LICENSE"] license-files = ["LICENSE"]
urls.homepage = "https://learn.microsoft.com/en-us/agent-framework/" urls.homepage = "https://learn.microsoft.com/en-us/agent-framework/"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,7 +23,7 @@ classifiers = [
"Typing :: Typed", "Typing :: Typed",
] ]
dependencies = [ dependencies = [
"agent-framework-core>=1.0.0rc6", "agent-framework-core>=1.0.0rc5",
"ollama>=0.5.3,<0.5.4", "ollama>=0.5.3,<0.5.4",
] ]
+2 -2
View File
@@ -4,7 +4,7 @@ description = "OpenAI integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"
version = "1.0.0rc6" version = "1.0.0rc5"
license-files = ["LICENSE"] license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework" urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,7 +23,7 @@ classifiers = [
"Typing :: Typed", "Typing :: Typed",
] ]
dependencies = [ dependencies = [
"agent-framework-core>=1.0.0rc6", "agent-framework-core>=1.0.0rc5",
"openai>=1.99.0,<3", "openai>=1.99.0,<3",
"packaging>=24.1,<25", "packaging>=24.1,<25",
] ]
@@ -4,7 +4,7 @@ description = "Orchestration patterns for Microsoft Agent Framework. Includes Se
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"
version = "1.0.0b260330" version = "1.0.0b260319"
license-files = ["LICENSE"] license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework" urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,7 +23,7 @@ classifiers = [
"Typing :: Typed", "Typing :: Typed",
] ]
dependencies = [ dependencies = [
"agent-framework-core>=1.0.0rc6", "agent-framework-core>=1.0.0rc5",
] ]
[tool.uv] [tool.uv]
+2 -2
View File
@@ -4,7 +4,7 @@ description = "Microsoft Purview (Graph dataSecurityAndGovernance) integration f
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"
version = "1.0.0b260330" version = "1.0.0b260319"
license-files = ["LICENSE"] license-files = ["LICENSE"]
urls.homepage = "https://github.com/microsoft/agent-framework" urls.homepage = "https://github.com/microsoft/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -24,7 +24,7 @@ classifiers = [
"Typing :: Typed", "Typing :: Typed",
] ]
dependencies = [ dependencies = [
"agent-framework-core>=1.0.0rc6", "agent-framework-core>=1.0.0rc5",
"azure-core>=1.30.0,<2", "azure-core>=1.30.0,<2",
"httpx>=0.27.0,<0.29", "httpx>=0.27.0,<0.29",
] ]
+2 -2
View File
@@ -4,7 +4,7 @@ description = "Redis integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"
version = "1.0.0b260330" version = "1.0.0b260319"
license-files = ["LICENSE"] license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework" urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,7 +23,7 @@ classifiers = [
"Typing :: Typed", "Typing :: Typed",
] ]
dependencies = [ dependencies = [
"agent-framework-core>=1.0.0rc6", "agent-framework-core>=1.0.0rc5",
"redis>=6.4.0,<7.2.1", "redis>=6.4.0,<7.2.1",
"redisvl>=0.11.0,<0.16", "redisvl>=0.11.0,<0.16",
"numpy>=2.2.6,<3" "numpy>=2.2.6,<3"
+2 -2
View File
@@ -4,7 +4,7 @@ description = "Microsoft Agent Framework for building AI Agents with Python. Thi
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"
version = "1.0.0rc6" version = "1.0.0rc5"
license-files = ["LICENSE"] license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework" urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,7 +23,7 @@ classifiers = [
"Typing :: Typed", "Typing :: Typed",
] ]
dependencies = [ dependencies = [
"agent-framework-core[all]==1.0.0rc6", "agent-framework-core[all]==1.0.0rc5",
] ]
[dependency-groups] [dependency-groups]
-7
View File
@@ -9,13 +9,6 @@ concepts of **Agent Framework** one step at a time.
pip install agent-framework --pre pip install agent-framework --pre
``` ```
Set the required environment variables:
```bash
export AZURE_AI_PROJECT_ENDPOINT="https://your-project-endpoint"
export AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME="gpt-4o" # optional, defaults to gpt-4o
```
## Samples ## Samples
| # | File | What you'll learn | | # | File | What you'll learn |
@@ -70,7 +70,7 @@ async def log_model_input(context: ChatContext, call_next: Any) -> None:
async def main() -> None: async def main() -> None:
client = OpenAIChatClient(model_id="gpt-4o-mini") client = OpenAIChatClient(model="gpt-4o-mini")
# History provider loads/stores conversation messages in session.state. # History provider loads/stores conversation messages in session.state.
# skip_excluded=True means get_messages() will omit messages that were # skip_excluded=True means get_messages() will omit messages that were
@@ -25,11 +25,11 @@ from agent_framework import Agent
from agent_framework.foundry import FoundryChatClient from agent_framework.foundry import FoundryChatClient
from agent_framework.redis import RedisContextProvider from agent_framework.redis import RedisContextProvider
from azure.identity import AzureCliCredential from azure.identity import AzureCliCredential
from dotenv import load_dotenv
from redisvl.extensions.cache.embeddings import EmbeddingsCache from redisvl.extensions.cache.embeddings import EmbeddingsCache
from redisvl.utils.vectorize import OpenAITextVectorizer from redisvl.utils.vectorize import OpenAITextVectorizer
# Copyright (c) Microsoft. All rights reserved. load_dotenv()
# Default Redis URL for local Redis Stack. # Default Redis URL for local Redis Stack.
# Override via the REDIS_URL environment variable for remote or authenticated instances. # Override via the REDIS_URL environment variable for remote or authenticated instances.
+6 -6
View File
@@ -3,7 +3,7 @@
import asyncio import asyncio
from typing import Literal from typing import Literal
from agent_framework import Agent from agent_framework import Agent, Message
from agent_framework.anthropic import AnthropicClient from agent_framework.anthropic import AnthropicClient
from agent_framework.foundry import FoundryChatClient from agent_framework.foundry import FoundryChatClient
from agent_framework.openai import OpenAIChatClient, OpenAIChatOptions from agent_framework.openai import OpenAIChatClient, OpenAIChatOptions
@@ -40,11 +40,11 @@ async def demo_anthropic_chat_client() -> None:
print("\n=== Anthropic ChatClient with TypedDict Options ===\n") print("\n=== Anthropic ChatClient with TypedDict Options ===\n")
# Create Anthropic client # Create Anthropic client
client = AnthropicClient(model="claude-sonnet-4-5-20250929") client = AnthropicClient(model_id="claude-sonnet-4-5-20250929")
# Standard options work great: # Standard options work great:
response = await client.get_response( response = await client.get_response(
"What is the capital of France?", [Message("user", text="What is the capital of France?")],
options={ options={
"temperature": 0.5, "temperature": 0.5,
"max_tokens": 1000, "max_tokens": 1000,
@@ -62,7 +62,7 @@ async def demo_anthropic_agent() -> None:
"""Demonstrate Agent with Anthropic client and typed options.""" """Demonstrate Agent with Anthropic client and typed options."""
print("\n=== Agent with Anthropic and Typed Options ===\n") print("\n=== Agent with Anthropic and Typed Options ===\n")
client = AnthropicClient(model="claude-sonnet-4-5-20250929") client = AnthropicClient(model_id="claude-sonnet-4-5-20250929")
# Create a typed agent for Anthropic - IDE knows Anthropic-specific options! # Create a typed agent for Anthropic - IDE knows Anthropic-specific options!
agent = Agent( agent = Agent(
@@ -119,12 +119,12 @@ async def demo_openai_chat_client_reasoning_models() -> None:
print("\n=== OpenAI ChatClient with TypedDict Options ===\n") print("\n=== OpenAI ChatClient with TypedDict Options ===\n")
# Create OpenAI client # Create OpenAI client
client = OpenAIChatClient[OpenAIReasoningChatOptions](model_id="o3") client = OpenAIChatClient[OpenAIReasoningChatOptions](model="o3")
# With specific options, you get full IDE autocomplete! # With specific options, you get full IDE autocomplete!
# Try typing `client.get_response("Hello", options={` and see the suggestions # Try typing `client.get_response("Hello", options={` and see the suggestions
response = await client.get_response( response = await client.get_response(
"What is 2 + 2?", [Message("user", text="What is 2 + 2?")],
options={ options={
"max_tokens": 100, "max_tokens": 100,
"allow_multiple_tool_calls": True, "allow_multiple_tool_calls": True,
+2 -2
View File
@@ -172,7 +172,7 @@ Workflow and orchestration samples use `AzureOpenAIResponsesClient` rather than
Workflow samples that use `AzureOpenAIResponsesClient` expect: Workflow samples that use `AzureOpenAIResponsesClient` expect:
- `AZURE_AI_PROJECT_ENDPOINT` (Azure AI Foundry Agent Service (V2) project endpoint) - `FOUNDRY_PROJECT_ENDPOINT` (Azure AI Foundry Agent Service (V2) project endpoint)
- `AZURE_AI_MODEL_DEPLOYMENT_NAME` (model deployment name) - `FOUNDRY_MODEL` (model deployment name)
These values are passed directly into the client constructor via `os.getenv()` in sample code. These values are passed directly into the client constructor via `os.getenv()` in sample code.
@@ -24,7 +24,7 @@ how agents can be used in a workflow.
Prerequisites: Prerequisites:
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. - FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Azure OpenAI configured for FoundryChatClient with required environment variables. - FOUNDRY_MODEL must be the deployment name of a model in your Foundry project.
- Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample. - Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample.
- Basic familiarity with WorkflowBuilder, edges, events, and streaming or non-streaming runs. - Basic familiarity with WorkflowBuilder, edges, events, and streaming or non-streaming runs.
""" """
@@ -35,7 +35,7 @@ async def main():
# Create the Azure chat client. AzureCliCredential uses your current az login. # Create the Azure chat client. AzureCliCredential uses your current az login.
client = FoundryChatClient( client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(), credential=AzureCliCredential(),
) )
writer_agent = Agent( writer_agent = Agent(
@@ -23,7 +23,7 @@ how agents can be used in a workflow.
Prerequisites: Prerequisites:
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. - FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Azure OpenAI configured for FoundryChatClient with required environment variables. - FOUNDRY_MODEL must be the deployment name of a model in your Foundry project.
- Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample. - Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample.
- Basic familiarity with WorkflowBuilder, executors, edges, events, and streaming runs. - Basic familiarity with WorkflowBuilder, executors, edges, events, and streaming runs.
""" """
@@ -34,7 +34,7 @@ async def main():
# Create the Azure chat client. AzureCliCredential uses your current az login. # Create the Azure chat client. AzureCliCredential uses your current az login.
client = FoundryChatClient( client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(), credential=AzureCliCredential(),
) )
writer_agent = Agent( writer_agent = Agent(
@@ -18,7 +18,7 @@ This sample shows how to create agents backed by Azure OpenAI Responses and use
Prerequisites: Prerequisites:
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. - FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- AZURE_AI_MODEL_DEPLOYMENT_NAME must be set to your Azure OpenAI model deployment name. - FOUNDRY_MODEL must be the deployment name of a model in your Foundry project.
- Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample. - Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample.
- Basic familiarity with WorkflowBuilder, edges, events, and streaming runs. - Basic familiarity with WorkflowBuilder, edges, events, and streaming runs.
""" """
@@ -27,7 +27,7 @@ Prerequisites:
async def main() -> None: async def main() -> None:
client = FoundryChatClient( client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(), credential=AzureCliCredential(),
) )
@@ -39,7 +39,7 @@ Demonstrate:
Prerequisites: Prerequisites:
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. - FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- AZURE_AI_MODEL_DEPLOYMENT_NAME must be set to your Azure OpenAI model deployment name. - FOUNDRY_MODEL must be set to your Azure OpenAI model deployment name.
- Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample. - Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample.
- Basic familiarity with agents, workflows, and executors in the agent framework. - Basic familiarity with agents, workflows, and executors in the agent framework.
""" """
@@ -60,7 +60,7 @@ async def intercept_agent_response(
async def main() -> None: async def main() -> None:
client = FoundryChatClient( client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(), credential=AzureCliCredential(),
) )
@@ -37,7 +37,7 @@ Demonstrates:
Prerequisites: Prerequisites:
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. - FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Azure OpenAI configured for FoundryChatClient with required environment variables. - FOUNDRY_MODEL must be set to your Azure OpenAI model deployment name.
- Authentication via azure-identity. Run `az login` before executing. - Authentication via azure-identity. Run `az login` before executing.
""" """
@@ -104,7 +104,7 @@ async def main() -> None:
research_agent = Agent( research_agent = Agent(
client=FoundryChatClient( client=FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(), credential=AzureCliCredential(),
), ),
name="research_agent", name="research_agent",
@@ -116,7 +116,7 @@ async def main() -> None:
final_editor_agent = Agent( final_editor_agent = Agent(
client=FoundryChatClient( client=FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(), credential=AzureCliCredential(),
), ),
name="final_editor_agent", name="final_editor_agent",
@@ -18,7 +18,7 @@ This sample shows how to create AzureOpenAI Chat Agents and use them in a workfl
Prerequisites: Prerequisites:
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. - FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Azure OpenAI configured for FoundryChatClient with required environment variables. - FOUNDRY_MODEL must be set to your Azure OpenAI model deployment name.
- Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample. - Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample.
- Basic familiarity with WorkflowBuilder, edges, events, and streaming runs. - Basic familiarity with WorkflowBuilder, edges, events, and streaming runs.
""" """
@@ -29,7 +29,7 @@ async def main():
# Create the agents # Create the agents
_writer_client = FoundryChatClient( _writer_client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(), credential=AzureCliCredential(),
) )
writer_agent = Agent( writer_agent = Agent(
@@ -42,7 +42,7 @@ async def main():
_reviewer_client = FoundryChatClient( _reviewer_client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(), credential=AzureCliCredential(),
) )
reviewer_agent = Agent( reviewer_agent = Agent(
@@ -49,7 +49,7 @@ Demonstrates:
Prerequisites: Prerequisites:
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. - FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Azure OpenAI configured for FoundryChatClient with required environment variables. - FOUNDRY_MODEL must be set to your Azure OpenAI model deployment name.
- Authentication via azure-identity. Run `az login` before executing. - Authentication via azure-identity. Run `az login` before executing.
""" """
@@ -122,11 +122,7 @@ class Coordinator(Executor):
# Writer agent response; request human feedback. # Writer agent response; request human feedback.
# Preserve the full conversation so the final editor # Preserve the full conversation so the final editor
# can see tool traces and the initial prompt. # can see tool traces and the initial prompt.
conversation: list[Message] conversation = list(draft.full_conversation)
if draft.full_conversation is not None:
conversation = list(draft.full_conversation)
else:
conversation = list(draft.agent_response.messages)
draft_text = draft.agent_response.text.strip() draft_text = draft.agent_response.text.strip()
if not draft_text: if not draft_text:
draft_text = "No draft text was produced." draft_text = "No draft text was produced."
@@ -178,7 +174,7 @@ def create_writer_agent() -> Agent:
return Agent( return Agent(
client=FoundryChatClient( client=FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(), credential=AzureCliCredential(),
), ),
name="writer_agent", name="writer_agent",
@@ -188,7 +184,9 @@ def create_writer_agent() -> Agent:
"produce a 3-sentence draft." "produce a 3-sentence draft."
), ),
tools=[fetch_product_brief, get_brand_voice_profile], tools=[fetch_product_brief, get_brand_voice_profile],
tool_choice="required", default_options={
"tool_choice": "required",
},
) )
@@ -197,7 +195,7 @@ def create_final_editor_agent() -> Agent:
return Agent( return Agent(
client=FoundryChatClient( client=FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(), credential=AzureCliCredential(),
), ),
name="final_editor_agent", name="final_editor_agent",
@@ -25,7 +25,7 @@ Demonstrates:
Prerequisites: Prerequisites:
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. - FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Azure OpenAI access configured for FoundryChatClient (use az login + env vars) - FOUNDRY_MODEL must be set to your Azure OpenAI model deployment name.
- Familiarity with Workflow events (WorkflowEvent with type "output") - Familiarity with Workflow events (WorkflowEvent with type "output")
""" """
@@ -34,7 +34,7 @@ async def main() -> None:
# 1) Create three domain agents using FoundryChatClient # 1) Create three domain agents using FoundryChatClient
client = FoundryChatClient( client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(), credential=AzureCliCredential(),
) )
@@ -69,7 +69,7 @@ async def main() -> None:
workflow = ConcurrentBuilder(participants=[researcher, marketer, legal]).build() workflow = ConcurrentBuilder(participants=[researcher, marketer, legal]).build()
# 3) Expose the concurrent workflow as an agent for easy reuse # 3) Expose the concurrent workflow as an agent for easy reuse
agent = Agent(client=workflow, name="ConcurrentWorkflowAgent") agent = workflow.as_agent()
prompt = "We are launching a new budget-friendly electric bike for urban commuters." prompt = "We are launching a new budget-friendly electric bike for urban commuters."
agent_response = await agent.run(prompt) agent_response = await agent.run(prompt)
@@ -33,7 +33,7 @@ Note: When an agent is passed to a workflow, the workflow wraps the agent in a m
Prerequisites: Prerequisites:
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. - FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Azure OpenAI configured for FoundryChatClient with required environment variables. - FOUNDRY_MODEL must be set to your Azure OpenAI model deployment name.
- Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample. - Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample.
- Basic familiarity with WorkflowBuilder, executors, edges, events, and streaming or non streaming runs. - Basic familiarity with WorkflowBuilder, executors, edges, events, and streaming or non streaming runs.
""" """
@@ -54,7 +54,7 @@ class Writer(Executor):
self.agent = Agent( self.agent = Agent(
client=FoundryChatClient( client=FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(), credential=AzureCliCredential(),
), ),
instructions=( instructions=(
@@ -101,7 +101,7 @@ class Reviewer(Executor):
self.agent = Agent( self.agent = Agent(
client=FoundryChatClient( client=FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(), credential=AzureCliCredential(),
), ),
instructions=( instructions=(
@@ -32,7 +32,7 @@ async def main() -> None:
instructions="Gather concise facts that help a teammate answer the question.", instructions="Gather concise facts that help a teammate answer the question.",
client=FoundryChatClient( client=FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(), credential=AzureCliCredential(),
), ),
) )
@@ -43,14 +43,14 @@ async def main() -> None:
instructions="Compose clear and structured answers using any notes provided.", instructions="Compose clear and structured answers using any notes provided.",
client=FoundryChatClient( client=FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(), credential=AzureCliCredential(),
), ),
) )
_orch_client = FoundryChatClient( _orch_client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(), credential=AzureCliCredential(),
) )
@@ -72,7 +72,7 @@ async def main() -> None:
print(f"Input: {task}\n") print(f"Input: {task}\n")
try: try:
workflow_agent = Agent(client=workflow, name="GroupChatWorkflowAgent") workflow_agent = workflow.as_agent()
agent_result = await workflow_agent.run(task) agent_result = await workflow_agent.run(task)
if agent_result.messages: if agent_result.messages:
@@ -31,7 +31,7 @@ them to transfer control to each other based on the conversation context.
Prerequisites: Prerequisites:
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. - FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- `az login` (Azure CLI authentication) - `az login` (Azure CLI authentication)
- Environment variables configured for FoundryChatClient (AZURE_AI_MODEL_DEPLOYMENT_NAME) - Environment variables configured for FoundryChatClient (FOUNDRY_MODEL)
Key Concepts: Key Concepts:
- Auto-registered handoff tools: HandoffBuilder automatically creates handoff tools - Auto-registered handoff tools: HandoffBuilder automatically creates handoff tools
@@ -159,7 +159,7 @@ async def main() -> None:
# Initialize the Azure OpenAI chat client # Initialize the Azure OpenAI chat client
client = FoundryChatClient( client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(), credential=AzureCliCredential(),
) )
@@ -174,21 +174,20 @@ async def main() -> None:
# Without this, the default behavior continues requesting user input until max_turns # Without this, the default behavior continues requesting user input until max_turns
# is reached. Here we use a custom condition that checks if the conversation has ended # is reached. Here we use a custom condition that checks if the conversation has ended
# naturally (when one of the agents says something like "you're welcome"). # naturally (when one of the agents says something like "you're welcome").
agent = Agent( agent = (
client=( HandoffBuilder(
HandoffBuilder( name="customer_support_handoff",
name="customer_support_handoff", participants=[triage, refund, order, support],
participants=[triage, refund, order, support], # Custom termination: Check if one of the agents has provided a closing message.
# Custom termination: Check if one of the agents has provided a closing message. # This looks for the last message containing "welcome", which indicates the
# This looks for the last message containing "welcome", which indicates the # conversation has concluded naturally.
# conversation has concluded naturally. termination_condition=lambda conversation: (
termination_condition=lambda conversation: ( len(conversation) > 0 and "welcome" in conversation[-1].text.lower()
len(conversation) > 0 and "welcome" in conversation[-1].text.lower() ),
), )
) .with_start_agent(triage)
.with_start_agent(triage) .build()
.build() .as_agent()
),
) )
# Scripted user responses for reproducible demo # Scripted user responses for reproducible demo
@@ -226,7 +225,7 @@ async def main() -> None:
responses = {req_id: HandoffAgentUserRequest.create_response(user_response) for req_id in pending_requests} responses = {req_id: HandoffAgentUserRequest.create_response(user_response) for req_id in pending_requests}
function_results = [ function_results = [
Content.from_function_result(call_id=req_id, result=response) for req_id, response in responses.items() Content("function_result", call_id=req_id, result=response) for req_id, response in responses.items()
] ]
response = await agent.run(Message("tool", function_results)) response = await agent.run(Message("tool", function_results))
pending_requests = handle_response_and_requests(response) pending_requests = handle_response_and_requests(response)
@@ -23,7 +23,7 @@ like any other agent while still emitting callback telemetry.
Prerequisites: Prerequisites:
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. - FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- OpenAI credentials configured for `FoundryChatClient` and `FoundryChatClient`. - FOUNDRY_MODEL must be set to your Azure OpenAI model deployment name.
""" """
@@ -37,7 +37,7 @@ async def main() -> None:
# This agent requires the gpt-4o-search-preview model to perform web searches. # This agent requires the gpt-4o-search-preview model to perform web searches.
client=FoundryChatClient( client=FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(), credential=AzureCliCredential(),
), ),
) )
@@ -45,7 +45,7 @@ async def main() -> None:
# Create code interpreter tool using instance method # Create code interpreter tool using instance method
coder_client = FoundryChatClient( coder_client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(), credential=AzureCliCredential(),
) )
code_interpreter_tool = coder_client.get_code_interpreter_tool() code_interpreter_tool = coder_client.get_code_interpreter_tool()
@@ -65,7 +65,7 @@ async def main() -> None:
instructions="You coordinate a team to complete complex tasks efficiently.", instructions="You coordinate a team to complete complex tasks efficiently.",
client=FoundryChatClient( client=FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(), credential=AzureCliCredential(),
), ),
) )
@@ -98,7 +98,7 @@ async def main() -> None:
try: try:
# Wrap the workflow as an agent for composition scenarios # Wrap the workflow as an agent for composition scenarios
print("\nWrapping workflow as an agent and running...") print("\nWrapping workflow as an agent and running...")
workflow_agent = Agent(client=workflow, name="MagenticWorkflowAgent") workflow_agent = workflow.as_agent()
last_response_id: str | None = None last_response_id: str | None = None
async for update in workflow_agent.run(task, stream=True): async for update in workflow_agent.run(task, stream=True):
@@ -27,7 +27,7 @@ Note on internal adapters:
Prerequisites: Prerequisites:
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. - FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Azure OpenAI access configured for FoundryChatClient (use az login + env vars) - FOUNDRY_MODEL must be set to your Azure OpenAI model deployment name.
""" """
@@ -35,7 +35,7 @@ async def main() -> None:
# 1) Create agents # 1) Create agents
client = FoundryChatClient( client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(), credential=AzureCliCredential(),
) )
@@ -55,7 +55,7 @@ async def main() -> None:
workflow = SequentialBuilder(participants=[writer, reviewer]).build() workflow = SequentialBuilder(participants=[writer, reviewer]).build()
# 3) Treat the workflow itself as an agent for follow-up invocations # 3) Treat the workflow itself as an agent for follow-up invocations
agent = Agent(client=workflow, name="SequentialWorkflowAgent") agent = workflow.as_agent()
prompt = "Write a tagline for a budget-friendly eBike." prompt = "Write a tagline for a budget-friendly eBike."
agent_response = await agent.run(prompt) agent_response = await agent.run(prompt)
@@ -8,7 +8,6 @@ from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from agent_framework import Agent
from agent_framework.foundry import FoundryChatClient from agent_framework.foundry import FoundryChatClient
from azure.identity import AzureCliCredential from azure.identity import AzureCliCredential
from dotenv import load_dotenv from dotenv import load_dotenv
@@ -50,7 +49,7 @@ to the Worker. The workflow completes when idle.
Prerequisites: Prerequisites:
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. - FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- OpenAI account configured and accessible for FoundryChatClient. - FOUNDRY_MODEL must be set to your Azure OpenAI model deployment name.
- Familiarity with WorkflowBuilder, Executor, and WorkflowContext from agent_framework. - Familiarity with WorkflowBuilder, Executor, and WorkflowContext from agent_framework.
- Understanding of request-response message handling in executors. - Understanding of request-response message handling in executors.
- (Optional) Review of reflection and escalation patterns, such as those in - (Optional) Review of reflection and escalation patterns, such as those in
@@ -113,14 +112,14 @@ async def main() -> None:
id="worker", id="worker",
client=FoundryChatClient( client=FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(), credential=AzureCliCredential(),
), ),
) )
reviewer = ReviewerWithHumanInTheLoop(worker_id="worker") reviewer = ReviewerWithHumanInTheLoop(worker_id="worker")
agent = Agent( agent = (
client=(WorkflowBuilder(start_executor=worker).add_edge(worker, reviewer).add_edge(reviewer, worker).build()), WorkflowBuilder(start_executor=worker).add_edge(worker, reviewer).add_edge(reviewer, worker).build().as_agent()
) )
print("Running workflow agent with user query...") print("Running workflow agent with user query...")
@@ -165,7 +164,8 @@ async def main() -> None:
human_response = ReviewResponse(request_id=request_id, feedback="", approved=True) human_response = ReviewResponse(request_id=request_id, feedback="", approved=True)
# Create the function call result object to send back to the agent. # Create the function call result object to send back to the agent.
human_review_function_result = Content.from_function_result( human_review_function_result = Content(
"function_result",
call_id=human_review_function_call.call_id, # type: ignore call_id=human_review_function_call.call_id, # type: ignore
result=human_response, result=human_response,
) )
@@ -35,7 +35,7 @@ When to use Agent(client=workflow,):
Prerequisites: Prerequisites:
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. - FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Environment variables configured - FOUNDRY_MODEL must be set to your Azure OpenAI model deployment name.
""" """
@@ -89,7 +89,7 @@ async def main() -> None:
# Create chat client # Create chat client
client = FoundryChatClient( client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(), credential=AzureCliCredential(),
) )
@@ -109,7 +109,7 @@ async def main() -> None:
workflow = SequentialBuilder(participants=[agent]).build() workflow = SequentialBuilder(participants=[agent]).build()
# Expose the workflow as an agent Agent(client=using,) # Expose the workflow as an agent Agent(client=using,)
workflow_agent = Agent(client=workflow, name="WorkflowAgent") workflow_agent = workflow.as_agent()
# Define custom context that will flow to tools via kwargs # Define custom context that will flow to tools via kwargs
custom_data = { custom_data = {
@@ -6,7 +6,6 @@ from dataclasses import dataclass
from uuid import uuid4 from uuid import uuid4
from agent_framework import ( from agent_framework import (
Agent,
AgentResponse, AgentResponse,
Executor, Executor,
Message, Message,
@@ -41,7 +40,7 @@ Key Concepts Demonstrated:
Prerequisites: Prerequisites:
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. - FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- OpenAI account configured and accessible for FoundryChatClient. - FOUNDRY_MODEL must be set to your Azure OpenAI model deployment name.
- Familiarity with WorkflowBuilder, Executor, WorkflowContext, and event handling. - Familiarity with WorkflowBuilder, Executor, WorkflowContext, and event handling.
- Understanding of how agent messages are generated, reviewed, and re-submitted. - Understanding of how agent messages are generated, reviewed, and re-submitted.
""" """
@@ -198,7 +197,7 @@ async def main() -> None:
id="worker", id="worker",
client=FoundryChatClient( client=FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(), credential=AzureCliCredential(),
), ),
) )
@@ -206,13 +205,13 @@ async def main() -> None:
id="reviewer", id="reviewer",
client=FoundryChatClient( client=FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(), credential=AzureCliCredential(),
), ),
) )
agent = Agent( agent = (
client=(WorkflowBuilder(start_executor=worker).add_edge(worker, reviewer).add_edge(reviewer, worker).build()), WorkflowBuilder(start_executor=worker).add_edge(worker, reviewer).add_edge(reviewer, worker).build().as_agent()
) )
print("Running workflow agent with user query...") print("Running workflow agent with user query...")
@@ -38,7 +38,7 @@ Use cases:
Prerequisites: Prerequisites:
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. - FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Environment variables configured for FoundryChatClient - FOUNDRY_MODEL must be set to your Azure OpenAI model deployment name.
""" """
@@ -46,7 +46,7 @@ async def main() -> None:
# Create a chat client # Create a chat client
client = FoundryChatClient( client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(), credential=AzureCliCredential(),
) )
@@ -72,7 +72,7 @@ async def main() -> None:
workflow = SequentialBuilder(participants=[assistant, summarizer]).build() workflow = SequentialBuilder(participants=[assistant, summarizer]).build()
# Wrap the workflow as an agent # Wrap the workflow as an agent
agent = Agent(client=workflow, name="ConversationalWorkflowAgent") agent = workflow.as_agent()
# Create a session to maintain history # Create a session to maintain history
session = agent.create_session() session = agent.create_session()
@@ -133,7 +133,7 @@ async def demonstrate_session_serialization() -> None:
""" """
client = FoundryChatClient( client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(), credential=AzureCliCredential(),
) )
@@ -144,7 +144,7 @@ async def demonstrate_session_serialization() -> None:
) )
workflow = SequentialBuilder(participants=[memory_assistant]).build() workflow = SequentialBuilder(participants=[memory_assistant]).build()
agent = Agent(client=workflow, name="MemoryWorkflowAgent") agent = workflow.as_agent()
# Create initial session and have a conversation # Create initial session and have a conversation
session = agent.create_session() session = agent.create_session()
@@ -182,7 +182,7 @@ def create_workflow(checkpoint_storage: FileCheckpointStorage) -> Workflow:
writer_agent = Agent( writer_agent = Agent(
client=FoundryChatClient( client=FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(), credential=AzureCliCredential(),
), ),
instructions="Write concise, warm release notes that sound human and helpful.", instructions="Write concise, warm release notes that sound human and helpful.",
@@ -21,7 +21,7 @@ Key concepts:
Prerequisites: Prerequisites:
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. - FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Environment variables configured for FoundryChatClient - FOUNDRY_MODEL must be set to your Azure OpenAI model deployment name.
""" """
import asyncio import asyncio
@@ -50,7 +50,7 @@ async def basic_checkpointing() -> None:
client = FoundryChatClient( client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(), credential=AzureCliCredential(),
) )
@@ -67,7 +67,7 @@ async def basic_checkpointing() -> None:
) )
workflow = SequentialBuilder(participants=[assistant, reviewer]).build() workflow = SequentialBuilder(participants=[assistant, reviewer]).build()
agent = Agent(client=workflow, name="CheckpointedAgent") agent = workflow.as_agent()
# Create checkpoint storage # Create checkpoint storage
checkpoint_storage = InMemoryCheckpointStorage() checkpoint_storage = InMemoryCheckpointStorage()
@@ -97,7 +97,7 @@ async def checkpointing_with_thread() -> None:
client = FoundryChatClient( client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(), credential=AzureCliCredential(),
) )
@@ -108,7 +108,7 @@ async def checkpointing_with_thread() -> None:
) )
workflow = SequentialBuilder(participants=[assistant]).build() workflow = SequentialBuilder(participants=[assistant]).build()
agent = Agent(client=workflow, name="MemoryAgent") agent = workflow.as_agent()
# Create both session (for conversation) and checkpoint storage (for workflow state) # Create both session (for conversation) and checkpoint storage (for workflow state)
session = agent.create_session() session = agent.create_session()
@@ -145,7 +145,7 @@ async def streaming_with_checkpoints() -> None:
client = FoundryChatClient( client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(), credential=AzureCliCredential(),
) )
@@ -156,7 +156,7 @@ async def streaming_with_checkpoints() -> None:
) )
workflow = SequentialBuilder(participants=[assistant]).build() workflow = SequentialBuilder(participants=[assistant]).build()
agent = Agent(client=workflow, name="StreamingCheckpointAgent") agent = workflow.as_agent()
checkpoint_storage = InMemoryCheckpointStorage() checkpoint_storage = InMemoryCheckpointStorage()
@@ -34,7 +34,7 @@ Key Concepts:
Prerequisites: Prerequisites:
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. - FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Environment variables configured - FOUNDRY_MODEL must be set to your Azure OpenAI model deployment name.
""" """
@@ -84,7 +84,7 @@ async def main() -> None:
# Create chat client # Create chat client
client = FoundryChatClient( client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(), credential=AzureCliCredential(),
) )
@@ -139,7 +139,7 @@ def create_spam_detector_agent() -> Agent:
return Agent( return Agent(
client=FoundryChatClient( client=FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(), credential=AzureCliCredential(),
), ),
instructions=( instructions=(
@@ -158,7 +158,7 @@ def create_email_assistant_agent() -> Agent:
return Agent( return Agent(
client=FoundryChatClient( client=FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(), credential=AzureCliCredential(),
), ),
instructions=( instructions=(

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