Python: Merge durabletask changes into main (#3420)

* Python: Add initial scaffold for `durabletask` package (#2761)

* Add initial scaffold

* Update design

* Fix mypy and update design

* add additional style considered

* Address comments

* Fix test

* Update readmes

* Python: Rebase durable task feature branch with main (#2806)

* Python: Add Entity State Providers for DurableTask Package (#2981)

* Add Entity State Providers

* address comments

* Fix tests

* Fix tests

* Revert unrelated changes and remove thread_id

* Revert unrelated files

* Python: [Durabletask] Update `feature-durabletask-python` branch with `main` (#3068)

* Python: Add factory pattern to concurrent orchestration builder (#2738)

* Add factory pattern to concurrent orchestration builder

* Update readme

* Address AI comments

* Fix unit tests

* Fix import

* Prevent multiple calls to set participants or factories

* Add comments

* Mitigate warnings

* Fix mypy

* Address comments

* Address Copilot comments

* Fix tests

* Python: fix: GroupChat ManagerSelectionResponse JSON Schema for OpenAI Structured Outpu… (#2750)

* fix: ManagerSelectionResponse JSON Schema for OpenAI Structured Output Strict Mode

* refactor: install pre-commit then commit again

* Capture file IDs from code interpreter in streaming responses (#2741)

* .NET: [BREAKING] Prevent nulls in AIAgent property (#2719)

* prevent nulls in AIAgent property

* address feedback

* code ql sm04598 (#2723)

Co-authored-by: Mark Wallace <127216156+markwallace-microsoft@users.noreply.github.com>

* .NET: Add Conversation State Sample (Step05) (#2697)

* Initial plan

* Add Agent_OpenAI_Step05_Conversation sample for conversation state management

Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>

* Update Program.cs comment to accurately describe the sample

Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>

* Update the code to use the ConversationClient more in line with the samples in OpenAI

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Changing sample to use ChatClientAgent and conversationId in GetNewThread

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Bump AWSSDK.Extensions.Bedrock.MEAI from 4.0.4.7 to 4.0.4.11 (#2777)

---
updated-dependencies:
- dependency-name: AWSSDK.Extensions.Bedrock.MEAI
  dependency-version: 4.0.4.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump Azure.Identity from 1.17.0 to 1.17.1 (#2780)

---
updated-dependencies:
- dependency-name: Azure.Identity
  dependency-version: 1.17.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: Azure.Identity
  dependency-version: 1.17.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: Azure.Identity
  dependency-version: 1.17.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: Azure.Identity
  dependency-version: 1.17.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump Azure.AI.AgentServer.AgentFramework from 1.0.0-beta.4 to 1.0.0-beta.5 (#2778)

---
updated-dependencies:
- dependency-name: Azure.AI.AgentServer.AgentFramework
  dependency-version: 1.0.0-beta.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: Azure.AI.AgentServer.AgentFramework
  dependency-version: 1.0.0-beta.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: Azure.AI.AgentServer.AgentFramework
  dependency-version: 1.0.0-beta.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Python: added more complete parsing for mcp tool arguments (#2756)

* added more complete parsing for mcp tool arguments

* fixed mypy

* added nonlocal model counter, and some fixes

* fixes in naming logic

* extracted json parsing function, added parametrized test and checked coverage

* Python: Updated package versions (#2784)

* Updated package versions

* Small fix

* Bump actions/checkout from 5 to 6 (#2404)

Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>

* .NET: adds support for labels in edges,  fixes rendering of labels in dot a… (#1507)

* adds support for labels in edges,  fixes rendering of labels in dot and mermaid, adds rendering of labels in edges

* Update dotnet/src/Microsoft.Agents.AI.Workflows/Visualization/WorkflowVisualizer.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* escaping edge labels, adding tests for labels containing strange characters that would break the diagram and enabling the previous signature so the API has backwards compatibility.

* Unify label in EdgeData

* Edge API adjustments, removed useless "sanitizer"

* fixed test

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Jacob Alber <jaalber@microsoft.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>

* Python: Added custom args and thread object to ai_function kwargs (#2769)

* Added an example of using kwargs in ai_function

* Added thread object to ai_function kwargs

* Updated docs

* Small fix

* Added thread parameter filtering

* Fix WorkflowAgent to include thread convo history. Enable checkpointing. (#2774)

* Update OpenAIResponses.yaml to match AgentSchema (#2598)

1. Update `connection` child types --  `kind: ApiKey` to `kind: key` otherwise schema will fail: https://microsoft.github.io/AgentSchema/reference/apikeyconnection/

2.  Update `outputSchema`'s `PropertySchema` to be `kind` instead of `type` otherwise schema will fail: https://microsoft.github.io/AgentSchema/reference/propertyschema/

* Python: Remove warnings from workflow builder on not using factories (#2808)

* Revert concurrent

* Fix comments

* Python: Filter framework kwargs from MCP tool invocations (#2870)

* Filter framework kwargs from MCP tool invocations

* Fixes

* Python: Fix WorkflowAgent to emit yield_output as agent response (#2866)

* Fix WorkflowAgent to emit yield_output as agent response

* use raw_representation

* Raw representation handling

* Python: Use agent description in HandoffBuilder auto-generated tools (#2713) (#2714)

## Summary
Enhanced `HandoffBuilder._apply_auto_tools` to use the target agent's
description when creating handoff tools, providing more informative tool
descriptions for LLMs.

## Changes
- Modified `_apply_auto_tools` to extract `description` from
  `AgentExecutor._agent` when available
- Updated iteration to use `.items()` for more efficient dict traversal
- Handoff tools now use agent descriptions instead of generic placeholders

## Example
Before: "Handoff to the refund_agent agent."
After: "You handle refund requests. Ask for order details and process refunds."

## Testing
- All handoff tests pass (20/20)
- No breaking changes to existing API

Fixes #2713

Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>

* Python: [BREAKING] Observability updates (#2782)

* fixes Python: Add env_file_path parameter to setup_observability() similar to AzureOpenAIChatClient
Fixes #2186

* WIP on updates using configure_azure_monitor

* improved setup and clarity

* fixed root .env.example

* revert changes

* updated files

* updated sample

* updated zero code

* test fixes and fixed links

* fix devui

* removed planning docs

* added enable method and updated readme and samples

* clarified docstring

* add return annotation

* updated naming

* update capatilized version

* updated readme and some fixes

* updated decorator name inline with the rest

* feedback from comments addressed

* Python: Fix middleware terminate flag to exit function calling loop immediately (#2868)

* Fix middleware terminate flag to exit function calling loop immediately

* Eliminating duck typing

* Improve function exec result handling

* Fix race condition

* Fix mypy issues

* Python: Fix context duplication in handoff workflows when restoring from checkpoint (#2867)

* Fix context duplication in handoff workflows when restoring from checkpoint

* Address Copilot PR review

* .NET: Update to latest Azure.AI.*, OpenAI, and M.E.AI* (#2850)

* Update to latest Azure.AI.*, OpenAI, and M.E.AI*

Absorb breaking changes in Responses surface area

* Update dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Utilities/ChatClientExtensions.cs

* Update dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Utilities/ChatClientExtensions.cs

* Update dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Utilities/ChatClientExtensions.cs

* Update dotnet/samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step04_CreateFromOpenAIResponseClient/Program.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Using patch to remove the model is necessary, updated the response client to actually use the the ForAgent

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>

* Bump actions/download-artifact from 6 to 7 (#2862)

Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 6 to 7.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump actions/cache from 4 to 5 (#2861)

Bumps [actions/cache](https://github.com/actions/cache) from 4 to 5.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump actions/upload-artifact from 5 to 6 (#2860)

Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 5 to 6.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Python : Ollama Connector for Agent Framework (#1104)

* Initial Commit for Olama Connector

* Added Olama Sample

* Add Sample & Fixed Open Telemetry

* Fixed Spelling from Olama to Ollama

* remove"opentelemetry-semantic-conventions-ai ~=0.4.13" since its handled in a different pr

* Added Tool Calling

* Finalizing test cases

* Adjust samples to be more reliable

* Update python/packages/ollama/agent_framework_ollama/_chat_client.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update python/packages/ollama/pyproject.toml

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update python/packages/ollama/tests/test_ollama_chat_client.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update python/packages/ollama/agent_framework_ollama/_chat_client.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Improved Docstrings & Sample

* Update python/packages/ollama/agent_framework_ollama/_chat_client.py

Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>

* Integrate PR Feedback
- Divided Streaming and Non-Streaming into independent Methods
- Catch Ollama Validation Error
- Add OTEL Provider Name
- Checked Ollama Messages
- Add Usage Statistics

* Revert setting, so it can be none

* Validate Message formatting between AF and Ollama

* Catch Ollama Error and raise a ServiceResponse Error

* Fix mypy error

* remove .vscode comma

* Add Reasoning support & adjust to new structure

* Add Ollama Multimodality and Reasoning

* Add test cases for reasoning

* Add Tests for Error Handling in Ollama Client

* Update python/samples/getting_started/multimodal_input/ollama_chat_multimodal.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Integrated Copilot Feedback

* Implement first PR Feedback

* Adjust Readme files for examples

* Adjust argument passing via additional chat options

* Implemented PR Feedback

* Removing Ollama Package from Core and moving samples

* Fix Link & Adding Samples to Main Sample Readme

* Fixing Links in Readme

* Moved Multimodal and Chat Example

* Fixed Link in ChatClient to Ollama

* Fix AgentFramework Links in Ollama Project

* Fix observability breaking change

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>

* Skip failing IT (#2904)

* .NET: Cosmos DB UT Fast Skip (For Non-Configured Local envs) (#2906)

* Cosmos DB UT Fast Skip (Non-Configured Local envs) + Long running UT skip in pipeline when no CosmosDB changes happened

* Force a CosmosDB source code change to trigger the pipeline

* Address possible string boolean mismatch

* Add debug

* Enabling emulator always when running IT

* .NET: Add TTLs to durable agent sessions (#2679)

* .NET: Add TTLs to durable agent sessions

* Remove unnecessary async

* PR feedback: clarify UTC

* PR feedback: limit minimum signal delay to <= 5 minutes

* PR feedback: Fix TTL disablement

* Linter: use auto-property

* Fix build break from OpenAI SDK change

* Updated CHANGELOG.md

* PR feedback

* Reduce default TTL to 14 days to work around DTS bug

* Python:  Update Mem0Provider to use v2 search API `filters` parameter (#2766)

* short fix to move id parameters to filters object

* added tests

* small fix

* mem0 dependency update

* Updated package versions (#2913)

* .NET: Switch to new "Run" method name. (#2843)

* Switch to new "RunAgent" method name.

* Try to disable false positive naming warning.

* Add comment about disabled warnings.

* Rename `RunAgent` to just `Run`.

* Update CHANGELOG.

* Python: Switch to new "run" method name. (#2890)

* Switch to `run` method.

* Add support for deprecated `run_agent`.

* Fix entity method name.

* Fix method name and improve tests.

* Update comment.

* Update Python CHANGELOG.

* [BREAKING] Python: Add factory pattern to handoff orchestration builder (#2844)

* WIP: Factory pattern to handoff

* Add factory pattern to concurrent orchestration builder; Next: tests and sample verification

* Add tests and improve comments

* Fix mypy

* Simplify handoff_simple.py

* Simplify handoff_autonoumous.py and bug fix

* Update readme

* Address Copilot comments

* Python: Flow custom kwargs to agents via Workflow SharedState (#2894)

* Flow custom kwargs to agents via SharedState

* Address Copilot feedback

* Improve sample typing

* Fix test

* Fix Pydantic error when using Literal type for tool params (#2893)

* Updated Ollama package version (#2920)

* Python: Azure AI Agent with Bing Grounding Citations Sample (#2892)

* bing grounding sample with citations

* small fix

* fix

* .NET: Make DelegatingAIAgent abstract (#2797)

* Initial plan

* Make DelegatingAIAgent abstract

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* Added additional arguments for Azure AI agent (#2922)

* Python: Correction of MCP image type conversion in  _mcp.py (#2901)

* Correction of MCP image type conversion in  _mcp.py

* Added a new overload to the init function of the DataContent() type of the Agent Framework, edited the test case to correctly test the usage of the data and uri fields while using DataContent()

* Fixed tests related to the changes of the DataContent type, added testing for both string and byte representations

* Pass kwargs into subworkflows (#2923)

* Python: Move ollama samples to samples getting started dir (#2921)

* Move ollama samples to samples getting started dir

* Address feedback

* Python: fix: correct BadRequestError when using Pydantic model in response_fo… (#1843)

* fix: correct BadRequestError when using Pydantic model in response_format

* Fix lint

---------

Co-authored-by: Evan Mattson <evan.mattson@microsoft.com>

* .NET: [Breaking] Delete display name property (#2758)

* delete the AIAgent.DisplayName property

* use agent name as a first value for activity display name

* Update dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/HandoffAgentExecutor.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Python: cleanup and refactoring of chat clients (#2937)

* refactoring and unifying naming schemes of internal methods of chat clients

* set tool_choice to auto

* fix for mypy

* added note on naming and fix #2951

* fix responses

* fixes in azure ai agents client

* Python: Workflow add option to visualize internal executors (#2917)

* Workflow add option to visualize internal executors

* Address Copilot comments

* Python: Fixes Run ID and Thread ID casing to align with AG-UI Typescript SDK (#2948)

* added camelCase input to run id and thread id aligning with @ag-ui/core

* fixed per copilot suggestions

* Python: Add workflow cancellation sample (#2732)

* Add workflow cancellation sample

Add sample demonstrating how to cancel a running workflow using asyncio
tasks. Shows both cancellation mid-execution and normal completion paths.
Useful for implementing timeouts, graceful shutdown, or A2A executors.

* update docstring

* .NET: Update Anthropic package to version 12.0.0 (#2914)

* Initial plan

* Update Anthropic package to version 12.0.0

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>

* Python: Add Azure Managed Redis Support with Credential Provider (#2887)

* azure redis support

* small fixes

* azure managed redis sample

* fixes

* Bump CommunityToolkit.Aspire.OllamaSharp from 13.0.0-beta.440 to 13.0.0 (#2856)

---
updated-dependencies:
- dependency-name: CommunityToolkit.Aspire.OllamaSharp
  dependency-version: 13.0.0
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump AWSSDK.Extensions.Bedrock.MEAI from 4.0.4.11 to 4.0.5 (#2853)

---
updated-dependencies:
- dependency-name: AWSSDK.Extensions.Bedrock.MEAI
  dependency-version: 4.0.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Mark Wallace <127216156+markwallace-microsoft@users.noreply.github.com>

* Bump Azure.AI.AgentServer.AgentFramework from 1.0.0-beta.4 to 1.0.0-beta.5 (#2854)

---
updated-dependencies:
- dependency-name: Azure.AI.AgentServer.AgentFramework
  dependency-version: 1.0.0-beta.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: Azure.AI.AgentServer.AgentFramework
  dependency-version: 1.0.0-beta.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>

* Python: Fix WorkflowAgent event handling and kwargs forwarding (#2946)

* Fix kwargs propagation through workflow.as_agent()

* Fix WorkflowAgent to respect AgentExecutor output_response setting

* .NET: Use GrpcEntityRunner instead of TaskEntityDispatcher (#2759)

* Use GrpcEntityRunner instead of TaskEntityDispatcher

* Pin to Durable worker 1.11.0

* Set the invocation result

* Update all Durable packages

* Update changelog, rename dispatcher to encondedEntityRequest

* Python: Bump Py version to 1.0.0b251218 for a release. Update CHANGELOG (#2968)

* Bump Py version to 1.0.0b251218 for a release. Update CHANGELOG

* update lock

* Fix formatting

* Fix ChatKit typing

* Python: Introducing Foundry Local Chat Clients (#2915)

* redo foundry local chat client

* fix mypy and spelling

* better docstring, updated sample

* fixed tests and added tests

* small sample update

* Updated package versions (#2978)

* Python: Added GitHub MCP sample with PAT (#2967)

* added github mcp sample with PAT

* addressed copilot fixes

* env fix

* Python: Preserve reasoning blocks with OpenRouter (#2950)

* Preserve reasoning blocks with OpenRouter

* Put encrypted reasoning in TextReasoningContent

* Remove unneccessary change

* Fix docs

* Support streaming

* Fix handling None in TextReasoningContent.text

* Python: Added response.created and response.in_progress event process to OpenAIBaseResponseClient (#2975)

* added response.created and response.in_progress to include response.id

* better doc string

* added tests for the new streaming event types

* Python: Introducing support for Bedrock-hosted models (Anthropic, Cohere, etc.) (#2610)

* Pushing the bedrock related changes to the new branch after addressing the review comments

* 2524 Addressed the second round review comments

* 2524 Addressed few more minor comments on the PR

* resolving the merge conflict

* 2524 resolved the uv.lock conflicts

* 2524 addressed more comments

* 2524 removed the print statement to fix the checks failure

* 2524 resolved the CI failure issues

* 2524 fixing the CI breaks

* 2524 Addressed the review comment

* 2524 resolved conflict

---------

Co-authored-by: Sunil Dutta <sunil.dutta@penske.com>
Co-authored-by: budgetboardingai <apurva.sharma31@gmail.com>

* .NET: [Durable Agents] Reliable streaming sample (#2942)

* .NET: [Durable Agents] Reliable streaming sample

* Add automated validation for new sample

* Address Copilot PR feedback

* Fix typo in README.md about agent definitions (#2634)

* Fix typo in README.md about agent definitions

* Update agent-samples/README.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Python: latency improvements (#3014)

* latency improvements

* fixed mypy, added coding standards and instructions

* slight logic improvement

* Python: Updated package versions (#3024)

* Updated package versions

* Updated changelog

* Python: add powerfx safe mode (#3028)

* add powerfx safe mode

* improved docstring and aligned env_file loading

* ensured test uses reset

* .NET: [Breaking] Introduce RunCoreAsync/RunCoreStreamingAsync delegation pattern in AIAgent (#2749)

* Initial plan

* Refactor AIAgent: Make RunAsync and RunStreamingAsync non-abstract, add RunCoreAsync and RunCoreStreamingAsync

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* Fix infinite recursion in test implementations

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* Make RunAsync and RunStreamingAsync non-virtual as requested

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* Fix DelegatingAIAgent subclasses to use RunCoreAsync/RunCoreStreamingAsync

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* Fix XML documentation references in AnonymousDelegatingAIAgent

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* Restore <see cref> tags with proper qualified signatures in AnonymousDelegatingAIAgent

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* Rollback unnecessary XML documentation changes in AnonymousDelegatingAIAgent

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* Remove pragma and update crefs to RunCoreAsync/RunCoreStreamingAsync

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* Fix EntityAgentWrapper to call base.RunCoreAsync/RunCoreStreamingAsync

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* fix compilation issues

* fix compilatio issue

* fix tests

* fix unit tests

* fix unit test

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>
Co-authored-by: SergeyMenshykh <sergemenshikh@gmail.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>

* Remove from feature branch

* Remove ollama changes

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Tao Chen <taochen@microsoft.com>
Co-authored-by: Kurt <65111699+q33566@users.noreply.github.com>
Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>
Co-authored-by: Korolev Dmitry <deagle.gross@gmail.com>
Co-authored-by: Mark Wallace <127216156+markwallace-microsoft@users.noreply.github.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>
Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>
Co-authored-by: Jose Luis Latorre Millas <joslat@gmail.com>
Co-authored-by: Jacob Alber <jaalber@microsoft.com>
Co-authored-by: Richard Ortega <richardjortega@gmail.com>
Co-authored-by: 刘邦学AI <lbbniu@gmail.com>
Co-authored-by: Stephen Toub <stoub@microsoft.com>
Co-authored-by: Nico Möller <nkm-moeller@mail.de>
Co-authored-by: Chris Gillum <cgillum@microsoft.com>
Co-authored-by: Giles Odigwe <79032838+giles17@users.noreply.github.com>
Co-authored-by: Phillip Hoff <phillip.hoff@gmail.com>
Co-authored-by: Ege Ozan Özyedek <36128615+egeozanozyedek@users.noreply.github.com>
Co-authored-by: samueljohnsiby <66901393+samueljohnsiby@users.noreply.github.com>
Co-authored-by: Evan Mattson <evan.mattson@microsoft.com>
Co-authored-by: Hao Luo <338265+howlowck@users.noreply.github.com>
Co-authored-by: Victor Dibia <chuvidi2003@gmail.com>
Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
Co-authored-by: Jacob Viau <javia@microsoft.com>
Co-authored-by: SuperKenVery <39673849+SuperKenVery@users.noreply.github.com>
Co-authored-by: Sunil Dutta <dutta.2003@gmail.com>
Co-authored-by: Sunil Dutta <sunil.dutta@penske.com>
Co-authored-by: budgetboardingai <apurva.sharma31@gmail.com>
Co-authored-by: Syrine Chelly <62653967+SyChell@users.noreply.github.com>
Co-authored-by: SergeyMenshykh <sergemenshikh@gmail.com>

* Python: Complete durableagent package   (#3058)

* Add worker and clients

* Clean code and refactor common code

* Implement sample

* Add sample

* Update readmes

* Fix tests

* Fix tests

* Update requirements

* Fix typo

* Address comments

* use response.text

* .NET: Python: Merge main into feature-durabletask-python branch  (#3160)

* Python: Add factory pattern to concurrent orchestration builder (#2738)

* Add factory pattern to concurrent orchestration builder

* Update readme

* Address AI comments

* Fix unit tests

* Fix import

* Prevent multiple calls to set participants or factories

* Add comments

* Mitigate warnings

* Fix mypy

* Address comments

* Address Copilot comments

* Fix tests

* Python: fix: GroupChat ManagerSelectionResponse JSON Schema for OpenAI Structured Outpu… (#2750)

* fix: ManagerSelectionResponse JSON Schema for OpenAI Structured Output Strict Mode

* refactor: install pre-commit then commit again

* Capture file IDs from code interpreter in streaming responses (#2741)

* .NET: [BREAKING] Prevent nulls in AIAgent property (#2719)

* prevent nulls in AIAgent property

* address feedback

* code ql sm04598 (#2723)

Co-authored-by: Mark Wallace <127216156+markwallace-microsoft@users.noreply.github.com>

* .NET: Add Conversation State Sample (Step05) (#2697)

* Initial plan

* Add Agent_OpenAI_Step05_Conversation sample for conversation state management

Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>

* Update Program.cs comment to accurately describe the sample

Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>

* Update the code to use the ConversationClient more in line with the samples in OpenAI

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Changing sample to use ChatClientAgent and conversationId in GetNewThread

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Bump AWSSDK.Extensions.Bedrock.MEAI from 4.0.4.7 to 4.0.4.11 (#2777)

---
updated-dependencies:
- dependency-name: AWSSDK.Extensions.Bedrock.MEAI
  dependency-version: 4.0.4.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump Azure.Identity from 1.17.0 to 1.17.1 (#2780)

---
updated-dependencies:
- dependency-name: Azure.Identity
  dependency-version: 1.17.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: Azure.Identity
  dependency-version: 1.17.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: Azure.Identity
  dependency-version: 1.17.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: Azure.Identity
  dependency-version: 1.17.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump Azure.AI.AgentServer.AgentFramework from 1.0.0-beta.4 to 1.0.0-beta.5 (#2778)

---
updated-dependencies:
- dependency-name: Azure.AI.AgentServer.AgentFramework
  dependency-version: 1.0.0-beta.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: Azure.AI.AgentServer.AgentFramework
  dependency-version: 1.0.0-beta.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: Azure.AI.AgentServer.AgentFramework
  dependency-version: 1.0.0-beta.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Python: added more complete parsing for mcp tool arguments (#2756)

* added more complete parsing for mcp tool arguments

* fixed mypy

* added nonlocal model counter, and some fixes

* fixes in naming logic

* extracted json parsing function, added parametrized test and checked coverage

* Python: Updated package versions (#2784)

* Updated package versions

* Small fix

* Bump actions/checkout from 5 to 6 (#2404)

Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>

* .NET: adds support for labels in edges,  fixes rendering of labels in dot a… (#1507)

* adds support for labels in edges,  fixes rendering of labels in dot and mermaid, adds rendering of labels in edges

* Update dotnet/src/Microsoft.Agents.AI.Workflows/Visualization/WorkflowVisualizer.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* escaping edge labels, adding tests for labels containing strange characters that would break the diagram and enabling the previous signature so the API has backwards compatibility.

* Unify label in EdgeData

* Edge API adjustments, removed useless "sanitizer"

* fixed test

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Jacob Alber <jaalber@microsoft.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>

* Python: Added custom args and thread object to ai_function kwargs (#2769)

* Added an example of using kwargs in ai_function

* Added thread object to ai_function kwargs

* Updated docs

* Small fix

* Added thread parameter filtering

* Fix WorkflowAgent to include thread convo history. Enable checkpointing. (#2774)

* Update OpenAIResponses.yaml to match AgentSchema (#2598)

1. Update `connection` child types --  `kind: ApiKey` to `kind: key` otherwise schema will fail: https://microsoft.github.io/AgentSchema/reference/apikeyconnection/

2.  Update `outputSchema`'s `PropertySchema` to be `kind` instead of `type` otherwise schema will fail: https://microsoft.github.io/AgentSchema/reference/propertyschema/

* Python: Remove warnings from workflow builder on not using factories (#2808)

* Revert concurrent

* Fix comments

* Python: Filter framework kwargs from MCP tool invocations (#2870)

* Filter framework kwargs from MCP tool invocations

* Fixes

* Python: Fix WorkflowAgent to emit yield_output as agent response (#2866)

* Fix WorkflowAgent to emit yield_output as agent response

* use raw_representation

* Raw representation handling

* Python: Use agent description in HandoffBuilder auto-generated tools (#2713) (#2714)

## Summary
Enhanced `HandoffBuilder._apply_auto_tools` to use the target agent's
description when creating handoff tools, providing more informative tool
descriptions for LLMs.

## Changes
- Modified `_apply_auto_tools` to extract `description` from
  `AgentExecutor._agent` when available
- Updated iteration to use `.items()` for more efficient dict traversal
- Handoff tools now use agent descriptions instead of generic placeholders

## Example
Before: "Handoff to the refund_agent agent."
After: "You handle refund requests. Ask for order details and process refunds."

## Testing
- All handoff tests pass (20/20)
- No breaking changes to existing API

Fixes #2713

Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>

* Python: [BREAKING] Observability updates (#2782)

* fixes Python: Add env_file_path parameter to setup_observability() similar to AzureOpenAIChatClient
Fixes #2186

* WIP on updates using configure_azure_monitor

* improved setup and clarity

* fixed root .env.example

* revert changes

* updated files

* updated sample

* updated zero code

* test fixes and fixed links

* fix devui

* removed planning docs

* added enable method and updated readme and samples

* clarified docstring

* add return annotation

* updated naming

* update capatilized version

* updated readme and some fixes

* updated decorator name inline with the rest

* feedback from comments addressed

* Python: Fix middleware terminate flag to exit function calling loop immediately (#2868)

* Fix middleware terminate flag to exit function calling loop immediately

* Eliminating duck typing

* Improve function exec result handling

* Fix race condition

* Fix mypy issues

* Python: Fix context duplication in handoff workflows when restoring from checkpoint (#2867)

* Fix context duplication in handoff workflows when restoring from checkpoint

* Address Copilot PR review

* .NET: Update to latest Azure.AI.*, OpenAI, and M.E.AI* (#2850)

* Update to latest Azure.AI.*, OpenAI, and M.E.AI*

Absorb breaking changes in Responses surface area

* Update dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Utilities/ChatClientExtensions.cs

* Update dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Utilities/ChatClientExtensions.cs

* Update dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Utilities/ChatClientExtensions.cs

* Update dotnet/samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step04_CreateFromOpenAIResponseClient/Program.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Using patch to remove the model is necessary, updated the response client to actually use the the ForAgent

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>

* Bump actions/download-artifact from 6 to 7 (#2862)

Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 6 to 7.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump actions/cache from 4 to 5 (#2861)

Bumps [actions/cache](https://github.com/actions/cache) from 4 to 5.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump actions/upload-artifact from 5 to 6 (#2860)

Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 5 to 6.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Python : Ollama Connector for Agent Framework (#1104)

* Initial Commit for Olama Connector

* Added Olama Sample

* Add Sample & Fixed Open Telemetry

* Fixed Spelling from Olama to Ollama

* remove"opentelemetry-semantic-conventions-ai ~=0.4.13" since its handled in a different pr

* Added Tool Calling

* Finalizing test cases

* Adjust samples to be more reliable

* Update python/packages/ollama/agent_framework_ollama/_chat_client.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update python/packages/ollama/pyproject.toml

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update python/packages/ollama/tests/test_ollama_chat_client.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update python/packages/ollama/agent_framework_ollama/_chat_client.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Improved Docstrings & Sample

* Update python/packages/ollama/agent_framework_ollama/_chat_client.py

Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>

* Integrate PR Feedback
- Divided Streaming and Non-Streaming into independent Methods
- Catch Ollama Validation Error
- Add OTEL Provider Name
- Checked Ollama Messages
- Add Usage Statistics

* Revert setting, so it can be none

* Validate Message formatting between AF and Ollama

* Catch Ollama Error and raise a ServiceResponse Error

* Fix mypy error

* remove .vscode comma

* Add Reasoning support & adjust to new structure

* Add Ollama Multimodality and Reasoning

* Add test cases for reasoning

* Add Tests for Error Handling in Ollama Client

* Update python/samples/getting_started/multimodal_input/ollama_chat_multimodal.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Integrated Copilot Feedback

* Implement first PR Feedback

* Adjust Readme files for examples

* Adjust argument passing via additional chat options

* Implemented PR Feedback

* Removing Ollama Package from Core and moving samples

* Fix Link & Adding Samples to Main Sample Readme

* Fixing Links in Readme

* Moved Multimodal and Chat Example

* Fixed Link in ChatClient to Ollama

* Fix AgentFramework Links in Ollama Project

* Fix observability breaking change

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>

* Skip failing IT (#2904)

* .NET: Cosmos DB UT Fast Skip (For Non-Configured Local envs) (#2906)

* Cosmos DB UT Fast Skip (Non-Configured Local envs) + Long running UT skip in pipeline when no CosmosDB changes happened

* Force a CosmosDB source code change to trigger the pipeline

* Address possible string boolean mismatch

* Add debug

* Enabling emulator always when running IT

* .NET: Add TTLs to durable agent sessions (#2679)

* .NET: Add TTLs to durable agent sessions

* Remove unnecessary async

* PR feedback: clarify UTC

* PR feedback: limit minimum signal delay to <= 5 minutes

* PR feedback: Fix TTL disablement

* Linter: use auto-property

* Fix build break from OpenAI SDK change

* Updated CHANGELOG.md

* PR feedback

* Reduce default TTL to 14 days to work around DTS bug

* Python:  Update Mem0Provider to use v2 search API `filters` parameter (#2766)

* short fix to move id parameters to filters object

* added tests

* small fix

* mem0 dependency update

* Updated package versions (#2913)

* .NET: Switch to new "Run" method name. (#2843)

* Switch to new "RunAgent" method name.

* Try to disable false positive naming warning.

* Add comment about disabled warnings.

* Rename `RunAgent` to just `Run`.

* Update CHANGELOG.

* Python: Switch to new "run" method name. (#2890)

* Switch to `run` method.

* Add support for deprecated `run_agent`.

* Fix entity method name.

* Fix method name and improve tests.

* Update comment.

* Update Python CHANGELOG.

* [BREAKING] Python: Add factory pattern to handoff orchestration builder (#2844)

* WIP: Factory pattern to handoff

* Add factory pattern to concurrent orchestration builder; Next: tests and sample verification

* Add tests and improve comments

* Fix mypy

* Simplify handoff_simple.py

* Simplify handoff_autonoumous.py and bug fix

* Update readme

* Address Copilot comments

* Python: Flow custom kwargs to agents via Workflow SharedState (#2894)

* Flow custom kwargs to agents via SharedState

* Address Copilot feedback

* Improve sample typing

* Fix test

* Fix Pydantic error when using Literal type for tool params (#2893)

* Updated Ollama package version (#2920)

* Python: Azure AI Agent with Bing Grounding Citations Sample (#2892)

* bing grounding sample with citations

* small fix

* fix

* .NET: Make DelegatingAIAgent abstract (#2797)

* Initial plan

* Make DelegatingAIAgent abstract

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* Added additional arguments for Azure AI agent (#2922)

* Python: Correction of MCP image type conversion in  _mcp.py (#2901)

* Correction of MCP image type conversion in  _mcp.py

* Added a new overload to the init function of the DataContent() type of the Agent Framework, edited the test case to correctly test the usage of the data and uri fields while using DataContent()

* Fixed tests related to the changes of the DataContent type, added testing for both string and byte representations

* Pass kwargs into subworkflows (#2923)

* Python: Move ollama samples to samples getting started dir (#2921)

* Move ollama samples to samples getting started dir

* Address feedback

* Python: fix: correct BadRequestError when using Pydantic model in response_fo… (#1843)

* fix: correct BadRequestError when using Pydantic model in response_format

* Fix lint

---------

Co-authored-by: Evan Mattson <evan.mattson@microsoft.com>

* .NET: [Breaking] Delete display name property (#2758)

* delete the AIAgent.DisplayName property

* use agent name as a first value for activity display name

* Update dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/HandoffAgentExecutor.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Python: cleanup and refactoring of chat clients (#2937)

* refactoring and unifying naming schemes of internal methods of chat clients

* set tool_choice to auto

* fix for mypy

* added note on naming and fix #2951

* fix responses

* fixes in azure ai agents client

* Python: Workflow add option to visualize internal executors (#2917)

* Workflow add option to visualize internal executors

* Address Copilot comments

* Python: Fixes Run ID and Thread ID casing to align with AG-UI Typescript SDK (#2948)

* added camelCase input to run id and thread id aligning with @ag-ui/core

* fixed per copilot suggestions

* Python: Add workflow cancellation sample (#2732)

* Add workflow cancellation sample

Add sample demonstrating how to cancel a running workflow using asyncio
tasks. Shows both cancellation mid-execution and normal completion paths.
Useful for implementing timeouts, graceful shutdown, or A2A executors.

* update docstring

* .NET: Update Anthropic package to version 12.0.0 (#2914)

* Initial plan

* Update Anthropic package to version 12.0.0

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>

* Python: Add Azure Managed Redis Support with Credential Provider (#2887)

* azure redis support

* small fixes

* azure managed redis sample

* fixes

* Bump CommunityToolkit.Aspire.OllamaSharp from 13.0.0-beta.440 to 13.0.0 (#2856)

---
updated-dependencies:
- dependency-name: CommunityToolkit.Aspire.OllamaSharp
  dependency-version: 13.0.0
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump AWSSDK.Extensions.Bedrock.MEAI from 4.0.4.11 to 4.0.5 (#2853)

---
updated-dependencies:
- dependency-name: AWSSDK.Extensions.Bedrock.MEAI
  dependency-version: 4.0.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Mark Wallace <127216156+markwallace-microsoft@users.noreply.github.com>

* Bump Azure.AI.AgentServer.AgentFramework from 1.0.0-beta.4 to 1.0.0-beta.5 (#2854)

---
updated-dependencies:
- dependency-name: Azure.AI.AgentServer.AgentFramework
  dependency-version: 1.0.0-beta.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: Azure.AI.AgentServer.AgentFramework
  dependency-version: 1.0.0-beta.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>

* Python: Fix WorkflowAgent event handling and kwargs forwarding (#2946)

* Fix kwargs propagation through workflow.as_agent()

* Fix WorkflowAgent to respect AgentExecutor output_response setting

* .NET: Use GrpcEntityRunner instead of TaskEntityDispatcher (#2759)

* Use GrpcEntityRunner instead of TaskEntityDispatcher

* Pin to Durable worker 1.11.0

* Set the invocation result

* Update all Durable packages

* Update changelog, rename dispatcher to encondedEntityRequest

* Python: Bump Py version to 1.0.0b251218 for a release. Update CHANGELOG (#2968)

* Bump Py version to 1.0.0b251218 for a release. Update CHANGELOG

* update lock

* Fix formatting

* Fix ChatKit typing

* Python: Introducing Foundry Local Chat Clients (#2915)

* redo foundry local chat client

* fix mypy and spelling

* better docstring, updated sample

* fixed tests and added tests

* small sample update

* Updated package versions (#2978)

* Python: Added GitHub MCP sample with PAT (#2967)

* added github mcp sample with PAT

* addressed copilot fixes

* env fix

* Python: Preserve reasoning blocks with OpenRouter (#2950)

* Preserve reasoning blocks with OpenRouter

* Put encrypted reasoning in TextReasoningContent

* Remove unneccessary change

* Fix docs

* Support streaming

* Fix handling None in TextReasoningContent.text

* Python: Added response.created and response.in_progress event process to OpenAIBaseResponseClient (#2975)

* added response.created and response.in_progress to include response.id

* better doc string

* added tests for the new streaming event types

* Python: Introducing support for Bedrock-hosted models (Anthropic, Cohere, etc.) (#2610)

* Pushing the bedrock related changes to the new branch after addressing the review comments

* 2524 Addressed the second round review comments

* 2524 Addressed few more minor comments on the PR

* resolving the merge conflict

* 2524 resolved the uv.lock conflicts

* 2524 addressed more comments

* 2524 removed the print statement to fix the checks failure

* 2524 resolved the CI failure issues

* 2524 fixing the CI breaks

* 2524 Addressed the review comment

* 2524 resolved conflict

---------

Co-authored-by: Sunil Dutta <sunil.dutta@penske.com>
Co-authored-by: budgetboardingai <apurva.sharma31@gmail.com>

* .NET: [Durable Agents] Reliable streaming sample (#2942)

* .NET: [Durable Agents] Reliable streaming sample

* Add automated validation for new sample

* Address Copilot PR feedback

* Fix typo in README.md about agent definitions (#2634)

* Fix typo in README.md about agent definitions

* Update agent-samples/README.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Python: latency improvements (#3014)

* latency improvements

* fixed mypy, added coding standards and instructions

* slight logic improvement

* Python: Updated package versions (#3024)

* Updated package versions

* Updated changelog

* Python: add powerfx safe mode (#3028)

* add powerfx safe mode

* improved docstring and aligned env_file loading

* ensured test uses reset

* .NET: [Breaking] Introduce RunCoreAsync/RunCoreStreamingAsync delegation pattern in AIAgent (#2749)

* Initial plan

* Refactor AIAgent: Make RunAsync and RunStreamingAsync non-abstract, add RunCoreAsync and RunCoreStreamingAsync

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* Fix infinite recursion in test implementations

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* Make RunAsync and RunStreamingAsync non-virtual as requested

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* Fix DelegatingAIAgent subclasses to use RunCoreAsync/RunCoreStreamingAsync

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* Fix XML documentation references in AnonymousDelegatingAIAgent

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* Restore <see cref> tags with proper qualified signatures in AnonymousDelegatingAIAgent

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* Rollback unnecessary XML documentation changes in AnonymousDelegatingAIAgent

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* Remove pragma and update crefs to RunCoreAsync/RunCoreStreamingAsync

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* Fix EntityAgentWrapper to call base.RunCoreAsync/RunCoreStreamingAsync

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* fix compilation issues

* fix compilatio issue

* fix tests

* fix unit tests

* fix unit test

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>
Co-authored-by: SergeyMenshykh <sergemenshikh@gmail.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>

* add issue template and additional labeling (#3006)

* fix and extra int test (#3037)

* .NET: [BREAKING] Refactor ChatMessageStore methods to be similar to AIContextProvider and add filtering support (#2604)

* Refactor ChatMessageStore methods to be similar to AIContextProvider

* Fix file encoding

* Ensure that AIContextProvider messages area also persisted.

* Update formatting and seal context classes

* Improve formatting

* Remove optional messages from constructor and add unit test

* Add ChatMessageStore filtering via a decorator

* Update sample and cosmos message store to store AIContextProvider messages in right order. Fix unit tests.

* Update Workflowmessage store to use aicontext provider messages.

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* Improve xml docs messaging

* Address code review comments.

* Also notify message store on failure

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* [BREAKING] Remove unused AgentThreadMetadata (#3067)

* Remove unused AgentThreadMetadata

* Update DurableTask Changelog

* Python: Fix AzureAIClient failure when conversation history contains assistant messages (#3076)

* Fix AzureAIClient failure when conversation history contains assistant messages

* Address PR review feedback: improve docstring and test assertions

* Remove redundant cast

* Fix: Update OTLP exporter protocol conditions (#3070)

* Python: Fix ExecutorInvokedEvent and ExecutorCompletedEvent observability data (#3090)

* Fix ExecutorInvokedEvent.data mutation bug

* Fix bug related to not yielding output type

* .NET: Seal ChatClientAgentThread (#2842)

* Initial plan

* Seal ChatClientAgentThread class

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* Fix broken strands urls. (#3102)

* Fix broken strands urls.

* Fix typos

* .NET: Fix message ordering inconsistency when using AIContextProvider (#2659)

* Initial plan

* Fix message ordering inconsistency when using AIContextProvider

Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>

* Revert to original message ordering: Input, AIContextProvider, Response

Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>

* Reorder messages to ChatClient to match MessageStore order: Existing, Input, AIContextProvider

Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>

* Remove redundant test methods as existing tests already verify the behavior

Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>
Co-authored-by: Mark Wallace <127216156+markwallace-microsoft@users.noreply.github.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>

* fix: tool_choice parameter not being honored when passed to agent.run() (#3095)

* sharepoint sample fix (#3108)

* Bump versions to 1.0.0b260106 for a release. Update CHANGELOG.md (#3109)

* Bump Bedrock version to latest (#3110)

* Python: Fix MCP tool result serialization for list[TextContent] (#2523)

* Fix MCP tool result serialization for list[TextContent]

When MCP tools return results containing list[TextContent], they were
incorrectly serialized to object repr strings like:
'[<agent_framework._types.TextContent object at 0x...>]'

This fix properly extracts text content from list items by:
1. Checking if items have a 'text' attribute (TextContent)
2. Using model_dump() for items that support it
3. Falling back to str() for other types
4. Joining single items as plain text, multiple items as JSON array

Fixes #2509

* Address PR review feedback for MCP tool result serialization

- Extract serialize_content_result() to shared _utils.py
- Fix logic: use texts[0] instead of join for single item
- Add type annotation: texts: list[str] = []
- Return empty string for empty list instead of '[]'
- Move import json to file top level
- Add comprehensive unit tests for serialization

* Address PR review feedback: fix type checking and double serialization

- Add isinstance(item.text, str) check to ensure text attribute is a string
- Fix double-serialization issue by keeping model_dump results as dicts
  until final json.dumps (removes escaped JSON strings in arrays)
- Improve docstring with detailed return value documentation
- Add test for non-string text attribute handling
- Add tests for list type tool results in _events.py path

* Simplify PR: minimal changes to fix MCP tool result serialization

Addresses reviewer feedback about excessive refactoring:
- Reset _events.py to original structure
- Only add import and use serialize_content_result in one location
- All review comments addressed in serialize_content_result():
  - Added isinstance(item.text, str) check
  - Use model_dump(mode="json") to avoid double-serialization
  - Improved docstring with explicit return value documentation
  - Empty list returns "" instead of "[]"

* Refactor: Move MCP TextContent serialization to core prepare_function_call_results

Per reviewer feedback, moved the TextContent serialization logic from
ag-ui's serialize_content_result to the core package's
prepare_function_call_results function.

Changes:
- Added handling for objects with 'text' attribute (like MCP TextContent)
  in _prepare_function_call_results_as_dumpable
- Removed serialize_content_result from ag-ui/_utils.py
- Updated _events.py and _message_adapters.py to use
  prepare_function_call_results from core package
- Updated tests to match the core function's behavior

* Fix failing tests for prepare_function_call_results behavior

- test_tool_result_with_none: Update expected value to 'null' (JSON serialization of None)
- test_tool_result_with_model_dump_objects: Use Pydantic BaseModel instead of plain class

* Fix B903 linter error: Convert MockTextContent to dataclass

The ruff linter was reporting B903 (class could be dataclass or namedtuple)
for the MockTextContent test helper classes. This commit converts them to
dataclasses to satisfy the linter check.

* Python: Improve DevUI, add Context Inspector view as new tab under traces (#2742)

* Improve DevUI, add Context Inspector view as new tab under traces

* fix mypy errors

* fix: Handle stale MCP connections in DevUI executor

MCP tools can become stale when HTTP streaming responses end - the underlying
stdio streams close but `is_connected` remains True. This causes subsequent
requests to fail with `ClosedResourceError`.

Add `_ensure_mcp_connections()` to detect and reconnect stale MCP tools before
agent execution. This is a workaround for an upstream Agent Framework issue
where connection state isn't properly tracked.

Fixes MCP tools failing on second HTTP request in DevUI.

fixes  #1476 #1515 #2865

* fix #1572 report import dependency errors more clearly

* Ensure there is streaming toggle where users can select streaming vs non streaming mode in devui . Fixes .NET: [Python] DevUI tool call rendering in non-streaming mode?

* remove unused dead code

* improve ux - workflows with agents show a chat component in execution timelien, also ensure magentic final output shows correctly

* update ui build

* update devui to use instrumentation instead of tracing, other instrumentation and type/instance check fixes

* .NET: Seal factory contexts and add non JSO deserialize overloads (#3066)

* Seal factory contexts and add non JSO deserialize overloads

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Enable blank issues in issue template configuration

Need to re-enable creating blank issues

* updated templates (#3106)

* updated templates

* enabled blank and fixed triage

* made language optional and moved to the bottom for features

* Python: Streaming sample for azurefunctions (#3057)

* Streaming sample for azurefunctions

* Fixed links and sample name

* Addressed feedback

* Addressed feedback

* Fixed integration tests

* Updated test

* Python: fix(azure-ai): Fix response_format handling for structured outputs (#3114)

* fix(azure-ai): read response_format from chat_options instead of run_options

* refactor: use explicit None checks for response_format

* Fix mypy error

* Mypy fix

* Python: Bump python version to 1.0.0b260107 for a release (#3128)

* Bump python version to 1.0.0b260107 for a release

* Update changelog

* Make A2AAgent public, so that it's concrete implementation methods can be used. (#3119)

* .NET: Map additional props <-> A2A metadata (#3137)

* map additional props from agent run options to a2a request metadata

* small touches

* add unit tests for new extension methods

* Sort using

* add unit test

* add additiona unit tests

* special case json element to avoid unnecessary serialization

* Python: Fix Anthropic streaming response bugs (#3141)

* test commit identity

* fix(anthropic): fix raw_representation and finish_reason in streaming

* lint fix

* Bump AWSSDK.Extensions.Bedrock.MEAI from 4.0.5 to 4.0.5.1 (#2994)

---
updated-dependencies:
- dependency-name: AWSSDK.Extensions.Bedrock.MEAI
  dependency-version: 4.0.5.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>

* Bump Anthropic from 12.0.0 to 12.0.1 (#2993)

---
updated-dependencies:
- dependency-name: Anthropic
  dependency-version: 12.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>

* .NET: [Breaking] Prevent loss of input messages & streamed updates when resuming streaming (#2748)

* save input messages and stream updates to the continuation token to be able to use them in the last successful stream resumption call.

* Update dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentContinuationToken.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentContinuationToken.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_BackgroundResponsesTests.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentContinuationToken.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentContinuationToken.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix typo

* init continuation token from chat response

* remove unnecessary types for source generation

* remove check for continuation token passed at initial run

* remove check for continuation token pass at initial run

* centralize continuation token parsing

* update xml comments

* use readonly collection instead of enumerable

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* .NET: fix: Expose WorkflowErrorEvent as ErrorContent (#2762)

* fix: Expose WorkflowErrorEvent as ErrorContent

When hosted using .AsAgent(), Workflows were not exposing inner errors coming as Exceptions (through the WorkflowErrorEvent)

The fix is to convert their message to an ErrorContent on the way out, rather than rely on the default "empty update" to collect the raw event.

* feat: Add a way to show/suppress exception information

* Bump Microsoft.Agents.AI.Workflows from 1.0.0-preview.251125.1 to 1.0.0-preview.251219.1 (#2997)

---
updated-dependencies:
- dependency-name: Microsoft.Agents.AI.Workflows
  dependency-version: 1.0.0-preview.251219.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>
Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* .NET: Add Run overloads to expose ChatClientAgentRunOptions in IntelliSense (#3115)

* Initial plan

* Add ChatClientAgentExtensions for improved discoverability of ChatClientAgentRunOptions

Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>

* Address code review feedback - use collection expression syntax

Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>

* Apply suggestion from @westey-m

* Fix issues with Copilot implementation

* Add additional tests for structured output overloads.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>

* Python: Add tool call/result content types and update connectors and samples (#2971)

* Add new AI content types and image tool support

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Add Python content types for tool calls/results and image generation tool support

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Address review feedback for tool content and samples

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Tighten image generation typing and sample tools list

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Align image generation output typing

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Handle MCP naming, image options mapping, and connector tool content

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Allow MCP call in function approval request

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Remove raw image_generation tool remapping

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Restore Anthropic tool_use to function calls unless code execution

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Fix lint issues for hosted file docstring and MCP parsing

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Import ChatResponse types in Anthropic client

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Fix Anthropics citation type imports and MCP typing for handoff/tools

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Skip lightning tests without agentlightning and fix function call import

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* fix lint on lab package

* rebuilt anthropic parsing

* redid anthropic parsing

* typo

* updated parsing and added missing docstrings

* fix tests

* mypy fixes

* second mypy fix

* add new class to other samples

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>
Co-authored-by: eavanvalkenburg <github@vanvalkenburg.eu>

* Bump Google.GenAI from 0.6.0 to 0.9.0 (#2995)

---
updated-dependencies:
- dependency-name: Google.GenAI
  dependency-version: 0.9.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>

* Bump js-yaml from 4.1.0 to 4.1.1 in /python/packages/devui/frontend (#3123)

Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.0 to 4.1.1.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.0...4.1.1)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 4.1.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Updated package versions (#3144)

* .NET: Bump Microsoft.Agents.AI.OpenAI and Microsoft.Extensions.AI.OpenAI (#2996)

* Bump Microsoft.Agents.AI.OpenAI and Microsoft.Extensions.AI.OpenAI

Bumps Microsoft.Agents.AI.OpenAI from 1.0.0-preview.251125.1 to 1.0.0-preview.251219.1
Bumps Microsoft.Extensions.AI.OpenAI from 10.1.0-preview.1.25608.1 to 10.1.1-preview.1.25612.2

---
updated-dependencies:
- dependency-name: Microsoft.Agents.AI.OpenAI
  dependency-version: 1.0.0-preview.251219.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: Microsoft.Extensions.AI.OpenAI
  dependency-version: 10.1.1-preview.1.25612.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: Microsoft.Agents.AI.OpenAI
  dependency-version: 1.0.0-preview.251219.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: Microsoft.Extensions.AI.OpenAI
  dependency-version: 10.1.1-preview.1.25612.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* Fixed samples

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>
Co-authored-by: Mark Wallace <127216156+markwallace-microsoft@users.noreply.github.com>
Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com>

* Python: fix(ag-ui): Execute tools with approval_mode, fix shared state, code cleanup  (#3079)

* fix(ag-ui): execute tools after approval in human-in-the-loop flow

* Fix shared state bug

* Bug fix finalized

* Refactoring to clean up code

* Code cleanup

* More fixes

* More code cleanup

* Add version detection in __init__.py to ruff ignore list

* Track agent name with updates for workflow agent (#3146)

* Python: Fix AzureAIClient tool call bug for AG-UI use (#3148)

* Fiz AzureAIClient tool call bug

* Address copilot feedback

* Revert to match main

* revert file to main

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Tao Chen <taochen@microsoft.com>
Co-authored-by: Kurt <65111699+q33566@users.noreply.github.com>
Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>
Co-authored-by: Korolev Dmitry <deagle.gross@gmail.com>
Co-authored-by: Mark Wallace <127216156+markwallace-microsoft@users.noreply.github.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>
Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>
Co-authored-by: Jose Luis Latorre Millas <joslat@gmail.com>
Co-authored-by: Jacob Alber <jaalber@microsoft.com>
Co-authored-by: Richard Ortega <richardjortega@gmail.com>
Co-authored-by: 刘邦学AI <lbbniu@gmail.com>
Co-authored-by: Stephen Toub <stoub@microsoft.com>
Co-authored-by: Nico Möller <nkm-moeller@mail.de>
Co-authored-by: Chris Gillum <cgillum@microsoft.com>
Co-authored-by: Giles Odigwe <79032838+giles17@users.noreply.github.com>
Co-authored-by: Phillip Hoff <phillip.hoff@gmail.com>
Co-authored-by: Ege Ozan Özyedek <36128615+egeozanozyedek@users.noreply.github.com>
Co-authored-by: samueljohnsiby <66901393+samueljohnsiby@users.noreply.github.com>
Co-authored-by: Evan Mattson <evan.mattson@microsoft.com>
Co-authored-by: Hao Luo <338265+howlowck@users.noreply.github.com>
Co-authored-by: Victor Dibia <chuvidi2003@gmail.com>
Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
Co-authored-by: Jacob Viau <javia@microsoft.com>
Co-authored-by: SuperKenVery <39673849+SuperKenVery@users.noreply.github.com>
Co-authored-by: Sunil Dutta <dutta.2003@gmail.com>
Co-authored-by: Sunil Dutta <sunil.dutta@penske.com>
Co-authored-by: budgetboardingai <apurva.sharma31@gmail.com>
Co-authored-by: Syrine Chelly <62653967+SyChell@users.noreply.github.com>
Co-authored-by: SergeyMenshykh <sergemenshikh@gmail.com>
Co-authored-by: westey <164392973+westey-m@users.noreply.github.com>
Co-authored-by: takanori-terai <123897708+takanori-terai@users.noreply.github.com>
Co-authored-by: claude89757 <138977524+claude89757@users.noreply.github.com>
Co-authored-by: Gavin Aguiar <80794152+gavin-aguiar@users.noreply.github.com>
Co-authored-by: Sukeesh <vsukeeshbabu@gmail.com>
Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>
Co-authored-by: eavanvalkenburg <github@vanvalkenburg.eu>

* Python: Add Durabletask samples and minor fixes (#3157)

* Add samples and minor fixes

* Add redis sample and wait-for-completion

* Add wait-for-completion support

* ADd missing docs

* Python: Merge `main` into `feature-durabletask-python` branch  (#3261)

* Python: Add factory pattern to concurrent orchestration builder (#2738)

* Add factory pattern to concurrent orchestration builder

* Update readme

* Address AI comments

* Fix unit tests

* Fix import

* Prevent multiple calls to set participants or factories

* Add comments

* Mitigate warnings

* Fix mypy

* Address comments

* Address Copilot comments

* Fix tests

* Python: fix: GroupChat ManagerSelectionResponse JSON Schema for OpenAI Structured Outpu… (#2750)

* fix: ManagerSelectionResponse JSON Schema for OpenAI Structured Output Strict Mode

* refactor: install pre-commit then commit again

* Capture file IDs from code interpreter in streaming responses (#2741)

* .NET: [BREAKING] Prevent nulls in AIAgent property (#2719)

* prevent nulls in AIAgent property

* address feedback

* code ql sm04598 (#2723)

Co-authored-by: Mark Wallace <127216156+markwallace-microsoft@users.noreply.github.com>

* .NET: Add Conversation State Sample (Step05) (#2697)

* Initial plan

* Add Agent_OpenAI_Step05_Conversation sample for conversation state management

Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>

* Update Program.cs comment to accurately describe the sample

Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>

* Update the code to use the ConversationClient more in line with the samples in OpenAI

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Changing sample to use ChatClientAgent and conversationId in GetNewThread

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Bump AWSSDK.Extensions.Bedrock.MEAI from 4.0.4.7 to 4.0.4.11 (#2777)

---
updated-dependencies:
- dependency-name: AWSSDK.Extensions.Bedrock.MEAI
  dependency-version: 4.0.4.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump Azure.Identity from 1.17.0 to 1.17.1 (#2780)

---
updated-dependencies:
- dependency-name: Azure.Identity
  dependency-version: 1.17.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: Azure.Identity
  dependency-version: 1.17.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: Azure.Identity
  dependency-version: 1.17.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: Azure.Identity
  dependency-version: 1.17.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump Azure.AI.AgentServer.AgentFramework from 1.0.0-beta.4 to 1.0.0-beta.5 (#2778)

---
updated-dependencies:
- dependency-name: Azure.AI.AgentServer.AgentFramework
  dependency-version: 1.0.0-beta.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: Azure.AI.AgentServer.AgentFramework
  dependency-version: 1.0.0-beta.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: Azure.AI.AgentServer.AgentFramework
  dependency-version: 1.0.0-beta.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Python: added more complete parsing for mcp tool arguments (#2756)

* added more complete parsing for mcp tool arguments

* fixed mypy

* added nonlocal model counter, and some fixes

* fixes in naming logic

* extracted json parsing function, added parametrized test and checked coverage

* Python: Updated package versions (#2784)

* Updated package versions

* Small fix

* Bump actions/checkout from 5 to 6 (#2404)

Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>

* .NET: adds support for labels in edges,  fixes rendering of labels in dot a… (#1507)

* adds support for labels in edges,  fixes rendering of labels in dot and mermaid, adds rendering of labels in edges

* Update dotnet/src/Microsoft.Agents.AI.Workflows/Visualization/WorkflowVisualizer.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* escaping edge labels, adding tests for labels containing strange characters that would break the diagram and enabling the previous signature so the API has backwards compatibility.

* Unify label in EdgeData

* Edge API adjustments, removed useless "sanitizer"

* fixed test

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Jacob Alber <jaalber@microsoft.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>

* Python: Added custom args and thread object to ai_function kwargs (#2769)

* Added an example of using kwargs in ai_function

* Added thread object to ai_function kwargs

* Updated docs

* Small fix

* Added thread parameter filtering

* Fix WorkflowAgent to include thread convo history. Enable checkpointing. (#2774)

* Update OpenAIResponses.yaml to match AgentSchema (#2598)

1. Update `connection` child types --  `kind: ApiKey` to `kind: key` otherwise schema will fail: https://microsoft.github.io/AgentSchema/reference/apikeyconnection/

2.  Update `outputSchema`'s `PropertySchema` to be `kind` instead of `type` otherwise schema will fail: https://microsoft.github.io/AgentSchema/reference/propertyschema/

* Python: Remove warnings from workflow builder on not using factories (#2808)

* Revert concurrent

* Fix comments

* Python: Filter framework kwargs from MCP tool invocations (#2870)

* Filter framework kwargs from MCP tool invocations

* Fixes

* Python: Fix WorkflowAgent to emit yield_output as agent response (#2866)

* Fix WorkflowAgent to emit yield_output as agent response

* use raw_representation

* Raw representation handling

* Python: Use agent description in HandoffBuilder auto-generated tools (#2713) (#2714)

## Summary
Enhanced `HandoffBuilder._apply_auto_tools` to use the target agent's
description when creating handoff tools, providing more informative tool
descriptions for LLMs.

## Changes
- Modified `_apply_auto_tools` to extract `description` from
  `AgentExecutor._agent` when available
- Updated iteration to use `.items()` for more efficient dict traversal
- Handoff tools now use agent descriptions instead of generic placeholders

## Example
Before: "Handoff to the refund_agent agent."
After: "You handle refund requests. Ask for order details and process refunds."

## Testing
- All handoff tests pass (20/20)
- No breaking changes to existing API

Fixes #2713

Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>

* Python: [BREAKING] Observability updates (#2782)

* fixes Python: Add env_file_path parameter to setup_observability() similar to AzureOpenAIChatClient
Fixes #2186

* WIP on updates using configure_azure_monitor

* improved setup and clarity

* fixed root .env.example

* revert changes

* updated files

* updated sample

* updated zero code

* test fixes and fixed links

* fix devui

* removed planning docs

* added enable method and updated readme and samples

* clarified docstring

* add return annotation

* updated naming

* update capatilized version

* updated readme and some fixes

* updated decorator name inline with the rest

* feedback from comments addressed

* Python: Fix middleware terminate flag to exit function calling loop immediately (#2868)

* Fix middleware terminate flag to exit function calling loop immediately

* Eliminating duck typing

* Improve function exec result handling

* Fix race condition

* Fix mypy issues

* Python: Fix context duplication in handoff workflows when restoring from checkpoint (#2867)

* Fix context duplication in handoff workflows when restoring from checkpoint

* Address Copilot PR review

* .NET: Update to latest Azure.AI.*, OpenAI, and M.E.AI* (#2850)

* Update to latest Azure.AI.*, OpenAI, and M.E.AI*

Absorb breaking changes in Responses surface area

* Update dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Utilities/ChatClientExtensions.cs

* Update dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Utilities/ChatClientExtensions.cs

* Update dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Utilities/ChatClientExtensions.cs

* Update dotnet/samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step04_CreateFromOpenAIResponseClient/Program.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Using patch to remove the model is necessary, updated the response client to actually use the the ForAgent

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>

* Bump actions/download-artifact from 6 to 7 (#2862)

Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 6 to 7.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump actions/cache from 4 to 5 (#2861)

Bumps [actions/cache](https://github.com/actions/cache) from 4 to 5.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump actions/upload-artifact from 5 to 6 (#2860)

Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 5 to 6.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Python : Ollama Connector for Agent Framework (#1104)

* Initial Commit for Olama Connector

* Added Olama Sample

* Add Sample & Fixed Open Telemetry

* Fixed Spelling from Olama to Ollama

* remove"opentelemetry-semantic-conventions-ai ~=0.4.13" since its handled in a different pr

* Added Tool Calling

* Finalizing test cases

* Adjust samples to be more reliable

* Update python/packages/ollama/agent_framework_ollama/_chat_client.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update python/packages/ollama/pyproject.toml

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update python/packages/ollama/tests/test_ollama_chat_client.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update python/packages/ollama/agent_framework_ollama/_chat_client.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Improved Docstrings & Sample

* Update python/packages/ollama/agent_framework_ollama/_chat_client.py

Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>

* Integrate PR Feedback
- Divided Streaming and Non-Streaming into independent Methods
- Catch Ollama Validation Error
- Add OTEL Provider Name
- Checked Ollama Messages
- Add Usage Statistics

* Revert setting, so it can be none

* Validate Message formatting between AF and Ollama

* Catch Ollama Error and raise a ServiceResponse Error

* Fix mypy error

* remove .vscode comma

* Add Reasoning support & adjust to new structure

* Add Ollama Multimodality and Reasoning

* Add test cases for reasoning

* Add Tests for Error Handling in Ollama Client

* Update python/samples/getting_started/multimodal_input/ollama_chat_multimodal.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Integrated Copilot Feedback

* Implement first PR Feedback

* Adjust Readme files for examples

* Adjust argument passing via additional chat options

* Implemented PR Feedback

* Removing Ollama Package from Core and moving samples

* Fix Link & Adding Samples to Main Sample Readme

* Fixing Links in Readme

* Moved Multimodal and Chat Example

* Fixed Link in ChatClient to Ollama

* Fix AgentFramework Links in Ollama Project

* Fix observability breaking change

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>

* Skip failing IT (#2904)

* .NET: Cosmos DB UT Fast Skip (For Non-Configured Local envs) (#2906)

* Cosmos DB UT Fast Skip (Non-Configured Local envs) + Long running UT skip in pipeline when no CosmosDB changes happened

* Force a CosmosDB source code change to trigger the pipeline

* Address possible string boolean mismatch

* Add debug

* Enabling emulator always when running IT

* .NET: Add TTLs to durable agent sessions (#2679)

* .NET: Add TTLs to durable agent sessions

* Remove unnecessary async

* PR feedback: clarify UTC

* PR feedback: limit minimum signal delay to <= 5 minutes

* PR feedback: Fix TTL disablement

* Linter: use auto-property

* Fix build break from OpenAI SDK change

* Updated CHANGELOG.md

* PR feedback

* Reduce default TTL to 14 days to work around DTS bug

* Python:  Update Mem0Provider to use v2 search API `filters` parameter (#2766)

* short fix to move id parameters to filters object

* added tests

* small fix

* mem0 dependency update

* Updated package versions (#2913)

* .NET: Switch to new "Run" method name. (#2843)

* Switch to new "RunAgent" method name.

* Try to disable false positive naming warning.

* Add comment about disabled warnings.

* Rename `RunAgent` to just `Run`.

* Update CHANGELOG.

* Python: Switch to new "run" method name. (#2890)

* Switch to `run` method.

* Add support for deprecated `run_agent`.

* Fix entity method name.

* Fix method name and improve tests.

* Update comment.

* Update Python CHANGELOG.

* [BREAKING] Python: Add factory pattern to handoff orchestration builder (#2844)

* WIP: Factory pattern to handoff

* Add factory pattern to concurrent orchestration builder; Next: tests and sample verification

* Add tests and improve comments

* Fix mypy

* Simplify handoff_simple.py

* Simplify handoff_autonoumous.py and bug fix

* Update readme

* Address Copilot comments

* Python: Flow custom kwargs to agents via Workflow SharedState (#2894)

* Flow custom kwargs to agents via SharedState

* Address Copilot feedback

* Improve sample typing

* Fix test

* Fix Pydantic error when using Literal type for tool params (#2893)

* Updated Ollama package version (#2920)

* Python: Azure AI Agent with Bing Grounding Citations Sample (#2892)

* bing grounding sample with citations

* small fix

* fix

* .NET: Make DelegatingAIAgent abstract (#2797)

* Initial plan

* Make DelegatingAIAgent abstract

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* Added additional arguments for Azure AI agent (#2922)

* Python: Correction of MCP image type conversion in  _mcp.py (#2901)

* Correction of MCP image type conversion in  _mcp.py

* Added a new overload to the init function of the DataContent() type of the Agent Framework, edited the test case to correctly test the usage of the data and uri fields while using DataContent()

* Fixed tests related to the changes of the DataContent type, added testing for both string and byte representations

* Pass kwargs into subworkflows (#2923)

* Python: Move ollama samples to samples getting started dir (#2921)

* Move ollama samples to samples getting started dir

* Address feedback

* Python: fix: correct BadRequestError when using Pydantic model in response_fo… (#1843)

* fix: correct BadRequestError when using Pydantic model in response_format

* Fix lint

---------

Co-authored-by: Evan Mattson <evan.mattson@microsoft.com>

* .NET: [Breaking] Delete display name property (#2758)

* delete the AIAgent.DisplayName property

* use agent name as a first value for activity display name

* Update dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/HandoffAgentExecutor.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Python: cleanup and refactoring of chat clients (#2937)

* refactoring and unifying naming schemes of internal methods of chat clients

* set tool_choice to auto

* fix for mypy

* added note on naming and fix #2951

* fix responses

* fixes in azure ai agents client

* Python: Workflow add option to visualize internal executors (#2917)

* Workflow add option to visualize internal executors

* Address Copilot comments

* Python: Fixes Run ID and Thread ID casing to align with AG-UI Typescript SDK (#2948)

* added camelCase input to run id and thread id aligning with @ag-ui/core

* fixed per copilot suggestions

* Python: Add workflow cancellation sample (#2732)

* Add workflow cancellation sample

Add sample demonstrating how to cancel a running workflow using asyncio
tasks. Shows both cancellation mid-execution and normal completion paths.
Useful for implementing timeouts, graceful shutdown, or A2A executors.

* update docstring

* .NET: Update Anthropic package to version 12.0.0 (#2914)

* Initial plan

* Update Anthropic package to version 12.0.0

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>

* Python: Add Azure Managed Redis Support with Credential Provider (#2887)

* azure redis support

* small fixes

* azure managed redis sample

* fixes

* Bump CommunityToolkit.Aspire.OllamaSharp from 13.0.0-beta.440 to 13.0.0 (#2856)

---
updated-dependencies:
- dependency-name: CommunityToolkit.Aspire.OllamaSharp
  dependency-version: 13.0.0
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump AWSSDK.Extensions.Bedrock.MEAI from 4.0.4.11 to 4.0.5 (#2853)

---
updated-dependencies:
- dependency-name: AWSSDK.Extensions.Bedrock.MEAI
  dependency-version: 4.0.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Mark Wallace <127216156+markwallace-microsoft@users.noreply.github.com>

* Bump Azure.AI.AgentServer.AgentFramework from 1.0.0-beta.4 to 1.0.0-beta.5 (#2854)

---
updated-dependencies:
- dependency-name: Azure.AI.AgentServer.AgentFramework
  dependency-version: 1.0.0-beta.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: Azure.AI.AgentServer.AgentFramework
  dependency-version: 1.0.0-beta.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>

* Python: Fix WorkflowAgent event handling and kwargs forwarding (#2946)

* Fix kwargs propagation through workflow.as_agent()

* Fix WorkflowAgent to respect AgentExecutor output_response setting

* .NET: Use GrpcEntityRunner instead of TaskEntityDispatcher (#2759)

* Use GrpcEntityRunner instead of TaskEntityDispatcher

* Pin to Durable worker 1.11.0

* Set the invocation result

* Update all Durable packages

* Update changelog, rename dispatcher to encondedEntityRequest

* Python: Bump Py version to 1.0.0b251218 for a release. Update CHANGELOG (#2968)

* Bump Py version to 1.0.0b251218 for a release. Update CHANGELOG

* update lock

* Fix formatting

* Fix ChatKit typing

* Python: Introducing Foundry Local Chat Clients (#2915)

* redo foundry local chat client

* fix mypy and spelling

* better docstring, updated sample

* fixed tests and added tests

* small sample update

* Updated package versions (#2978)

* Python: Added GitHub MCP sample with PAT (#2967)

* added github mcp sample with PAT

* addressed copilot fixes

* env fix

* Python: Preserve reasoning blocks with OpenRouter (#2950)

* Preserve reasoning blocks with OpenRouter

* Put encrypted reasoning in TextReasoningContent

* Remove unneccessary change

* Fix docs

* Support streaming

* Fix handling None in TextReasoningContent.text

* Python: Added response.created and response.in_progress event process to OpenAIBaseResponseClient (#2975)

* added response.created and response.in_progress to include response.id

* better doc string

* added tests for the new streaming event types

* Python: Introducing support for Bedrock-hosted models (Anthropic, Cohere, etc.) (#2610)

* Pushing the bedrock related changes to the new branch after addressing the review comments

* 2524 Addressed the second round review comments

* 2524 Addressed few more minor comments on the PR

* resolving the merge conflict

* 2524 resolved the uv.lock conflicts

* 2524 addressed more comments

* 2524 removed the print statement to fix the checks failure

* 2524 resolved the CI failure issues

* 2524 fixing the CI breaks

* 2524 Addressed the review comment

* 2524 resolved conflict

---------

Co-authored-by: Sunil Dutta <sunil.dutta@penske.com>
Co-authored-by: budgetboardingai <apurva.sharma31@gmail.com>

* .NET: [Durable Agents] Reliable streaming sample (#2942)

* .NET: [Durable Agents] Reliable streaming sample

* Add automated validation for new sample

* Address Copilot PR feedback

* Fix typo in README.md about agent definitions (#2634)

* Fix typo in README.md about agent definitions

* Update agent-samples/README.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Python: latency improvements (#3014)

* latency improvements

* fixed mypy, added coding standards and instructions

* slight logic improvement

* Python: Updated package versions (#3024)

* Updated package versions

* Updated changelog

* Python: add powerfx safe mode (#3028)

* add powerfx safe mode

* improved docstring and aligned env_file loading

* ensured test uses reset

* .NET: [Breaking] Introduce RunCoreAsync/RunCoreStreamingAsync delegation pattern in AIAgent (#2749)

* Initial plan

* Refactor AIAgent: Make RunAsync and RunStreamingAsync non-abstract, add RunCoreAsync and RunCoreStreamingAsync

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* Fix infinite recursion in test implementations

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* Make RunAsync and RunStreamingAsync non-virtual as requested

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* Fix DelegatingAIAgent subclasses to use RunCoreAsync/RunCoreStreamingAsync

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* Fix XML documentation references in AnonymousDelegatingAIAgent

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* Restore <see cref> tags with proper qualified signatures in AnonymousDelegatingAIAgent

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* Rollback unnecessary XML documentation changes in AnonymousDelegatingAIAgent

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* Remove pragma and update crefs to RunCoreAsync/RunCoreStreamingAsync

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* Fix EntityAgentWrapper to call base.RunCoreAsync/RunCoreStreamingAsync

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* fix compilation issues

* fix compilatio issue

* fix tests

* fix unit tests

* fix unit test

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>
Co-authored-by: SergeyMenshykh <sergemenshikh@gmail.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>

* add issue template and additional labeling (#3006)

* fix and extra int test (#3037)

* .NET: [BREAKING] Refactor ChatMessageStore methods to be similar to AIContextProvider and add filtering support (#2604)

* Refactor ChatMessageStore methods to be similar to AIContextProvider

* Fix file encoding

* Ensure that AIContextProvider messages area also persisted.

* Update formatting and seal context classes

* Improve formatting

* Remove optional messages from constructor and add unit test

* Add ChatMessageStore filtering via a decorator

* Update sample and cosmos message store to store AIContextProvider messages in right order. Fix unit tests.

* Update Workflowmessage store to use aicontext provider messages.

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* Improve xml docs messaging

* Address code review comments.

* Also notify message store on failure

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* [BREAKING] Remove unused AgentThreadMetadata (#3067)

* Remove unused AgentThreadMetadata

* Update DurableTask Changelog

* Python: Fix AzureAIClient failure when conversation history contains assistant messages (#3076)

* Fix AzureAIClient failure when conversation history contains assistant messages

* Address PR review feedback: improve docstring and test assertions

* Remove redundant cast

* Fix: Update OTLP exporter protocol conditions (#3070)

* Python: Fix ExecutorInvokedEvent and ExecutorCompletedEvent observability data (#3090)

* Fix ExecutorInvokedEvent.data mutation bug

* Fix bug related to not yielding output type

* .NET: Seal ChatClientAgentThread (#2842)

* Initial plan

* Seal ChatClientAgentThread class

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* Fix broken strands urls. (#3102)

* Fix broken strands urls.

* Fix typos

* .NET: Fix message ordering inconsistency when using AIContextProvider (#2659)

* Initial plan

* Fix message ordering inconsistency when using AIContextProvider

Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>

* Revert to original message ordering: Input, AIContextProvider, Response

Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>

* Reorder messages to ChatClient to match MessageStore order: Existing, Input, AIContextProvider

Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>

* Remove redundant test methods as existing tests already verify the behavior

Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>
Co-authored-by: Mark Wallace <127216156+markwallace-microsoft@users.noreply.github.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>

* fix: tool_choice parameter not being honored when passed to agent.run() (#3095)

* sharepoint sample fix (#3108)

* Bump versions to 1.0.0b260106 for a release. Update CHANGELOG.md (#3109)

* Bump Bedrock version to latest (#3110)

* Python: Fix MCP tool result serialization for list[TextContent] (#2523)

* Fix MCP tool result serialization for list[TextContent]

When MCP tools return results containing list[TextContent], they were
incorrectly serialized to object repr strings like:
'[<agent_framework._types.TextContent object at 0x...>]'

This fix properly extracts text content from list items by:
1. Checking if items have a 'text' attribute (TextContent)
2. Using model_dump() for items that support it
3. Falling back to str() for other types
4. Joining single items as plain text, multiple items as JSON array

Fixes #2509

* Address PR review feedback for MCP tool result serialization

- Extract serialize_content_result() to shared _utils.py
- Fix logic: use texts[0] instead of join for single item
- Add type annotation: texts: list[str] = []
- Return empty string for empty list instead of '[]'
- Move import json to file top level
- Add comprehensive unit tests for serialization

* Address PR review feedback: fix type checking and double serialization

- Add isinstance(item.text, str) check to ensure text attribute is a string
- Fix double-serialization issue by keeping model_dump results as dicts
  until final json.dumps (removes escaped JSON strings in arrays)
- Improve docstring with detailed return value documentation
- Add test for non-string text attribute handling
- Add tests for list type tool results in _events.py path

* Simplify PR: minimal changes to fix MCP tool result serialization

Addresses reviewer feedback about excessive refactoring:
- Reset _events.py to original structure
- Only add import and use serialize_content_result in one location
- All review comments addressed in serialize_content_result():
  - Added isinstance(item.text, str) check
  - Use model_dump(mode="json") to avoid double-serialization
  - Improved docstring with explicit return value documentation
  - Empty list returns "" instead of "[]"

* Refactor: Move MCP TextContent serialization to core prepare_function_call_results

Per reviewer feedback, moved the TextContent serialization logic from
ag-ui's serialize_content_result to the core package's
prepare_function_call_results function.

Changes:
- Added handling for objects with 'text' attribute (like MCP TextContent)
  in _prepare_function_call_results_as_dumpable
- Removed serialize_content_result from ag-ui/_utils.py
- Updated _events.py and _message_adapters.py to use
  prepare_function_call_results from core package
- Updated tests to match the core function's behavior

* Fix failing tests for prepare_function_call_results behavior

- test_tool_result_with_none: Update expected value to 'null' (JSON serialization of None)
- test_tool_result_with_model_dump_objects: Use Pydantic BaseModel instead of plain class

* Fix B903 linter error: Convert MockTextContent to dataclass

The ruff linter was reporting B903 (class could be dataclass or namedtuple)
for the MockTextContent test helper classes. This commit converts them to
dataclasses to satisfy the linter check.

* Python: Improve DevUI, add Context Inspector view as new tab under traces (#2742)

* Improve DevUI, add Context Inspector view as new tab under traces

* fix mypy errors

* fix: Handle stale MCP connections in DevUI executor

MCP tools can become stale when HTTP streaming responses end - the underlying
stdio streams close but `is_connected` remains True. This causes subsequent
requests to fail with `ClosedResourceError`.

Add `_ensure_mcp_connections()` to detect and reconnect stale MCP tools before
agent execution. This is a workaround for an upstream Agent Framework issue
where connection state isn't properly tracked.

Fixes MCP tools failing on second HTTP request in DevUI.

fixes  #1476 #1515 #2865

* fix #1572 report import dependency errors more clearly

* Ensure there is streaming toggle where users can select streaming vs non streaming mode in devui . Fixes .NET: [Python] DevUI tool call rendering in non-streaming mode?

* remove unused dead code

* improve ux - workflows with agents show a chat component in execution timelien, also ensure magentic final output shows correctly

* update ui build

* update devui to use instrumentation instead of tracing, other instrumentation and type/instance check fixes

* .NET: Seal factory contexts and add non JSO deserialize overloads (#3066)

* Seal factory contexts and add non JSO deserialize overloads

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Enable blank issues in issue template configuration

Need to re-enable creating blank issues

* updated templates (#3106)

* updated templates

* enabled blank and fixed triage

* made language optional and moved to the bottom for features

* Python: Streaming sample for azurefunctions (#3057)

* Streaming sample for azurefunctions

* Fixed links and sample name

* Addressed feedback

* Addressed feedback

* Fixed integration tests

* Updated test

* Python: fix(azure-ai): Fix response_format handling for structured outputs (#3114)

* fix(azure-ai): read response_format from chat_options instead of run_options

* refactor: use explicit None checks for response_format

* Fix mypy error

* Mypy fix

* Python: Bump python version to 1.0.0b260107 for a release (#3128)

* Bump python version to 1.0.0b260107 for a release

* Update changelog

* Make A2AAgent public, so that it's concrete implementation methods can be used. (#3119)

* .NET: Map additional props <-> A2A metadata (#3137)

* map additional props from agent run options to a2a request metadata

* small touches

* add unit tests for new extension methods

* Sort using

* add unit test

* add additiona unit tests

* special case json element to avoid unnecessary serialization

* Python: Fix Anthropic streaming response bugs (#3141)

* test commit identity

* fix(anthropic): fix raw_representation and finish_reason in streaming

* lint fix

* Bump AWSSDK.Extensions.Bedrock.MEAI from 4.0.5 to 4.0.5.1 (#2994)

---
updated-dependencies:
- dependency-name: AWSSDK.Extensions.Bedrock.MEAI
  dependency-version: 4.0.5.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>

* Bump Anthropic from 12.0.0 to 12.0.1 (#2993)

---
updated-dependencies:
- dependency-name: Anthropic
  dependency-version: 12.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>

* .NET: [Breaking] Prevent loss of input messages & streamed updates when resuming streaming (#2748)

* save input messages and stream updates to the continuation token to be able to use them in the last successful stream resumption call.

* Update dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentContinuationToken.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentContinuationToken.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_BackgroundResponsesTests.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentContinuationToken.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentContinuationToken.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix typo

* init continuation token from chat response

* remove unnecessary types for source generation

* remove check for continuation token passed at initial run

* remove check for continuation token pass at initial run

* centralize continuation token parsing

* update xml comments

* use readonly collection instead of enumerable

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* .NET: fix: Expose WorkflowErrorEvent as ErrorContent (#2762)

* fix: Expose WorkflowErrorEvent as ErrorContent

When hosted using .AsAgent(), Workflows were not exposing inner errors coming as Exceptions (through the WorkflowErrorEvent)

The fix is to convert their message to an ErrorContent on the way out, rather than rely on the default "empty update" to collect the raw event.

* feat: Add a way to show/suppress exception information

* Bump Microsoft.Agents.AI.Workflows from 1.0.0-preview.251125.1 to 1.0.0-preview.251219.1 (#2997)

---
updated-dependencies:
- dependency-name: Microsoft.Agents.AI.Workflows
  dependency-version: 1.0.0-preview.251219.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>
Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* .NET: Add Run overloads to expose ChatClientAgentRunOptions in IntelliSense (#3115)

* Initial plan

* Add ChatClientAgentExtensions for improved discoverability of ChatClientAgentRunOptions

Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>

* Address code review feedback - use collection expression syntax

Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>

* Apply suggestion from @westey-m

* Fix issues with Copilot implementation

* Add additional tests for structured output overloads.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>

* Python: Add tool call/result content types and update connectors and samples (#2971)

* Add new AI content types and image tool support

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Add Python content types for tool calls/results and image generation tool support

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Address review feedback for tool content and samples

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Tighten image generation typing and sample tools list

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Align image generation output typing

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Handle MCP naming, image options mapping, and connector tool content

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Allow MCP call in function approval request

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Remove raw image_generation tool remapping

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Restore Anthropic tool_use to function calls unless code execution

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Fix lint issues for hosted file docstring and MCP parsing

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Import ChatResponse types in Anthropic client

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Fix Anthropics citation type imports and MCP typing for handoff/tools

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Skip lightning tests without agentlightning and fix function call import

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* fix lint on lab package

* rebuilt anthropic parsing

* redid anthropic parsing

* typo

* updated parsing and added missing docstrings

* fix tests

* mypy fixes

* second mypy fix

* add new class to other samples

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>
Co-authored-by: eavanvalkenburg <github@vanvalkenburg.eu>

* Bump Google.GenAI from 0.6.0 to 0.9.0 (#2995)

---
updated-dependencies:
- dependency-name: Google.GenAI
  dependency-version: 0.9.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>

* Bump js-yaml from 4.1.0 to 4.1.1 in /python/packages/devui/frontend (#3123)

Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.0 to 4.1.1.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.0...4.1.1)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 4.1.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Updated package versions (#3144)

* .NET: Bump Microsoft.Agents.AI.OpenAI and Microsoft.Extensions.AI.OpenAI (#2996)

* Bump Microsoft.Agents.AI.OpenAI and Microsoft.Extensions.AI.OpenAI

Bumps Microsoft.Agents.AI.OpenAI from 1.0.0-preview.251125.1 to 1.0.0-preview.251219.1
Bumps Microsoft.Extensions.AI.OpenAI from 10.1.0-preview.1.25608.1 to 10.1.1-preview.1.25612.2

---
updated-dependencies:
- dependency-name: Microsoft.Agents.AI.OpenAI
  dependency-version: 1.0.0-preview.251219.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: Microsoft.Extensions.AI.OpenAI
  dependency-version: 10.1.1-preview.1.25612.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: Microsoft.Agents.AI.OpenAI
  dependency-version: 1.0.0-preview.251219.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: Microsoft.Extensions.AI.OpenAI
  dependency-version: 10.1.1-preview.1.25612.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* Fixed samples

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>
Co-authored-by: Mark Wallace <127216156+markwallace-microsoft@users.noreply.github.com>
Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com>

* Python: fix(ag-ui): Execute tools with approval_mode, fix shared state, code cleanup  (#3079)

* fix(ag-ui): execute tools after approval in human-in-the-loop flow

* Fix shared state bug

* Bug fix finalized

* Refactoring to clean up code

* Code cleanup

* More fixes

* More code cleanup

* Add version detection in __init__.py to ruff ignore list

* Track agent name with updates for workflow agent (#3146)

* Python: Fix AzureAIClient tool call bug for AG-UI use (#3148)

* Fiz AzureAIClient tool call bug

* Address copilot feedback

* Python: multiple bug fixes (#3150)

* fix Python: kwargs are not passed to _prepare_thread_and_messages in ChatAgent.run
Fixes #3118

* fix Python: [Bug]: model_id versus model_deployment_name is confusing in Azure AI Agents
Fixes #3147

* add types

* fixed type and docstring

* fix(anthropic): fix duplicate ToolCallStartEvent in streaming tool calls (#3051)

When processing `input_json_delta` events, the Anthropic client was
passing the tool name from the previous `tool_use` event. This caused
ag-ui's `_handle_function_call_content` to emit a `ToolCallStartEvent`
for every streaming chunk (since it triggers on `if content.name:`).

This fix changes the behavior to pass an empty string for `name` in
`input_json_delta` events, matching OpenAI's behavior where streaming
argument chunks have `name=""`. The initial `tool_use` event still
provides the tool name, so only one `ToolCallStartEvent` is emitted.

Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>

* .NET: [BREAKING] Change GetNewThread and DeserializeThread to async (#3152)

* Change GetNewThread and DeserializeThread plus ChatMessageStore and AIContextProvider Factories to async

* Merge fixes

* Fix Ollama model env var in documentation (#3156)

Signed-off-by: Dina Suehiro Jones <dina.s.jones@intel.com>

* Python: Add Pydantic request model and OpenAPI tags support to AG-UI FastAPI endpoint (#2522)

* feat(ag-ui): Add Pydantic request model and OpenAPI tags support

- Add AGUIRequest Pydantic model in _types.py with field descriptions
- Update add_agent_framework_fastapi_endpoint() to accept tags parameter
- Use AGUIRequest model for automatic validation and OpenAPI schema generation
- Export AGUIRequest and DEFAULT_TAGS in __init__.py
- Update test_endpoint.py to expect 422 for invalid requests
- Add tests for OpenAPI schema, default tags, custom tags, and validation

Benefits:
- Better API documentation with complete request schema in Swagger UI
- Automatic request validation with Pydantic
- Organized endpoints under 'AG-UI' tag instead of 'default'
- Improved developer experience and type safety

Fixes #<issue-number>

* test(ag-ui): Add test for internal error handling to achieve 100% coverage

- Add test_endpoint_internal_error_handling() to cover exception handling code
- Mock copy.deepcopy to simulate internal error during default_state processing
- Add type: ignore for FastAPI tags parameter (known pyright compatibility issue)
- Achieves 100% test coverage for _endpoint.py (previously missing lines 103-105)

* .NET: Improve resolving `AITool` from DI (#3175)

* remove localagenttoolregistry

* also give the factory method API

* Python: Fix MCPStreamableHTTPTool to use new streamable_http_client API (#3088)

* Fix MCPStreamableHTTPTool to use new streamable_http_client API with proper httpx client cleanup

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Update docstring to reflect new streamable_http_client API usage

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Refactor MCPStreamableHTTPTool to accept optional http_client parameter and delegate client creation to streamable_http_client

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Update mcp package minimum version to 1.24.0 for streamable_http_client API support

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Fix critical bugs: apply headers/timeout/sse_read_timeout when creating httpx client, add version constraint <2, and properly manage client lifecycle

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Simplify implementation: remove headers/timeout/sse_read_timeout params, remove kwargs, remove close() override per feedback

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Add back **kwargs parameter for backward compatibility (accepted but not used)

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Remove unused httpx import from test file

Note: The uv.lock file needs to be updated with 'uv sync' to reflect the mcp version constraint change (>=1.24.0,<2)

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* cicd fixes

* udpated samples with headers examples

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>
Co-authored-by: eavanvalkenburg <github@vanvalkenburg.eu>

* azureai direct a2a endpoint support (#3127)

* Python: [BREAKING]: removed display_name, renamed context_providers, middleware and AggregateContextProvider (#3139)

* removed display_name, renamed context_providers, middleware and AggregateContextProvider

* fixes

* fixed test

* testfix

* removed mistakenly put back test

* updated new test

* rename middlewares to middleware

* middleware fixes

* Python: MCP Improvements: improved connection loss behavior, pagination for loading and a param to control representation (#3154)

* pagination support (#2848) added a parse_tool_result param and connection loss (#2884)

* fix #3153

* improved connection handling

* improved logic

* Python: Add declarative workflow runtime (#2815)

* Further support for declarative python workflows

* Add tests. Clean up for typing and formatting

* Improvements and cleanup

* Typing cleanup. Improve docstrings

* Proper code in docstrings

* Fix malformed code-block directive in docstring

* Remove dead links

* PR feedback

* Address PR feedback

* Address PR feedback

* Remove sl

* Update devui frontend

* More cleanup

* Fix uv lock

* Skip Py 3.14 tests as powerfx doesn't support it

* Fix mypy error

* Fix for tool calls

* Removed stale docstring

* Fix lint

* Standardize on .NET namespaces. Revert DevUI changes (bring in later)

* Implement remaining items for Python declarative support to match dotnet

* point URL to agent, not to agentcard (#3176)

* Python: [BREAKING]: Introducing Options as TypedDict and Generic (#3140)

* WIP typeddict for options

* updated all clients and ChatAgents

* updated everything

* added ADR

* fix mypy

* proper typevar imports

* fixed import

* fixed other imports

* slight update in the sample

* updated from feedback

* fixes

* fixed missing covariants and test fixes

* fixed typing

* updated anthropic thinking config

* ruff fixes

* fixed int tests

* fix tests and mypy

* updated integration tests

* updated docstring and test fix

* improved options handling in obser

* mypy fix

* updated a host of integration tests

* fix tests

* bedrock fix

* [BREAKING] Python: Refactor orchestrations (#3023)

* Group chat refactoring Part 1; Next: HIL and handoff

* Add agent approval flow; next samples

* WIP: samples

* WIP: HIL samples

* Group chat HIL working; next: handoff

* Fix group chat tool approval sample

* WIP: refactor handoff; next handoff handling

* Handoff done; next handoff samples and concurrent and sequential

* Handoff samples, concurrent, and sequential done; next Magentic

* WIP: magentic; next test with samples + HIL

* Magentic Working; next fix all samples and tests

* Fix handoff samples; next tests

* WIP: fixing tests; some orchestration as agent samples are failing

* Group chat unit tests done

* Handoff  unit tests done

* Remove old orchestration_request_info and fix related tests

* Magentic unit tests done

* Fix samples

* Fix test

* Fix test 2

* mypy

* Address comments

* Update readme

* Address comments

* Address comments 2

* Replace display name

* Python: ADR for create/get agent API (#2618)

* ADR for create/get agent API

* Updated ADR with implementation options

* Small updates

* Updated decision outcome section

* Updated broken links

* Small updates

* Fixed merge conflicts

* Small fix

* Updated decision outcome section

* Small fixes

* Updated provider naming based on client SDK

* Add ignored parameter for CodeQL in workflow (#3204)

* Implement IReadOnlyList on InMemoryChatMessageStore (#3205)

* .NET: Make ChatMessageStore and AIContextProvider context props settable (#3196)

* Make ChatMessageStore and AIContextProvider context props setable

* Add validation to preserve non-null requirement of certain properties.

* Fix broken tests.

* Python: Add dependencies param to ag-ui FastAPI endpoint (#3191)

* Add dependencies param to ag-ui FastAPI endpoint

* Address Copilot feedback

* renamed all (#3207)

* Python: ADR for simplified get response (#3098)

* ADR for simplified get response

* updated some language, added agent option and code comparison

* small update in sample

* added workflows and expanded some points

* changed decision and number

* updated with stream=False default

* .NET: [Breaking] Rename`AgentRunResponse` and `AgentRunResponseUpdate` classes (#3197)

* rename AgentRunResponse and AgentRunResponseUpdate classes - part1

* rename varialbles, parameters, methods and tests

* rollback unnecessary changes

* .NET: [Breaking] Rename AgentRunResponseEvent and AgentRunUpdateEvent classes (#3214)

* rename AgentRunResponseEvent and AgentRunUpdateEvent classes

* rollback unnecessary changes

* Python: Create/Get Agent API for Azure V2 (#3059)

* Added get_agent method to Azure AI V2

* Small fixes

* Small fix

* Removed AzureAIAgentProvider

* Added create_agent method

* Small fixes

* Fixed code interpreter tool mapping

* Added agent provider for V2 client

* Updated response format handling

* Added provider example

* Fixed errors

* Update python/samples/getting_started/agents/azure_ai/README.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Small fix

* Updates from merge

* Resolved comments

* Resolved comments

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Python: Add more specific exceptions to Workflow (#3188)

* Add more specifc workflow exceptions

* Fix tests

* AI comments

* Misc

* Python: Added AzureAI sample for downloading code interpreter generated files (#3189)

* added azure ai code interpreter file download sample

* copilot fix suggestions

* function name fixes + readme update

* small fix

* update package versions (#3223)

Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com>

* Python: fix(core): correct FunctionResultContent ordering in WorkflowAgent.merge_updates (#3168)

* fix(core): simplify FunctionResultContent ordering in WorkflowAgent.merge_updates

* improve comment

* Fix name

* fix(workflows): rename WorkflowOutputEvent.source_executor_id to executor_id for API consistency (#3166)

* Python: fix(ag-ui): add MCP tool support for AG-UI approval flows (#3212)

* add MCP tool support for AG-UI approval flows

* use attribute in place of property

* Python: Properly configure structured outputs based on new options dict (#3213)

* Properly configure structured outputs based on new options dict

* Fix mypy

* .NET: Merge AgentRunOptions.AdditionalProperties into ChatOptions.AdditionalProperties (#3184)

* Merge AgentRunOptions.AdditionalProperties into ChatOptions.AdditionalProperties

* Fix namespace and typo.

* .NET: Update Google.GenAI to 0.11.0 and remove polyfill implementations (#3232)

* Initial plan

* Update Google.GenAI to 0.11.0 and remove polyfill files

Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>

* .NET: [BREAKING] Renamed CreateAIAgent/GetAIAgent to AsAIAgent (#3222)

* Renamed chat client extension method

* Additional renaming

* Updated documentation

* Fixed tests

* Small fix

* Small fix

* Updated DurableAIAgent and fixed integration tests (#3241)

* Python: Create/Get Agent API for Azure V1 (#3192)

* Added provider implementation for Azure AI V1

* Small fixes

* Fixed OpenAPI example

* Fixed local MCP example

* Fixed hosted MCP example

* Fixed file search sample

* Small fixes

* Resolved comments

* Doc updates

* Bump azure-core from 1.37.0 to 1.38.0 in /python (#3209)

Bumps [azure-core](https://github.com/Azure/azure-sdk-for-python) from 1.37.0 to 1.38.0.
- [Release notes](https://github.com/Azure/azure-sdk-for-python/releases)
- [Commits](https://github.com/Azure/azure-sdk-for-python/compare/azure-core_1.37.0...azure-core_1.38.0)

---
updated-dependencies:
- dependency-name: azure-core
  dependency-version: 1.38.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Python: Create/Get Agent API for OpenAI Assistants (#3208)

* Added provider implementation

* Added example with response format

* Small improvements

* Python: (AG-UI) Support service-managed thread on AG-UI  (#3136)

* added service thread support

* set service_thread_id to only supplied_thread_id

* uses raw_representation to extract the conversation_id

* removed accidental edit

* updated test to use raw_representation

* resolves copilot review feedback

* revert back StubAgent, since not used

* removed relative module import

* removed hasattr check per PR feedback

* Create/Get Agent API - fixes and example improvements (#3246)

* Fix merge conflicts

---------

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Dina Suehiro Jones <dina.s.jones@intel.com>
Co-authored-by: Tao Chen <taochen@microsoft.com>
Co-authored-by: Kurt <65111699+q33566@users.noreply.github.com>
Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>
Co-authored-by: Korolev Dmitry <deagle.gross@gmail.com>
Co-authored-by: Mark Wallace <127216156+markwallace-microsoft@users.noreply.github.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>
Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>
Co-authored-by: Jose Luis Latorre Millas <joslat@gmail.com>
Co-authored-by: Jacob Alber <jaalber@microsoft.com>
Co-authored-by: Richard Ortega <richardjortega@gmail.com>
Co-authored-by: 刘邦学AI <lbbniu@gmail.com>
Co-authored-by: Stephen Toub <stoub@microsoft.com>
Co-authored-by: Nico Möller <nkm-moeller@mail.de>
Co-authored-by: Chris Gillum <cgillum@microsoft.com>
Co-authored-by: Giles Odigwe <79032838+giles17@users.noreply.github.com>
Co-authored-by: Phillip Hoff <phillip.hoff@gmail.com>
Co-authored-by: Ege Ozan Özyedek <36128615+egeozanozyedek@users.noreply.github.com>
Co-authored-by: samueljohnsiby <66901393+samueljohnsiby@users.noreply.github.com>
Co-authored-by: Evan Mattson <evan.mattson@microsoft.com>
Co-authored-by: Hao Luo <338265+howlowck@users.noreply.github.com>
Co-authored-by: Victor Dibia <chuvidi2003@gmail.com>
Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
Co-authored-by: Jacob Viau <javia@microsoft.com>
Co-authored-by: SuperKenVery <39673849+SuperKenVery@users.noreply.github.com>
Co-authored-by: Sunil Dutta <dutta.2003@gmail.com>
Co-authored-by: Sunil Dutta <sunil.dutta@penske.com>
Co-authored-by: budgetboardingai <apurva.sharma31@gmail.com>
Co-authored-by: Syrine Chelly <62653967+SyChell@users.noreply.github.com>
Co-authored-by: SergeyMenshykh <sergemenshikh@gmail.com>
Co-authored-by: westey <164392973+westey-m@users.noreply.github.com>
Co-authored-by: takanori-terai <123897708+takanori-terai@users.noreply.github.com>
Co-authored-by: claude89757 <138977524+claude89757@users.noreply.github.com>
Co-authored-by: Gavin Aguiar <80794152+gavin-aguiar@users.noreply.github.com>
Co-authored-by: Sukeesh <vsukeeshbabu@gmail.com>
Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>
Co-authored-by: eavanvalkenburg <github@vanvalkenburg.eu>
Co-authored-by: Ao Chen <chenao3220@gmail.com>
Co-authored-by: Dina Suehiro Jones <dina.s.jones@intel.com>

* Python: Add integration tests for durabletask package (#3317)

* Add integration tests

* Fix flaky test

* Fix env viz

* Fix tests and address feedback

* Fix imports for durabletask (#3345)

* .NET: Python: Merge `main` into `feature-durabletask` branch (#3385)

* Python: Add factory pattern to concurrent orchestration builder (#2738)

* Add factory pattern to concurrent orchestration builder

* Update readme

* Address AI comments

* Fix unit tests

* Fix import

* Prevent multiple calls to set participants or factories

* Add comments

* Mitigate warnings

* Fix mypy

* Address comments

* Address Copilot comments

* Fix tests

* Python: fix: GroupChat ManagerSelectionResponse JSON Schema for OpenAI Structured Outpu… (#2750)

* fix: ManagerSelectionResponse JSON Schema for OpenAI Structured Output Strict Mode

* refactor: install pre-commit then commit again

* Capture file IDs from code interpreter in streaming responses (#2741)

* .NET: [BREAKING] Prevent nulls in AIAgent property (#2719)

* prevent nulls in AIAgent property

* address feedback

* code ql sm04598 (#2723)

Co-authored-by: Mark Wallace <127216156+markwallace-microsoft@users.noreply.github.com>

* .NET: Add Conversation State Sample (Step05) (#2697)

* Initial plan

* Add Agent_OpenAI_Step05_Conversation sample for conversation state management

Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>

* Update Program.cs comment to accurately describe the sample

Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>

* Update the code to use the ConversationClient more in line with the samples in OpenAI

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Changing sample to use ChatClientAgent and conversationId in GetNewThread

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Bump AWSSDK.Extensions.Bedrock.MEAI from 4.0.4.7 to 4.0.4.11 (#2777)

---
updated-dependencies:
- dependency-name: AWSSDK.Extensions.Bedrock.MEAI
  dependency-version: 4.0.4.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump Azure.Identity from 1.17.0 to 1.17.1 (#2780)

---
updated-dependencies:
- dependency-name: Azure.Identity
  dependency-version: 1.17.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: Azure.Identity
  dependency-version: 1.17.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: Azure.Identity
  dependency-version: 1.17.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: Azure.Identity
  dependency-version: 1.17.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump Azure.AI.AgentServer.AgentFramework from 1.0.0-beta.4 to 1.0.0-beta.5 (#2778)

---
updated-dependencies:
- dependency-name: Azure.AI.AgentServer.AgentFramework
  dependency-version: 1.0.0-beta.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: Azure.AI.AgentServer.AgentFramework
  dependency-version: 1.0.0-beta.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: Azure.AI.AgentServer.AgentFramework
  dependency-version: 1.0.0-beta.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Python: added more complete parsing for mcp tool arguments (#2756)

* added more complete parsing for mcp tool arguments

* fixed mypy

* added nonlocal model counter, and some fixes

* fixes in naming logic

* extracted json parsing function, added parametrized test and checked coverage

* Python: Updated package versions (#2784)

* Updated package versions

* Small fix

* Bump actions/checkout from 5 to 6 (#2404)

Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>

* .NET: adds support for labels in edges,  fixes rendering of labels in dot a… (#1507)

* adds support for labels in edges,  fixes rendering of labels in dot and mermaid, adds rendering of labels in edges

* Update dotnet/src/Microsoft.Agents.AI.Workflows/Visualization/WorkflowVisualizer.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* escaping edge labels, adding tests for labels containing strange characters that would break the diagram and enabling the previous signature so the API has backwards compatibility.

* Unify label in EdgeData

* Edge API adjustments, removed useless "sanitizer"

* fixed test

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Jacob Alber <jaalber@microsoft.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>

* Python: Added custom args and thread object to ai_function kwargs (#2769)

* Added an example of using kwargs in ai_function

* Added thread object to ai_function kwargs

* Updated docs

* Small fix

* Added thread parameter filtering

* Fix WorkflowAgent to include thread convo history. Enable checkpointing. (#2774)

* Update OpenAIResponses.yaml to match AgentSchema (#2598)

1. Update `connection` child types --  `kind: ApiKey` to `kind: key` otherwise schema will fail: https://microsoft.github.io/AgentSchema/reference/apikeyconnection/

2.  Update `outputSchema`'s `PropertySchema` to be `kind` instead of `type` otherwise schema will fail: https://microsoft.github.io/AgentSchema/reference/propertyschema/

* Python: Remove warnings from workflow builder on not using factories (#2808)

* Revert concurrent

* Fix comments

* Python: Filter framework kwargs from MCP tool invocations (#2870)

* Filter framework kwargs from MCP tool invocations

* Fixes

* Python: Fix WorkflowAgent to emit yield_output as agent response (#2866)

* Fix WorkflowAgent to emit yield_output as agent response

* use raw_representation

* Raw representation handling

* Python: Use agent description in HandoffBuilder auto-generated tools (#2713) (#2714)

## Summary
Enhanced `HandoffBuilder._apply_auto_tools` to use the target agent's
description when creating handoff tools, providing more informative tool
descriptions for LLMs.

## Changes
- Modified `_apply_auto_tools` to extract `description` from
  `AgentExecutor._agent` when available
- Updated iteration to use `.items()` for more efficient dict traversal
- Handoff tools now use agent descriptions instead of generic placeholders

## Example
Before: "Handoff to the refund_agent agent."
After: "You handle refund requests. Ask for order details and process refunds."

## Testing
- All handoff tests pass (20/20)
- No breaking changes to existing API

Fixes #2713

Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>

* Python: [BREAKING] Observability updates (#2782)

* fixes Python: Add env_file_path parameter to setup_observability() similar to AzureOpenAIChatClient
Fixes #2186

* WIP on updates using configure_azure_monitor

* improved setup and clarity

* fixed root .env.example

* revert changes

* updated files

* updated sample

* updated zero code

* test fixes and fixed links

* fix devui

* removed planning docs

* added enable method and updated readme and samples

* clarified docstring

* add return annotation

* updated naming

* update capatilized version

* updated readme and some fixes

* updated decorator name inline with the rest

* feedback from comments addressed

* Python: Fix middleware terminate flag to exit function calling loop immediately (#2868)

* Fix middleware terminate flag to exit function calling loop immediately

* Eliminating duck typing

* Improve function exec result handling

* Fix race condition

* Fix mypy issues

* Python: Fix context duplication in handoff workflows when restoring from checkpoint (#2867)

* Fix context duplication in handoff workflows when restoring from checkpoint

* Address Copilot PR review

* .NET: Update to latest Azure.AI.*, OpenAI, and M.E.AI* (#2850)

* Update to latest Azure.AI.*, OpenAI, and M.E.AI*

Absorb breaking changes in Responses surface area

* Update dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Utilities/ChatClientExtensions.cs

* Update dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Utilities/ChatClientExtensions.cs

* Update dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Utilities/ChatClientExtensions.cs

* Update dotnet/samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step04_CreateFromOpenAIResponseClient/Program.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Using patch to remove the model is necessary, updated the response client to actually use the the ForAgent

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>

* Bump actions/download-artifact from 6 to 7 (#2862)

Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 6 to 7.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump actions/cache from 4 to 5 (#2861)

Bumps [actions/cache](https://github.com/actions/cache) from 4 to 5.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump actions/upload-artifact from 5 to 6 (#2860)

Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 5 to 6.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Python : Ollama Connector for Agent Framework (#1104)

* Initial Commit for Olama Connector

* Added Olama Sample

* Add Sample & Fixed Open Telemetry

* Fixed Spelling from Olama to Ollama

* remove"opentelemetry-semantic-conventions-ai ~=0.4.13" since its handled in a different pr

* Added Tool Calling

* Finalizing test cases

* Adjust samples to be more reliable

* Update python/packages/ollama/agent_framework_ollama/_chat_client.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update python/packages/ollama/pyproject.toml

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update python/packages/ollama/tests/test_ollama_chat_client.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update python/packages/ollama/agent_framework_ollama/_chat_client.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Improved Docstrings & Sample

* Update python/packages/ollama/agent_framework_ollama/_chat_client.py

Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>

* Integrate PR Feedback
- Divided Streaming and Non-Streaming into independent Methods
- Catch Ollama Validation Error
- Add OTEL Provider Name
- Checked Ollama Messages
- Add Usage Statistics

* Revert setting, so it can be none

* Validate Message formatting between AF and Ollama

* Catch Ollama Error and raise a ServiceResponse Error

* Fix mypy error

* remove .vscode comma

* Add Reasoning support & adjust to new structure

* Add Ollama Multimodality and Reasoning

* Add test cases for reasoning

* Add Tests for Error Handling in Ollama Client

* Update python/samples/getting_started/multimodal_input/ollama_chat_multimodal.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Integrated Copilot Feedback

* Implement first PR Feedback

* Adjust Readme files for examples

* Adjust argument passing via additional chat options

* Implemented PR Feedback

* Removing Ollama Package from Core and moving samples

* Fix Link & Adding Samples to Main Sample Readme

* Fixing Links in Readme

* Moved Multimodal and Chat Example

* Fixed Link in ChatClient to Ollama

* Fix AgentFramework Links in Ollama Project

* Fix observability breaking change

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>

* Skip failing IT (#2904)

* .NET: Cosmos DB UT Fast Skip (For Non-Configured Local envs) (#2906)

* Cosmos DB UT Fast Skip (Non-Configured Local envs) + Long running UT skip in pipeline when no CosmosDB changes happened

* Force a CosmosDB source code change to trigger the pipeline

* Address possible string boolean mismatch

* Add debug

* Enabling emulator always when running IT

* .NET: Add TTLs to durable agent sessions (#2679)

* .NET: Add TTLs to durable agent sessions

* Remove unnecessary async

* PR feedback: clarify UTC

* PR feedback: limit minimum signal delay to <= 5 minutes

* PR feedback: Fix TTL disablement

* Linter: use auto-property

* Fix build break from OpenAI SDK change

* Updated CHANGELOG.md

* PR feedback

* Reduce default TTL to 14 days to work around DTS bug

* Python:  Update Mem0Provider to use v2 search API `filters` parameter (#2766)

* short fix to move id parameters to filters object

* added tests

* small fix

* mem0 dependency update

* Updated package versions (#2913)

* .NET: Switch to new "Run" method name. (#2843)

* Switch to new "RunAgent" method name.

* Try to disable false positive naming warning.

* Add comment about disabled warnings.

* Rename `RunAgent` to just `Run`.

* Update CHANGELOG.

* Python: Switch to new "run" method name. (#2890)

* Switch to `run` method.

* Add support for deprecated `run_agent`.

* Fix entity method name.

* Fix method name and improve tests.

* Update comment.

* Update Python CHANGELOG.

* [BREAKING] Python: Add factory pattern to handoff orchestration builder (#2844)

* WIP: Factory pattern to handoff

* Add factory pattern to concurrent orchestration builder; Next: tests and sample verification

* Add tests and improve comments

* Fix mypy

* Simplify handoff_simple.py

* Simplify handoff_autonoumous.py and bug fix

* Update readme

* Address Copilot comments

* Python: Flow custom kwargs to agents via Workflow SharedState (#2894)

* Flow custom kwargs to agents via SharedState

* Address Copilot feedback

* Improve sample typing

* Fix test

* Fix Pydantic error when using Literal type for tool params (#2893)

* Updated Ollama package version (#2920)

* Python: Azure AI Agent with Bing Grounding Citations Sample (#2892)

* bing grounding sample with citations

* small fix

* fix

* .NET: Make DelegatingAIAgent abstract (#2797)

* Initial plan

* Make DelegatingAIAgent abstract

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* Added additional arguments for Azure AI agent (#2922)

* Python: Correction of MCP image type conversion in  _mcp.py (#2901)

* Correction of MCP image type conversion in  _mcp.py

* Added a new overload to the init function of the DataContent() type of the Agent Framework, edited the test case to correctly test the usage of the data and uri fields while using DataContent()

* Fixed tests related to the changes of the DataContent type, added testing for both string and byte representations

* Pass kwargs into subworkflows (#2923)

* Python: Move ollama samples to samples getting started dir (#2921)

* Move ollama samples to samples getting started dir

* Address feedback

* Python: fix: correct BadRequestError when using Pydantic model in response_fo… (#1843)

* fix: correct BadRequestError when using Pydantic model in response_format

* Fix lint

---------

Co-authored-by: Evan Mattson <evan.mattson@microsoft.com>

* .NET: [Breaking] Delete display name property (#2758)

* delete the AIAgent.DisplayName property

* use agent name as a first value for activity display name

* Update dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/HandoffAgentExecutor.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Python: cleanup and refactoring of chat clients (#2937)

* refactoring and unifying naming schemes of internal methods of chat clients

* set tool_choice to auto

* fix for mypy

* added note on naming and fix #2951

* fix responses

* fixes in azure ai agents client

* Python: Workflow add option to visualize internal executors (#2917)

* Workflow add option to visualize internal executors

* Address Copilot comments

* Python: Fixes Run ID and Thread ID casing to align with AG-UI Typescript SDK (#2948)

* added camelCase input to run id and thread id aligning with @ag-ui/core

* fixed per copilot suggestions

* Python: Add workflow cancellation sample (#2732)

* Add workflow cancellation sample

Add sample demonstrating how to cancel a running workflow using asyncio
tasks. Shows both cancellation mid-execution and normal completion paths.
Useful for implementing timeouts, graceful shutdown, or A2A executors.

* update docstring

* .NET: Update Anthropic package to version 12.0.0 (#2914)

* Initial plan

* Update Anthropic package to version 12.0.0

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>

* Python: Add Azure Managed Redis Support with Credential Provider (#2887)

* azure redis support

* small fixes

* azure managed redis sample

* fixes

* Bump CommunityToolkit.Aspire.OllamaSharp from 13.0.0-beta.440 to 13.0.0 (#2856)

---
updated-dependencies:
- dependency-name: CommunityToolkit.Aspire.OllamaSharp
  dependency-version: 13.0.0
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump AWSSDK.Extensions.Bedrock.MEAI from 4.0.4.11 to 4.0.5 (#2853)

---
updated-dependencies:
- dependency-name: AWSSDK.Extensions.Bedrock.MEAI
  dependency-version: 4.0.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Mark Wallace <127216156+markwallace-microsoft@users.noreply.github.com>

* Bump Azure.AI.AgentServer.AgentFramework from 1.0.0-beta.4 to 1.0.0-beta.5 (#2854)

---
updated-dependencies:
- dependency-name: Azure.AI.AgentServer.AgentFramework
  dependency-version: 1.0.0-beta.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: Azure.AI.AgentServer.AgentFramework
  dependency-version: 1.0.0-beta.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>

* Python: Fix WorkflowAgent event handling and kwargs forwarding (#2946)

* Fix kwargs propagation through workflow.as_agent()

* Fix WorkflowAgent to respect AgentExecutor output_response setting

* .NET: Use GrpcEntityRunner instead of TaskEntityDispatcher (#2759)

* Use GrpcEntityRunner instead of TaskEntityDispatcher

* Pin to Durable worker 1.11.0

* Set the invocation result

* Update all Durable packages

* Update changelog, rename dispatcher to encondedEntityRequest

* Python: Bump Py version to 1.0.0b251218 for a release. Update CHANGELOG (#2968)

* Bump Py version to 1.0.0b251218 for a release. Update CHANGELOG

* update lock

* Fix formatting

* Fix ChatKit typing

* Python: Introducing Foundry Local Chat Clients (#2915)

* redo foundry local chat client

* fix mypy and spelling

* better docstring, updated sample

* fixed tests and added tests

* small sample update

* Updated package versions (#2978)

* Python: Added GitHub MCP sample with PAT (#2967)

* added github mcp sample with PAT

* addressed copilot fixes

* env fix

* Python: Preserve reasoning blocks with OpenRouter (#2950)

* Preserve reasoning blocks with OpenRouter

* Put encrypted reasoning in TextReasoningContent

* Remove unneccessary change

* Fix docs

* Support streaming

* Fix handling None in TextReasoningContent.text

* Python: Added response.created and response.in_progress event process to OpenAIBaseResponseClient (#2975)

* added response.created and response.in_progress to include response.id

* better doc string

* added tests for the new streaming event types

* Python: Introducing support for Bedrock-hosted models (Anthropic, Cohere, etc.) (#2610)

* Pushing the bedrock related changes to the new branch after addressing the review comments

* 2524 Addressed the second round review comments

* 2524 Addressed few more minor comments on the PR

* resolving the merge conflict

* 2524 resolved the uv.lock conflicts

* 2524 addressed more comments

* 2524 removed the print statement to fix the checks failure

* 2524 resolved the CI failure issues

* 2524 fixing the CI breaks

* 2524 Addressed the review comment

* 2524 resolved conflict

---------

Co-authored-by: Sunil Dutta <sunil.dutta@penske.com>
Co-authored-by: budgetboardingai <apurva.sharma31@gmail.com>

* .NET: [Durable Agents] Reliable streaming sample (#2942)

* .NET: [Durable Agents] Reliable streaming sample

* Add automated validation for new sample

* Address Copilot PR feedback

* Fix typo in README.md about agent definitions (#2634)

* Fix typo in README.md about agent definitions

* Update agent-samples/README.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Python: latency improvements (#3014)

* latency improvements

* fixed mypy, added coding standards and instructions

* slight logic improvement

* Python: Updated package versions (#3024)

* Updated package versions

* Updated changelog

* Python: add powerfx safe mode (#3028)

* add powerfx safe mode

* improved docstring and aligned env_file loading

* ensured test uses reset

* .NET: [Breaking] Introduce RunCoreAsync/RunCoreStreamingAsync delegation pattern in AIAgent (#2749)

* Initial plan

* Refactor AIAgent: Make RunAsync and RunStreamingAsync non-abstract, add RunCoreAsync and RunCoreStreamingAsync

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* Fix infinite recursion in test implementations

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* Make RunAsync and RunStreamingAsync non-virtual as requested

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* Fix DelegatingAIAgent subclasses to use RunCoreAsync/RunCoreStreamingAsync

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* Fix XML documentation references in AnonymousDelegatingAIAgent

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* Restore <see cref> tags with proper qualified signatures in AnonymousDelegatingAIAgent

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* Rollback unnecessary XML documentation changes in AnonymousDelegatingAIAgent

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* Remove pragma and update crefs to RunCoreAsync/RunCoreStreamingAsync

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* Fix EntityAgentWrapper to call base.RunCoreAsync/RunCoreStreamingAsync

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* fix compilation issues

* fix compilatio issue

* fix tests

* fix unit tests

* fix unit test

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>
Co-authored-by: SergeyMenshykh <sergemenshikh@gmail.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>

* add issue template and additional labeling (#3006)

* fix and extra int test (#3037)

* .NET: [BREAKING] Refactor ChatMessageStore methods to be similar to AIContextProvider and add filtering support (#2604)

* Refactor ChatMessageStore methods to be similar to AIContextProvider

* Fix file encoding

* Ensure that AIContextProvider messages area also persisted.

* Update formatting and seal context classes

* Improve formatting

* Remove optional messages from constructor and add unit test

* Add ChatMessageStore filtering via a decorator

* Update sample and cosmos message store to store AIContextProvider messages in right order. Fix unit tests.

* Update Workflowmessage store to use aicontext provider messages.

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* Improve xml docs messaging

* Address code review comments.

* Also notify message store on failure

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* [BREAKING] Remove unused AgentThreadMetadata (#3067)

* Remove unused AgentThreadMetadata

* Update DurableTask Changelog

* Python: Fix AzureAIClient failure when conversation history contains assistant messages (#3076)

* Fix AzureAIClient failure when conversation history contains assistant messages

* Address PR review feedback: improve docstring and test assertions

* Remove redundant cast

* Fix: Update OTLP exporter protocol conditions (#3070)

* Python: Fix ExecutorInvokedEvent and ExecutorCompletedEvent observability data (#3090)

* Fix ExecutorInvokedEvent.data mutation bug

* Fix bug related to not yielding output type

* .NET: Seal ChatClientAgentThread (#2842)

* Initial plan

* Seal ChatClientAgentThread class

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* Fix broken strands urls. (#3102)

* Fix broken strands urls.

* Fix typos

* .NET: Fix message ordering inconsistency when using AIContextProvider (#2659)

* Initial plan

* Fix message ordering inconsistency when using AIContextProvider

Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>

* Revert to original message ordering: Input, AIContextProvider, Response

Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>

* Reorder messages to ChatClient to match MessageStore order: Existing, Input, AIContextProvider

Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>

* Remove redundant test methods as existing tests already verify the behavior

Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>
Co-authored-by: Mark Wallace <127216156+markwallace-microsoft@users.noreply.github.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>

* fix: tool_choice parameter not being honored when passed to agent.run() (#3095)

* sharepoint sample fix (#3108)

* Bump versions to 1.0.0b260106 for a release. Update CHANGELOG.md (#3109)

* Bump Bedrock version to latest (#3110)

* Python: Fix MCP tool result serialization for list[TextContent] (#2523)

* Fix MCP tool result serialization for list[TextContent]

When MCP tools return results containing list[TextContent], they were
incorrectly serialized to object repr strings like:
'[<agent_framework._types.TextContent object at 0x...>]'

This fix properly extracts text content from list items by:
1. Checking if items have a 'text' attribute (TextContent)
2. Using model_dump() for items that support it
3. Falling back to str() for other types
4. Joining single items as plain text, multiple items as JSON array

Fixes #2509

* Address PR review feedback for MCP tool result serialization

- Extract serialize_content_result() to shared _utils.py
- Fix logic: use texts[0] instead of join for single item
- Add type annotation: texts: list[str] = []
- Return empty string for empty list instead of '[]'
- Move import json to file top level
- Add comprehensive unit tests for serialization

* Address PR review feedback: fix type checking and double serialization

- Add isinstance(item.text, str) check to ensure text attribute is a string
- Fix double-serialization issue by keeping model_dump results as dicts
  until final json.dumps (removes escaped JSON strings in arrays)
- Improve docstring with detailed return value documentation
- Add test for non-string text attribute handling
- Add tests for list type tool results in _events.py path

* Simplify PR: minimal changes to fix MCP tool result serialization

Addresses reviewer feedback about excessive refactoring:
- Reset _events.py to original structure
- Only add import and use serialize_content_result in one location
- All review comments addressed in serialize_content_result():
  - Added isinstance(item.text, str) check
  - Use model_dump(mode="json") to avoid double-serialization
  - Improved docstring with explicit return value documentation
  - Empty list returns "" instead of "[]"

* Refactor: Move MCP TextContent serialization to core prepare_function_call_results

Per reviewer feedback, moved the TextContent serialization logic from
ag-ui's serialize_content_result to the core package's
prepare_function_call_results function.

Changes:
- Added handling for objects with 'text' attribute (like MCP TextContent)
  in _prepare_function_call_results_as_dumpable
- Removed serialize_content_result from ag-ui/_utils.py
- Updated _events.py and _message_adapters.py to use
  prepare_function_call_results from core package
- Updated tests to match the core function's behavior

* Fix failing tests for prepare_function_call_results behavior

- test_tool_result_with_none: Update expected value to 'null' (JSON serialization of None)
- test_tool_result_with_model_dump_objects: Use Pydantic BaseModel instead of plain class

* Fix B903 linter error: Convert MockTextContent to dataclass

The ruff linter was reporting B903 (class could be dataclass or namedtuple)
for the MockTextContent test helper classes. This commit converts them to
dataclasses to satisfy the linter check.

* Python: Improve DevUI, add Context Inspector view as new tab under traces (#2742)

* Improve DevUI, add Context Inspector view as new tab under traces

* fix mypy errors

* fix: Handle stale MCP connections in DevUI executor

MCP tools can become stale when HTTP streaming responses end - the underlying
stdio streams close but `is_connected` remains True. This causes subsequent
requests to fail with `ClosedResourceError`.

Add `_ensure_mcp_connections()` to detect and reconnect stale MCP tools before
agent execution. This is a workaround for an upstream Agent Framework issue
where connection state isn't properly tracked.

Fixes MCP tools failing on second HTTP request in DevUI.

fixes  #1476 #1515 #2865

* fix #1572 report import dependency errors more clearly

* Ensure there is streaming toggle where users can select streaming vs non streaming mode in devui . Fixes .NET: [Python] DevUI tool call rendering in non-streaming mode?

* remove unused dead code

* improve ux - workflows with agents show a chat component in execution timelien, also ensure magentic final output shows correctly

* update ui build

* update devui to use instrumentation instead of tracing, other instrumentation and type/instance check fixes

* .NET: Seal factory contexts and add non JSO deserialize overloads (#3066)

* Seal factory contexts and add non JSO deserialize overloads

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Enable blank issues in issue template configuration

Need to re-enable creating blank issues

* updated templates (#3106)

* updated templates

* enabled blank and fixed triage

* made language optional and moved to the bottom for features

* Python: Streaming sample for azurefunctions (#3057)

* Streaming sample for azurefunctions

* Fixed links and sample name

* Addressed feedback

* Addressed feedback

* Fixed integration tests

* Updated test

* Python: fix(azure-ai): Fix response_format handling for structured outputs (#3114)

* fix(azure-ai): read response_format from chat_options instead of run_options

* refactor: use explicit None checks for response_format

* Fix mypy error

* Mypy fix

* Python: Bump python version to 1.0.0b260107 for a release (#3128)

* Bump python version to 1.0.0b260107 for a release

* Update changelog

* Make A2AAgent public, so that it's concrete implementation methods can be used. (#3119)

* .NET: Map additional props <-> A2A metadata (#3137)

* map additional props from agent run options to a2a request metadata

* small touches

* add unit tests for new extension methods

* Sort using

* add unit test

* add additiona unit tests

* special case json element to avoid unnecessary serialization

* Python: Fix Anthropic streaming response bugs (#3141)

* test commit identity

* fix(anthropic): fix raw_representation and finish_reason in streaming

* lint fix

* Bump AWSSDK.Extensions.Bedrock.MEAI from 4.0.5 to 4.0.5.1 (#2994)

---
updated-dependencies:
- dependency-name: AWSSDK.Extensions.Bedrock.MEAI
  dependency-version: 4.0.5.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>

* Bump Anthropic from 12.0.0 to 12.0.1 (#2993)

---
updated-dependencies:
- dependency-name: Anthropic
  dependency-version: 12.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>

* .NET: [Breaking] Prevent loss of input messages & streamed updates when resuming streaming (#2748)

* save input messages and stream updates to the continuation token to be able to use them in the last successful stream resumption call.

* Update dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentContinuationToken.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentContinuationToken.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_BackgroundResponsesTests.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentContinuationToken.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentContinuationToken.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix typo

* init continuation token from chat response

* remove unnecessary types for source generation

* remove check for continuation token passed at initial run

* remove check for continuation token pass at initial run

* centralize continuation token parsing

* update xml comments

* use readonly collection instead of enumerable

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* .NET: fix: Expose WorkflowErrorEvent as ErrorContent (#2762)

* fix: Expose WorkflowErrorEvent as ErrorContent

When hosted using .AsAgent(), Workflows were not exposing inner errors coming as Exceptions (through the WorkflowErrorEvent)

The fix is to convert their message to an ErrorContent on the way out, rather than rely on the default "empty update" to collect the raw event.

* feat: Add a way to show/suppress exception information

* Bump Microsoft.Agents.AI.Workflows from 1.0.0-preview.251125.1 to 1.0.0-preview.251219.1 (#2997)

---
updated-dependencies:
- dependency-name: Microsoft.Agents.AI.Workflows
  dependency-version: 1.0.0-preview.251219.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>
Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* .NET: Add Run overloads to expose ChatClientAgentRunOptions in IntelliSense (#3115)

* Initial plan

* Add ChatClientAgentExtensions for improved discoverability of ChatClientAgentRunOptions

Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>

* Address code review feedback - use collection expression syntax

Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>

* Apply suggestion from @westey-m

* Fix issues with Copilot implementation

* Add additional tests for structured output overloads.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>

* Python: Add tool call/result content types and update connectors and samples (#2971)

* Add new AI content types and image tool support

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Add Python content types for tool calls/results and image generation tool support

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Address review feedback for tool content and samples

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Tighten image generation typing and sample tools list

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Align image generation output typing

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Handle MCP naming, image options mapping, and connector tool content

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Allow MCP call in function approval request

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Remove raw image_generation tool remapping

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Restore Anthropic tool_use to function calls unless code execution

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Fix lint issues for hosted file docstring and MCP parsing

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Import ChatResponse types in Anthropic client

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Fix Anthropics citation type imports and MCP typing for handoff/tools

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Skip lightning tests without agentlightning and fix function call import

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* fix lint on lab package

* rebuilt anthropic parsing

* redid anthropic parsing

* typo

* updated parsing and added missing docstrings

* fix tests

* mypy fixes

* second mypy fix

* add new class to other samples

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>
Co-authored-by: eavanvalkenburg <github@vanvalkenburg.eu>

* Bump Google.GenAI from 0.6.0 to 0.9.0 (#2995)

---
updated-dependencies:
- dependency-name: Google.GenAI
  dependency-version: 0.9.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>

* Bump js-yaml from 4.1.0 to 4.1.1 in /python/packages/devui/frontend (#3123)

Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.0 to 4.1.1.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.0...4.1.1)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 4.1.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Updated package versions (#3144)

* .NET: Bump Microsoft.Agents.AI.OpenAI and Microsoft.Extensions.AI.OpenAI (#2996)

* Bump Microsoft.Agents.AI.OpenAI and Microsoft.Extensions.AI.OpenAI

Bumps Microsoft.Agents.AI.OpenAI from 1.0.0-preview.251125.1 to 1.0.0-preview.251219.1
Bumps Microsoft.Extensions.AI.OpenAI from 10.1.0-preview.1.25608.1 to 10.1.1-preview.1.25612.2

---
updated-dependencies:
- dependency-name: Microsoft.Agents.AI.OpenAI
  dependency-version: 1.0.0-preview.251219.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: Microsoft.Extensions.AI.OpenAI
  dependency-version: 10.1.1-preview.1.25612.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: Microsoft.Agents.AI.OpenAI
  dependency-version: 1.0.0-preview.251219.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: Microsoft.Extensions.AI.OpenAI
  dependency-version: 10.1.1-preview.1.25612.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* Fixed samples

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>
Co-authored-by: Mark Wallace <127216156+markwallace-microsoft@users.noreply.github.com>
Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com>

* Python: fix(ag-ui): Execute tools with approval_mode, fix shared state, code cleanup  (#3079)

* fix(ag-ui): execute tools after approval in human-in-the-loop flow

* Fix shared state bug

* Bug fix finalized

* Refactoring to clean up code

* Code cleanup

* More fixes

* More code cleanup

* Add version detection in __init__.py to ruff ignore list

* Track agent name with updates for workflow agent (#3146)

* Python: Fix AzureAIClient tool call bug for AG-UI use (#3148)

* Fiz AzureAIClient tool call bug

* Address copilot feedback

* Python: multiple bug fixes (#3150)

* fix Python: kwargs are not passed to _prepare_thread_and_messages in ChatAgent.run
Fixes #3118

* fix Python: [Bug]: model_id versus model_deployment_name is confusing in Azure AI Agents
Fixes #3147

* add types

* fixed type and docstring

* fix(anthropic): fix duplicate ToolCallStartEvent in streaming tool calls (#3051)

When processing `input_json_delta` events, the Anthropic client was
passing the tool name from the previous `tool_use` event. This caused
ag-ui's `_handle_function_call_content` to emit a `ToolCallStartEvent`
for every streaming chunk (since it triggers on `if content.name:`).

This fix changes the behavior to pass an empty string for `name` in
`input_json_delta` events, matching OpenAI's behavior where streaming
argument chunks have `name=""`. The initial `tool_use` event still
provides the tool name, so only one `ToolCallStartEvent` is emitted.

Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>

* .NET: [BREAKING] Change GetNewThread and DeserializeThread to async (#3152)

* Change GetNewThread and DeserializeThread plus ChatMessageStore and AIContextProvider Factories to async

* Merge fixes

* Fix Ollama model env var in documentation (#3156)

Signed-off-by: Dina Suehiro Jones <dina.s.jones@intel.com>

* Python: Add Pydantic request model and OpenAPI tags support to AG-UI FastAPI endpoint (#2522)

* feat(ag-ui): Add Pydantic request model and OpenAPI tags support

- Add AGUIRequest Pydantic model in _types.py with field descriptions
- Update add_agent_framework_fastapi_endpoint() to accept tags parameter
- Use AGUIRequest model for automatic validation and OpenAPI schema generation
- Export AGUIRequest and DEFAULT_TAGS in __init__.py
- Update test_endpoint.py to expect 422 for invalid requests
- Add tests for OpenAPI schema, default tags, custom tags, and validation

Benefits:
- Better API documentation with complete request schema in Swagger UI
- Automatic request validation with Pydantic
- Organized endpoints under 'AG-UI' tag instead of 'default'
- Improved developer experience and type safety

Fixes #<issue-number>

* test(ag-ui): Add test for internal error handling to achieve 100% coverage

- Add test_endpoint_internal_error_handling() to cover exception handling code
- Mock copy.deepcopy to simulate internal error during default_state processing
- Add type: ignore for FastAPI tags parameter (known pyright compatibility issue)
- Achieves 100% test coverage for _endpoint.py (previously missing lines 103-105)

* .NET: Improve resolving `AITool` from DI (#3175)

* remove localagenttoolregistry

* also give the factory method API

* Python: Fix MCPStreamableHTTPTool to use new streamable_http_client API (#3088)

* Fix MCPStreamableHTTPTool to use new streamable_http_client API with proper httpx client cleanup

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Update docstring to reflect new streamable_http_client API usage

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Refactor MCPStreamableHTTPTool to accept optional http_client parameter and delegate client creation to streamable_http_client

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Update mcp package minimum version to 1.24.0 for streamable_http_client API support

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Fix critical bugs: apply headers/timeout/sse_read_timeout when creating httpx client, add version constraint <2, and properly manage client lifecycle

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Simplify implementation: remove headers/timeout/sse_read_timeout params, remove kwargs, remove close() override per feedback

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Add back **kwargs parameter for backward compatibility (accepted but not used)

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Remove unused httpx import from test file

Note: The uv.lock file needs to be updated with 'uv sync' to reflect the mcp version constraint change (>=1.24.0,<2)

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* cicd fixes

* udpated samples with headers examples

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>
Co-authored-by: eavanvalkenburg <github@vanvalkenburg.eu>

* azureai direct a2a endpoint support (#3127)

* Python: [BREAKING]: removed display_name, renamed context_providers, middleware and AggregateContextProvider (#3139)

* removed display_name, renamed context_providers, middleware and AggregateContextProvider

* fixes

* fixed test

* testfix

* removed mistakenly put back test

* updated new test

* rename middlewares to middleware

* middleware fixes

* Python: MCP Improvements: improved connection loss behavior, pagination for loading and a param to control representation (#3154)

* pagination support (#2848) added a parse_tool_result param and connection loss (#2884)

* fix #3153

* improved connection handling

* improved logic

* Python: Add declarative workflow runtime (#2815)

* Further support for declarative python workflows

* Add tests. Clean up for typing and formatting

* Improvements and cleanup

* Typing cleanup. Improve docstrings

* Proper code in docstrings

* Fix malformed code-block directive in docstring

* Remove dead links

* PR feedback

* Address PR feedback

* Address PR feedback

* Remove sl

* Update devui frontend

* More cleanup

* Fix uv lock

* Skip Py 3.14 tests as powerfx doesn't support it

* Fix mypy error

* Fix for tool calls

* Removed stale docstring

* Fix lint

* Standardize on .NET namespaces. Revert DevUI changes (bring in later)

* Implement remaining items for Python declarative support to match dotnet

* point URL to agent, not to agentcard (#3176)

* Python: [BREAKING]: Introducing Options as TypedDict and Generic (#3140)

* WIP typeddict for options

* updated all clients and ChatAgents

* updated everything

* added ADR

* fix mypy

* proper typevar imports

* fixed import

* fixed other imports

* slight update in the sample

* updated from feedback

* fixes

* fixed missing covariants and test fixes

* fixed typing

* updated anthropic thinking config

* ruff fixes

* fixed int tests

* fix tests and mypy

* updated integration tests

* updated docstring and test fix

* improved options handling in obser

* mypy fix

* updated a host of integration tests

* fix tests

* bedrock fix

* [BREAKING] Python: Refactor orchestrations (#3023)

* Group chat refactoring Part 1; Next: HIL and handoff

* Add agent approval flow; next samples

* WIP: samples

* WIP: HIL samples

* Group chat HIL working; next: handoff

* Fix group chat tool approval sample

* WIP: refactor handoff; next handoff handling

* Handoff done; next handoff samples and concurrent and sequential

* Handoff samples, concurrent, and sequential done; next Magentic

* WIP: magentic; next test with samples + HIL

* Magentic Working; next fix all samples and tests

* Fix handoff samples; next tests

* WIP: fixing tests; some orchestration as agent samples are failing

* Group chat unit tests done

* Handoff  unit tests done

* Remove old orchestration_request_info and fix related tests

* Magentic unit tests done

* Fix samples

* Fix test

* Fix test 2

* mypy

* Address comments

* Update readme

* Address comments

* Address comments 2

* Replace display name

* Python: ADR for create/get agent API (#2618)

* ADR for create/get agent API

* Updated ADR with implementation options

* Small updates

* Updated decision outcome section

* Updated broken links

* Small updates

* Fixed merge conflicts

* Small fix

* Updated decision outcome section

* Small fixes

* Updated provider naming based on client SDK

* Add ignored parameter for CodeQL in workflow (#3204)

* Implement IReadOnlyList on InMemoryChatMessageStore (#3205)

* .NET: Make ChatMessageStore and AIContextProvider context props settable (#3196)

* Make ChatMessageStore and AIContextProvider context props setable

* Add validation to preserve non-null requirement of certain properties.

* Fix broken tests.

* Python: Add dependencies param to ag-ui FastAPI endpoint (#3191)

* Add dependencies param to ag-ui FastAPI endpoint

* Address Copilot feedback

* renamed all (#3207)

* Python: ADR for simplified get response (#3098)

* ADR for simplified get response

* updated some language, added agent option and code comparison

* small update in sample

* added workflows and expanded some points

* changed decision and number

* updated with stream=False default

* .NET: [Breaking] Rename`AgentRunResponse` and `AgentRunResponseUpdate` classes (#3197)

* rename AgentRunResponse and AgentRunResponseUpdate classes - part1

* rename varialbles, parameters, methods and tests

* rollback unnecessary changes

* .NET: [Breaking] Rename AgentRunResponseEvent and AgentRunUpdateEvent classes (#3214)

* rename AgentRunResponseEvent and AgentRunUpdateEvent classes

* rollback unnecessary changes

* Python: Create/Get Agent API for Azure V2 (#3059)

* Added get_agent method to Azure AI V2

* Small fixes

* Small fix

* Removed AzureAIAgentProvider

* Added create_agent method

* Small fixes

* Fixed code interpreter tool mapping

* Added agent provider for V2 client

* Updated response format handling

* Added provider example

* Fixed errors

* Update python/samples/getting_started/agents/azure_ai/README.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Small fix

* Updates from merge

* Resolved comments

* Resolved comments

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Python: Add more specific exceptions to Workflow (#3188)

* Add more specifc workflow exceptions

* Fix tests

* AI comments

* Misc

* Python: Added AzureAI sample for downloading code interpreter generated files (#3189)

* added azure ai code interpreter file download sample

* copilot fix suggestions

* function name fixes + readme update

* small fix

* update package versions (#3223)

Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com>

* Python: fix(core): correct FunctionResultContent ordering in WorkflowAgent.merge_updates (#3168)

* fix(core): simplify FunctionResultContent ordering in WorkflowAgent.merge_updates

* improve comment

* Fix name

* fix(workflows): rename WorkflowOutputEvent.source_executor_id to executor_id for API consistency (#3166)

* Python: fix(ag-ui): add MCP tool support for AG-UI approval flows (#3212)

* add MCP tool support for AG-UI approval flows

* use attribute in place of property

* Python: Properly configure structured outputs based on new options dict (#3213)

* Properly configure structured outputs based on new options dict

* Fix mypy

* .NET: Merge AgentRunOptions.AdditionalProperties into ChatOptions.AdditionalProperties (#3184)

* Merge AgentRunOptions.AdditionalProperties into ChatOptions.AdditionalProperties

* Fix namespace and typo.

* .NET: Update Google.GenAI to 0.11.0 and remove polyfill implementations (#3232)

* Initial plan

* Update Google.GenAI to 0.11.0 and remove polyfill files

Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>

* .NET: [BREAKING] Renamed CreateAIAgent/GetAIAgent to AsAIAgent (#3222)

* Renamed chat client extension method

* Additional renaming

* Updated documentation

* Fixed tests

* Small fix

* Small fix

* Updated DurableAIAgent and fixed integration tests (#3241)

* Python: Create/Get Agent API for Azure V1 (#3192)

* Added provider implementation for Azure AI V1

* Small fixes

* Fixed OpenAPI example

* Fixed local MCP example

* Fixed hosted MCP example

* Fixed file search sample

* Small fixes

* Resolved comments

* Doc updates

* Bump azure-core from 1.37.0 to 1.38.0 in /python (#3209)

Bumps [azure-core](https://github.com/Azure/azure-sdk-for-python) from 1.37.0 to 1.38.0.
- [Release notes](https://github.com/Azure/azure-sdk-for-python/releases)
- [Commits](https://github.com/Azure/azure-sdk-for-python/compare/azure-core_1.37.0...azure-core_1.38.0)

---
updated-dependencies:
- dependency-name: azure-core
  dependency-version: 1.38.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Python: Create/Get Agent API for OpenAI Assistants (#3208)

* Added provider implementation

* Added example with response format

* Small improvements

* Python: (AG-UI) Support service-managed thread on AG-UI  (#3136)

* added service thread support

* set service_thread_id to only supplied_thread_id

* uses raw_representation to extract the conversation_id

* removed accidental edit

* updated test to use raw_representation

* resolves copilot review feedback

* revert back StubAgent, since not used

* removed relative module import

* removed hasattr check per PR feedback

* Create/Get Agent API - fixes and example improvements (#3246)

* .NET Purview Middleware: Improve Background Job Runner Injection (#3256)

* Clean up background job dependency injection

* Fix xml documentation grammar

* Python: [BREAKING] Renamed create_agent to as_agent (#3249)

* Renamed create_agent to as_agent

* Override for as_agent

* Added override

* Python: Update package version (#3258)

* package version 260116

* removed name tags

* Python: Fixed Azure chat client for asynchronous filtering (#3260)

* Fixed Azure chat client for asynchronous filtering

* Updated test

* Python: Fixed use_agent_middleware calling private _normalize_messages (#3264)

* Fix use_agent_middleware calling private _normalize_messages

* Fixed A2A and Copilot Studio agent

* Python: Added rai_config to Azure AI agent creation (#3265)

* Add kwargs to create_agent method

* Added test for kwargs

* Addressed comment

* Added doc string

* Python: Filter conversation_id when passing kwargs to agent as tool (#3266)

* Filter conversation_id when passing kwargs to agent as tool

* Small fix

* Update python/samples/getting_started/agents/azure_ai/README.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update python/samples/getting_started/agents/openai/openai_responses_client_with_agent_as_tool.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update python/samples/getting_started/agents/azure_ai/azure_ai_with_agent_as_tool.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Bump actions/setup-dotnet from 5.0.1 to 5.1.0 (#3273)

Bumps [actions/setup-dotnet](https://github.com/actions/setup-dotnet) from 5.0.1 to 5.1.0.
- [Release notes](https://github.com/actions/setup-dotnet/releases)
- [Commits](https://github.com/actions/setup-dotnet/compare/v5.0.1...v5.1.0)

---
updated-dependencies:
- dependency-name: actions/setup-dotnet
  dependency-version: 5.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Update ignored checks in merge-gatekeeper workflow

* Python: [BREAKING] Make response_format validation errors visible to users (#3274)

* Make response_format validation errors visible to users

* Small fix

* Addressed comments

* Python: fix(declarative): Fix MCP tool connection not passed from YAML to Azure AI agent creation API (#3248)

* fix(declarative): Fix MCP tool connection not passed from YAML

* Add samples to README

* Fix mypy

* Fix mypy again

* Address PR comments

* fix #3171, ensure proper form rendering for int (#3201)

* Bump uv from 0.9.25 to 0.9.26 in /python (#3288)

Bumps [uv](https://github.com/astral-sh/uv) from 0.9.25 to 0.9.26.
- [Release notes](https://github.com/astral-sh/uv/releases)
- [Changelog](https://github.com/astral-sh/uv/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/uv/compare/0.9.25...0.9.26)

---
updated-dependencies:
- dependency-name: uv
  dependency-version: 0.9.26
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump ruff from 0.14.11 to 0.14.13 in /python (#3287)

Bumps [ruff](https://github.com/astral-sh/ruff) from 0.14.11 to 0.14.13.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.14.11...0.14.13)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.14.13
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump tar from 7.4.3 to 7.5.3 in /python/packages/devui/frontend (#3267)

Bumps [tar](https://github.com/isaacs/node-tar) from 7.4.3 to 7.5.3.
- [Release notes](https://github.com/isaacs/node-tar/releases)
- [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-tar/compare/v7.4.3...v7.5.3)

---
updated-dependencies:
- dependency-name: tar
  dependency-version: 7.5.3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* .NET: Delete sync extension methods for agent (#3291)

* Delete sync extension methods for agent

* Fix comments and obsolete attribute

* Remove more sync methods.

* Fix naming and comments.

* Fix unit tests

* Python: Fix: Add system_instructions to ChatClient LLM span tracing (#3164)

* Fix: Add system_instructions to ChatClient LLM span tracing

- Add system_instructions parameter to _capture_messages() calls in
  _trace_get_response() and _trace_get_streaming_response()
- Extract instructions from chat_options in kwargs
- Add unit tests to verify system_instructions are captured correctly

When using ChatClient with ChatOptions.instructions, the OpenTelemetry
LLM span was missing system messages in gen_ai.input.messages and the
gen_ai.system_instructions attribute was not being set.

This fix aligns the ChatClient-level tracing with the Agent-level
tracing which already correctly passes system_instructions.

Fixes #3163

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Add edge case tests for system_instructions

- Add test for empty string instructions (should not set attribute)
- Add test for list-type instructions (verify multiple items captured)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Simplify: use options.get('instructions') directly instead of kwargs.get('chat_options')

Addresses reviewer feedback:
- Removed unnecessary chat_options variable from kwargs
- Directly access instructions from the options parameter
- Updated tests to use dict syntax for options (TypedDict convention)

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* Improve PR number handling in workflow (#3302)

* Improve PR number handling in workflow

Refine PR number extraction and validation method.

* Update .github/workflows/python-test-coverage-report.yml

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Fix error message for invalid PR number

---------

Co-authored-by: Copilot <175728472+Copilot@…

* Modify failures

* Fix mypy errors

* Address comments

* Update durabletask version

* Remove event loops

* Add comment

* Fix typing for apps

---------

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Dina Suehiro Jones <dina.s.jones@intel.com>
Co-authored-by: Tao Chen <taochen@microsoft.com>
Co-authored-by: Kurt <65111699+q33566@users.noreply.github.com>
Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>
Co-authored-by: Korolev Dmitry <deagle.gross@gmail.com>
Co-authored-by: Mark Wallace <127216156+markwallace-microsoft@users.noreply.github.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>
Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>
Co-authored-by: Jose Luis Latorre Millas <joslat@gmail.com>
Co-authored-by: Jacob Alber <jaalber@microsoft.com>
Co-authored-by: Richard Ortega <richardjortega@gmail.com>
Co-authored-by: 刘邦学AI <lbbniu@gmail.com>
Co-authored-by: Stephen Toub <stoub@microsoft.com>
Co-authored-by: Nico Möller <nkm-moeller@mail.de>
Co-authored-by: Chris Gillum <cgillum@microsoft.com>
Co-authored-by: Giles Odigwe <79032838+giles17@users.noreply.github.com>
Co-authored-by: Phillip Hoff <phillip.hoff@gmail.com>
Co-authored-by: Ege Ozan Özyedek <36128615+egeozanozyedek@users.noreply.github.com>
Co-authored-by: samueljohnsiby <66901393+samueljohnsiby@users.noreply.github.com>
Co-authored-by: Evan Mattson <evan.mattson@microsoft.com>
Co-authored-by: Hao Luo <338265+howlowck@users.noreply.github.com>
Co-authored-by: Victor Dibia <chuvidi2003@gmail.com>
Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
Co-authored-by: Jacob Viau <javia@microsoft.com>
Co-authored-by: SuperKenVery <39673849+SuperKenVery@users.noreply.github.com>
Co-authored-by: Sunil Dutta <dutta.2003@gmail.com>
Co-authored-by: Sunil Dutta <sunil.dutta@penske.com>
Co-authored-by: budgetboardingai <apurva.sharma31@gmail.com>
Co-authored-by: Syrine Chelly <62653967+SyChell@users.noreply.github.com>
Co-authored-by: SergeyMenshykh <sergemenshikh@gmail.com>
Co-authored-by: westey <164392973+westey-m@users.noreply.github.com>
Co-authored-by: takanori-terai <123897708+takanori-terai@users.noreply.github.com>
Co-authored-by: claude89757 <138977524+claude89757@users.noreply.github.com>
Co-authored-by: Gavin Aguiar <80794152+gavin-aguiar@users.noreply.github.com>
Co-authored-by: Sukeesh <vsukeeshbabu@gmail.com>
Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>
Co-authored-by: eavanvalkenburg <github@vanvalkenburg.eu>
Co-authored-by: Ao Chen <chenao3220@gmail.com>
Co-authored-by: Dina Suehiro Jones <dina.s.jones@intel.com>
Co-authored-by: eoindoherty1 <eoindoherty@microsoft.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Darren Cohen <39422044+dargilco@users.noreply.github.com>
Co-authored-by: Ben Thomas <ben.thomas@microsoft.com>
Co-authored-by: alliscode <bentho@microsoft.com>
Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com>
Co-authored-by: Shyju Krishnankutty <connectshyju@gmail.com>
This commit is contained in:
Laveesh Rohra
2026-01-27 13:08:05 -08:00
committed by GitHub
Unverified
parent c860385a8c
commit 787becfc9f
96 changed files with 11536 additions and 3032 deletions
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) Microsoft Corporation.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE
+31
View File
@@ -0,0 +1,31 @@
# Get Started with Microsoft Agent Framework Durable Task
[![PyPI](https://img.shields.io/pypi/v/agent-framework-durabletask)](https://pypi.org/project/agent-framework-durabletask/)
Please install this package via pip:
```bash
pip install agent-framework-durabletask --pre
```
## Durable Task Integration
The durable task integration lets you host Microsoft Agent Framework agents using the [Durable Task](https://github.com/microsoft/durabletask-python) framework so they can persist state, replay conversation history, and recover from failures automatically.
### Basic Usage Example
```python
from durabletask import TaskHubGrpcWorker
from agent_framework.azure import DurableAIAgentWorker
# Create the worker
with TaskHubGrpcWorker(...) as worker:
# Register the agent worker wrapper
agent_worker = DurableAIAgentWorker(worker)
# Register the agent
agent_worker.add_agent(my_agent)
```
For more details, review the Python [README](https://github.com/microsoft/agent-framework/tree/main/python/README.md) and the samples directory.
@@ -0,0 +1,108 @@
# Copyright (c) Microsoft. All rights reserved.
"""Durable Task integration for Microsoft Agent Framework."""
import importlib.metadata
from ._callbacks import AgentCallbackContext, AgentResponseCallbackProtocol
from ._client import DurableAIAgentClient
from ._constants import (
DEFAULT_MAX_POLL_RETRIES,
DEFAULT_POLL_INTERVAL_SECONDS,
MIMETYPE_APPLICATION_JSON,
MIMETYPE_TEXT_PLAIN,
REQUEST_RESPONSE_FORMAT_JSON,
REQUEST_RESPONSE_FORMAT_TEXT,
THREAD_ID_FIELD,
THREAD_ID_HEADER,
WAIT_FOR_RESPONSE_FIELD,
WAIT_FOR_RESPONSE_HEADER,
ApiResponseFields,
ContentTypes,
DurableStateFields,
)
from ._durable_agent_state import (
DurableAgentState,
DurableAgentStateContent,
DurableAgentStateData,
DurableAgentStateDataContent,
DurableAgentStateEntry,
DurableAgentStateEntryJsonType,
DurableAgentStateErrorContent,
DurableAgentStateFunctionCallContent,
DurableAgentStateFunctionResultContent,
DurableAgentStateHostedFileContent,
DurableAgentStateHostedVectorStoreContent,
DurableAgentStateMessage,
DurableAgentStateRequest,
DurableAgentStateResponse,
DurableAgentStateTextContent,
DurableAgentStateTextReasoningContent,
DurableAgentStateUnknownContent,
DurableAgentStateUriContent,
DurableAgentStateUsage,
DurableAgentStateUsageContent,
)
from ._entities import AgentEntity, AgentEntityStateProviderMixin
from ._executors import DurableAgentExecutor
from ._models import AgentSessionId, DurableAgentThread, RunRequest
from ._orchestration_context import DurableAIAgentOrchestrationContext
from ._response_utils import ensure_response_format, load_agent_response
from ._shim import DurableAIAgent
from ._worker import DurableAIAgentWorker
try:
__version__ = importlib.metadata.version(__name__)
except importlib.metadata.PackageNotFoundError:
__version__ = "0.0.0" # Fallback for development mode
__all__ = [
"DEFAULT_MAX_POLL_RETRIES",
"DEFAULT_POLL_INTERVAL_SECONDS",
"MIMETYPE_APPLICATION_JSON",
"MIMETYPE_TEXT_PLAIN",
"REQUEST_RESPONSE_FORMAT_JSON",
"REQUEST_RESPONSE_FORMAT_TEXT",
"THREAD_ID_FIELD",
"THREAD_ID_HEADER",
"WAIT_FOR_RESPONSE_FIELD",
"WAIT_FOR_RESPONSE_HEADER",
"AgentCallbackContext",
"AgentEntity",
"AgentEntityStateProviderMixin",
"AgentResponseCallbackProtocol",
"AgentSessionId",
"ApiResponseFields",
"ContentTypes",
"DurableAIAgent",
"DurableAIAgentClient",
"DurableAIAgentOrchestrationContext",
"DurableAIAgentWorker",
"DurableAgentExecutor",
"DurableAgentState",
"DurableAgentStateContent",
"DurableAgentStateData",
"DurableAgentStateDataContent",
"DurableAgentStateEntry",
"DurableAgentStateEntryJsonType",
"DurableAgentStateErrorContent",
"DurableAgentStateFunctionCallContent",
"DurableAgentStateFunctionResultContent",
"DurableAgentStateHostedFileContent",
"DurableAgentStateHostedVectorStoreContent",
"DurableAgentStateMessage",
"DurableAgentStateRequest",
"DurableAgentStateResponse",
"DurableAgentStateTextContent",
"DurableAgentStateTextReasoningContent",
"DurableAgentStateUnknownContent",
"DurableAgentStateUriContent",
"DurableAgentStateUsage",
"DurableAgentStateUsageContent",
"DurableAgentThread",
"DurableStateFields",
"RunRequest",
"__version__",
"ensure_response_format",
"load_agent_response",
]
@@ -0,0 +1,40 @@
# Copyright (c) Microsoft. All rights reserved.
"""Callback interfaces for Durable Agent executions.
This module enables callers of AgentFunctionApp to supply streaming and final-response callbacks that are
invoked during durable entity execution.
"""
from dataclasses import dataclass
from typing import Protocol
from agent_framework import AgentResponse, AgentResponseUpdate
@dataclass(frozen=True)
class AgentCallbackContext:
"""Context supplied to callback invocations."""
agent_name: str
correlation_id: str
thread_id: str | None = None
request_message: str | None = None
class AgentResponseCallbackProtocol(Protocol):
"""Protocol describing the callbacks invoked during agent execution."""
async def on_streaming_response_update(
self,
update: AgentResponseUpdate,
context: AgentCallbackContext,
) -> None:
"""Handle a streaming response update emitted by the agent."""
async def on_agent_response(
self,
response: AgentResponse,
context: AgentCallbackContext,
) -> None:
"""Handle the final agent response."""
@@ -0,0 +1,90 @@
# Copyright (c) Microsoft. All rights reserved.
"""Client wrapper for Durable Task Agent Framework.
This module provides the DurableAIAgentClient class for external clients to interact
with durable agents via gRPC.
"""
from __future__ import annotations
from agent_framework import AgentResponse, get_logger
from durabletask.client import TaskHubGrpcClient
from ._constants import DEFAULT_MAX_POLL_RETRIES, DEFAULT_POLL_INTERVAL_SECONDS
from ._executors import ClientAgentExecutor
from ._shim import DurableAgentProvider, DurableAIAgent
logger = get_logger("agent_framework.durabletask.client")
class DurableAIAgentClient(DurableAgentProvider[AgentResponse]):
"""Client wrapper for interacting with durable agents externally.
This class wraps a durabletask TaskHubGrpcClient and provides a convenient
interface for retrieving and executing durable agents from external contexts.
Example:
```python
from durabletask import TaskHubGrpcClient
from agent_framework.azure import DurableAIAgentClient
# Create the underlying client
client = TaskHubGrpcClient(host_address="localhost:4001")
# Wrap it with the agent client
agent_client = DurableAIAgentClient(client)
# Get an agent reference
agent = agent_client.get_agent("assistant")
# Run the agent (synchronous call that waits for completion)
response = agent.run("Hello, how are you?")
print(response.text)
```
"""
def __init__(
self,
client: TaskHubGrpcClient,
max_poll_retries: int = DEFAULT_MAX_POLL_RETRIES,
poll_interval_seconds: float = DEFAULT_POLL_INTERVAL_SECONDS,
):
"""Initialize the client wrapper.
Args:
client: The durabletask client instance to wrap
max_poll_retries: Maximum polling attempts when waiting for responses
poll_interval_seconds: Delay in seconds between polling attempts
"""
self._client = client
# Validate and set polling parameters
self.max_poll_retries = max(1, max_poll_retries)
self.poll_interval_seconds = (
poll_interval_seconds if poll_interval_seconds > 0 else DEFAULT_POLL_INTERVAL_SECONDS
)
self._executor = ClientAgentExecutor(self._client, self.max_poll_retries, self.poll_interval_seconds)
logger.debug("[DurableAIAgentClient] Initialized with client type: %s", type(client).__name__)
def get_agent(self, agent_name: str) -> DurableAIAgent[AgentResponse]:
"""Retrieve a DurableAIAgent shim for the specified agent.
This method returns a proxy object that can be used to execute the agent.
The actual agent must be registered on a worker with the same name.
Args:
agent_name: Name of the agent to retrieve (without the dafx- prefix)
Returns:
DurableAIAgent instance that can be used to run the agent
Note:
This method does not validate that the agent exists. Validation
will occur when the agent is executed. If the entity doesn't exist,
the execution will fail with an appropriate error.
"""
logger.debug("[DurableAIAgentClient] Creating agent proxy for: %s", agent_name)
return DurableAIAgent(self._executor, agent_name)
@@ -0,0 +1,130 @@
# Copyright (c) Microsoft. All rights reserved.
"""Constants for Azure Functions Agent Framework integration.
This module contains:
- Runtime configuration constants (polling, MIME types, headers)
- JSON field name mappings for camelCase (JSON) ↔ snake_case (Python) serialization
For serialization constants, use the DurableStateFields, ContentTypes, and EntryTypes classes
to ensure consistent field naming between to_dict() and from_dict() methods.
"""
from typing import Final
# Supported request/response formats and MIME types
REQUEST_RESPONSE_FORMAT_JSON: str = "json"
REQUEST_RESPONSE_FORMAT_TEXT: str = "text"
MIMETYPE_APPLICATION_JSON: str = "application/json"
MIMETYPE_TEXT_PLAIN: str = "text/plain"
# Field and header names
THREAD_ID_FIELD: str = "thread_id"
THREAD_ID_HEADER: str = "x-ms-thread-id"
WAIT_FOR_RESPONSE_FIELD: str = "wait_for_response"
WAIT_FOR_RESPONSE_HEADER: str = "x-ms-wait-for-response"
# Polling configuration
DEFAULT_MAX_POLL_RETRIES: int = 30
DEFAULT_POLL_INTERVAL_SECONDS: float = 1.0
# =============================================================================
# JSON Field Name Constants for Durable Agent State Serialization
# =============================================================================
# These constants ensure consistent camelCase field names in JSON serialization.
# Use these in both to_dict() and from_dict() methods to prevent mismatches.
# NOTE: Changing these constants is a breaking change and might require a schema version bump.
class DurableStateFields:
"""JSON field name constants for durable agent state serialization.
All field names are in camelCase to match the JSON schema.
Use these constants in both to_dict() and from_dict() methods.
"""
# Schema-level fields
SCHEMA_VERSION: Final[str] = "schemaVersion"
DATA: Final[str] = "data"
# Entry discriminator
TYPE_DISCRIMINATOR: Final[str] = "$type"
# Internal field names
JSON_TYPE: Final[str] = "json_type"
TYPE_INTERNAL: Final[str] = "type"
# Common entry fields
CORRELATION_ID: Final[str] = "correlationId"
CREATED_AT: Final[str] = "createdAt"
MESSAGES: Final[str] = "messages"
EXTENSION_DATA: Final[str] = "extensionData"
# Request-specific fields
RESPONSE_TYPE: Final[str] = "responseType"
RESPONSE_SCHEMA: Final[str] = "responseSchema"
ORCHESTRATION_ID: Final[str] = "orchestrationId"
# Response-specific fields
USAGE: Final[str] = "usage"
# Message fields
ROLE: Final[str] = "role"
CONTENTS: Final[str] = "contents"
AUTHOR_NAME: Final[str] = "authorName"
# Content fields
TEXT: Final[str] = "text"
URI: Final[str] = "uri"
MEDIA_TYPE: Final[str] = "mediaType"
MESSAGE: Final[str] = "message"
ERROR_CODE: Final[str] = "errorCode"
DETAILS: Final[str] = "details"
CALL_ID: Final[str] = "callId"
NAME: Final[str] = "name"
ARGUMENTS: Final[str] = "arguments"
RESULT: Final[str] = "result"
FILE_ID: Final[str] = "fileId"
VECTOR_STORE_ID: Final[str] = "vectorStoreId"
CONTENT: Final[str] = "content"
# Usage fields (noqa: S105 - these are JSON field names, not passwords)
INPUT_TOKEN_COUNT: Final[str] = "inputTokenCount" # noqa: S105
OUTPUT_TOKEN_COUNT: Final[str] = "outputTokenCount" # noqa: S105
TOTAL_TOKEN_COUNT: Final[str] = "totalTokenCount" # noqa: S105
# History field
CONVERSATION_HISTORY: Final[str] = "conversationHistory"
class ContentTypes:
"""Content type discriminator values for the $type field.
These values are used in the JSON $type field to identify content types.
"""
TEXT: Final[str] = "text"
DATA: Final[str] = "data"
ERROR: Final[str] = "error"
FUNCTION_CALL: Final[str] = "functionCall"
FUNCTION_RESULT: Final[str] = "functionResult"
HOSTED_FILE: Final[str] = "hostedFile"
HOSTED_VECTOR_STORE: Final[str] = "hostedVectorStore"
REASONING: Final[str] = "reasoning"
URI: Final[str] = "uri"
USAGE: Final[str] = "usage"
UNKNOWN: Final[str] = "unknown"
class ApiResponseFields:
"""Field names for HTTP API responses (not part of persisted schema).
These are used in try_get_agent_response() for backward compatibility
with the HTTP API response format.
"""
CONTENT: Final[str] = "content"
MESSAGE_COUNT: Final[str] = "message_count"
CORRELATION_ID: Final[str] = "correlationId"
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,347 @@
# Copyright (c) Microsoft. All rights reserved.
"""Durable Task entity implementations for Microsoft Agent Framework."""
from __future__ import annotations
import inspect
from collections.abc import AsyncIterable
from typing import Any, cast
from agent_framework import (
AgentProtocol,
AgentResponse,
AgentResponseUpdate,
ChatMessage,
Content,
Role,
get_logger,
)
from durabletask.entities import DurableEntity
from ._callbacks import AgentCallbackContext, AgentResponseCallbackProtocol
from ._durable_agent_state import (
DurableAgentState,
DurableAgentStateEntry,
DurableAgentStateRequest,
DurableAgentStateResponse,
)
from ._models import RunRequest
logger = get_logger("agent_framework.durabletask.entities")
class AgentEntityStateProviderMixin:
"""Mixin implementing durable agent state caching + (de)serialization + persistence.
Concrete classes must implement:
- _get_state_dict(): fetch raw persisted state dict (default should be {})
- _set_state_dict(): persist raw state dict
- _get_thread_id_from_entity(): fetch the thread ID from the underlying context
"""
_state_cache: DurableAgentState | None = None
def _get_state_dict(self) -> dict[str, Any]:
raise NotImplementedError
def _set_state_dict(self, state: dict[str, Any]) -> None:
raise NotImplementedError
def _get_thread_id_from_entity(self) -> str:
raise NotImplementedError
@property
def thread_id(self) -> str:
return self._get_thread_id_from_entity()
@property
def state(self) -> DurableAgentState:
if self._state_cache is None:
raw_state = self._get_state_dict()
self._state_cache = DurableAgentState.from_dict(raw_state) if raw_state else DurableAgentState()
return self._state_cache
@state.setter
def state(self, value: DurableAgentState) -> None:
self._state_cache = value
self.persist_state()
def persist_state(self) -> None:
"""Persist the current state to the underlying storage provider."""
if self._state_cache is None:
self._state_cache = DurableAgentState()
self._set_state_dict(self._state_cache.to_dict())
def reset(self) -> None:
"""Clear conversation history by resetting state to a fresh DurableAgentState."""
self._state_cache = DurableAgentState()
self.persist_state()
logger.debug("[AgentEntityStateProviderMixin.reset] State reset complete")
class AgentEntity:
"""Platform-agnostic agent execution logic.
This class encapsulates the core logic for executing an agent within a durable entity context.
"""
agent: AgentProtocol
callback: AgentResponseCallbackProtocol | None
def __init__(
self,
agent: AgentProtocol,
callback: AgentResponseCallbackProtocol | None = None,
*,
state_provider: AgentEntityStateProviderMixin,
) -> None:
self.agent = agent
self.callback = callback
self._state_provider = state_provider
logger.debug("[AgentEntity] Initialized with agent type: %s", type(agent).__name__)
@property
def state(self) -> DurableAgentState:
return self._state_provider.state
@state.setter
def state(self, value: DurableAgentState) -> None:
self._state_provider.state = value
def persist_state(self) -> None:
self._state_provider.persist_state()
def reset(self) -> None:
self._state_provider.reset()
def _is_error_response(self, entry: DurableAgentStateEntry) -> bool:
"""Check if a conversation history entry is an error response."""
if isinstance(entry, DurableAgentStateResponse):
return entry.is_error
return False
async def run(
self,
request: RunRequest | dict[str, Any] | str,
) -> AgentResponse:
"""Execute the agent with a message."""
if isinstance(request, str):
run_request = RunRequest.from_json(request)
elif isinstance(request, dict):
run_request = RunRequest.from_dict(request)
else:
run_request = request
message = run_request.message
thread_id = self._state_provider.thread_id
correlation_id = run_request.correlation_id
if not thread_id:
raise ValueError("Entity State Provider must provide a thread_id")
options: dict[str, Any] = dict(run_request.options)
options.setdefault("response_format", run_request.response_format)
if not run_request.enable_tool_calls:
options.setdefault("tools", None)
logger.debug("[AgentEntity.run] Received ThreadId %s Message: %s", thread_id, run_request)
state_request = DurableAgentStateRequest.from_run_request(run_request)
self.state.data.conversation_history.append(state_request)
try:
chat_messages: list[ChatMessage] = [
m.to_chat_message()
for entry in self.state.data.conversation_history
if not self._is_error_response(entry)
for m in entry.messages
]
run_kwargs: dict[str, Any] = {"messages": chat_messages, "options": options}
agent_run_response: AgentResponse = await self._invoke_agent(
run_kwargs=run_kwargs,
correlation_id=correlation_id,
thread_id=thread_id,
request_message=message,
)
state_response = DurableAgentStateResponse.from_run_response(correlation_id, agent_run_response)
self.state.data.conversation_history.append(state_response)
self.persist_state()
return agent_run_response
except Exception as exc:
logger.exception("[AgentEntity.run] Agent execution failed.")
error_message = ChatMessage(
role=Role.ASSISTANT, contents=[Content.from_error(message=str(exc), error_code=type(exc).__name__)]
)
error_response = AgentResponse(messages=[error_message])
error_state_response = DurableAgentStateResponse.from_run_response(correlation_id, error_response)
error_state_response.is_error = True
self.state.data.conversation_history.append(error_state_response)
self.persist_state()
return error_response
async def _invoke_agent(
self,
run_kwargs: dict[str, Any],
correlation_id: str,
thread_id: str,
request_message: str,
) -> AgentResponse:
"""Execute the agent, preferring streaming when available."""
callback_context: AgentCallbackContext | None = None
if self.callback is not None:
callback_context = self._build_callback_context(
correlation_id=correlation_id,
thread_id=thread_id,
request_message=request_message,
)
run_stream_callable = getattr(self.agent, "run_stream", None)
if callable(run_stream_callable):
try:
stream_candidate = run_stream_callable(**run_kwargs)
if inspect.isawaitable(stream_candidate):
stream_candidate = await stream_candidate
return await self._consume_stream(
stream=cast(AsyncIterable[AgentResponseUpdate], stream_candidate),
callback_context=callback_context,
)
except TypeError as type_error:
if "__aiter__" not in str(type_error):
raise
logger.debug(
"run_stream returned a non-async result; falling back to run(): %s",
type_error,
)
except Exception as stream_error:
logger.warning(
"run_stream failed; falling back to run(): %s",
stream_error,
exc_info=True,
)
else:
logger.debug("Agent does not expose run_stream; falling back to run().")
agent_run_response = await self._invoke_non_stream(run_kwargs)
await self._notify_final_response(agent_run_response, callback_context)
return agent_run_response
async def _consume_stream(
self,
stream: AsyncIterable[AgentResponseUpdate],
callback_context: AgentCallbackContext | None = None,
) -> AgentResponse:
"""Consume streaming responses and build the final AgentResponse."""
updates: list[AgentResponseUpdate] = []
async for update in stream:
updates.append(update)
await self._notify_stream_update(update, callback_context)
if updates:
response = AgentResponse.from_agent_run_response_updates(updates)
else:
logger.debug("[AgentEntity] No streaming updates received; creating empty response")
response = AgentResponse(messages=[])
await self._notify_final_response(response, callback_context)
return response
async def _invoke_non_stream(self, run_kwargs: dict[str, Any]) -> AgentResponse:
"""Invoke the agent without streaming support."""
run_callable = getattr(self.agent, "run", None)
if run_callable is None or not callable(run_callable):
raise AttributeError("Agent does not implement run() method")
result = run_callable(**run_kwargs)
if inspect.isawaitable(result):
result = await result
if not isinstance(result, AgentResponse):
raise TypeError(f"Agent run() must return an AgentResponse instance; received {type(result).__name__}")
return result
async def _notify_stream_update(
self,
update: AgentResponseUpdate,
context: AgentCallbackContext | None,
) -> None:
"""Invoke the streaming callback if one is registered."""
if self.callback is None or context is None:
return
try:
callback_result = self.callback.on_streaming_response_update(update, context)
if inspect.isawaitable(callback_result):
await callback_result
except Exception as exc:
logger.warning(
"[AgentEntity] Streaming callback raised an exception: %s",
exc,
exc_info=True,
)
async def _notify_final_response(
self,
response: AgentResponse,
context: AgentCallbackContext | None,
) -> None:
"""Invoke the final response callback if one is registered."""
if self.callback is None or context is None:
return
try:
callback_result = self.callback.on_agent_response(response, context)
if inspect.isawaitable(callback_result):
await callback_result
except Exception as exc:
logger.warning(
"[AgentEntity] Response callback raised an exception: %s",
exc,
exc_info=True,
)
def _build_callback_context(
self,
correlation_id: str,
thread_id: str,
request_message: str,
) -> AgentCallbackContext:
"""Create the callback context provided to consumers."""
agent_name = getattr(self.agent, "name", None) or type(self.agent).__name__
return AgentCallbackContext(
agent_name=agent_name,
correlation_id=correlation_id,
thread_id=thread_id,
request_message=request_message,
)
class DurableTaskEntityStateProvider(DurableEntity, AgentEntityStateProviderMixin):
"""DurableTask Durable Entity state provider for AgentEntity.
This class utilizes the Durable Entity context from `durabletask` package
to get and set the state of the agent entity.
"""
def __init__(self) -> None:
super().__init__()
def _get_state_dict(self) -> dict[str, Any]:
raw = self.get_state(dict, default={})
return cast(dict[str, Any], raw)
def _set_state_dict(self, state: dict[str, Any]) -> None:
self.set_state(state)
def _get_thread_id_from_entity(self) -> str:
return self.entity_context.entity_id.key
@@ -0,0 +1,516 @@
# Copyright (c) Microsoft. All rights reserved.
"""Provider strategies for Durable Agent execution.
These classes are internal execution strategies used by the DurableAIAgent shim.
They are intentionally separate from the public client/orchestration APIs to keep
only `get_agent` exposed to consumers. Executors implement the execution contract
and are injected into the shim.
"""
from __future__ import annotations
import time
import uuid
from abc import ABC, abstractmethod
from datetime import datetime, timezone
from typing import Any, Generic, TypeVar
from agent_framework import AgentResponse, AgentThread, ChatMessage, Content, Role, get_logger
from durabletask.client import TaskHubGrpcClient
from durabletask.entities import EntityInstanceId
from durabletask.task import CompletableTask, CompositeTask, OrchestrationContext, Task
from pydantic import BaseModel
from ._constants import DEFAULT_MAX_POLL_RETRIES, DEFAULT_POLL_INTERVAL_SECONDS
from ._durable_agent_state import DurableAgentState
from ._models import AgentSessionId, DurableAgentThread, RunRequest
from ._response_utils import ensure_response_format, load_agent_response
logger = get_logger("agent_framework.durabletask.executors")
# TypeVar for the task type returned by executors
TaskT = TypeVar("TaskT")
class DurableAgentTask(CompositeTask[AgentResponse], CompletableTask[AgentResponse]):
"""A custom Task that wraps entity calls and provides typed AgentResponse results.
This task wraps the underlying entity call task and intercepts its completion
to convert the raw result into a typed AgentResponse object.
When yielded in an orchestration, this task returns an AgentResponse:
response: AgentResponse = yield durable_agent_task
"""
def __init__(
self,
entity_task: CompletableTask[Any],
response_format: type[BaseModel] | None,
correlation_id: str,
):
"""Initialize the DurableAgentTask.
Args:
entity_task: The underlying entity call task
response_format: Optional Pydantic model for response parsing
correlation_id: Correlation ID for logging
"""
self._response_format = response_format
self._correlation_id = correlation_id
super().__init__([entity_task]) # type: ignore
def on_child_completed(self, task: Task[Any]) -> None:
"""Handle completion of the underlying entity task.
Parameters
----------
task : Task
The entity call task that just completed
"""
if self.is_complete:
return
if task.is_failed:
# Propagate the failure - pass the original exception directly
self.fail("call_entity Task failed", task.get_exception())
return
# Task succeeded - transform the raw result
raw_result = task.get_result()
logger.debug(
"[DurableAgentTask] Converting raw result for correlation_id %s",
self._correlation_id,
)
try:
response = load_agent_response(raw_result)
if self._response_format is not None:
ensure_response_format(
self._response_format,
self._correlation_id,
response,
)
# Set the typed AgentResponse as this task's result
self.complete(response)
except Exception as ex:
err_msg = "[DurableAgentTask] Failed to convert result for correlation_id: " + self._correlation_id
logger.exception(err_msg)
self.fail(err_msg, ex)
class DurableAgentExecutor(ABC, Generic[TaskT]):
"""Abstract base class for durable agent execution strategies.
Type Parameters:
TaskT: The task type returned by this executor
"""
@abstractmethod
def run_durable_agent(
self,
agent_name: str,
run_request: RunRequest,
thread: AgentThread | None = None,
) -> TaskT:
"""Execute the durable agent.
Returns:
TaskT: The task type specific to this executor implementation
"""
raise NotImplementedError
def get_new_thread(self, agent_name: str, **kwargs: Any) -> DurableAgentThread:
"""Create a new DurableAgentThread with random session ID."""
session_id = self._create_session_id(agent_name)
return DurableAgentThread.from_session_id(session_id, **kwargs)
def _create_session_id(
self,
agent_name: str,
thread: AgentThread | None = None,
) -> AgentSessionId:
"""Create the AgentSessionId for the execution."""
if isinstance(thread, DurableAgentThread) and thread.session_id is not None:
return thread.session_id
# Create new session ID - either no thread provided or it's a regular AgentThread
key = self.generate_unique_id()
return AgentSessionId(name=agent_name, key=key)
def generate_unique_id(self) -> str:
"""Generate a new Unique ID."""
return uuid.uuid4().hex
def get_run_request(
self,
message: str,
*,
options: dict[str, Any] | None = None,
) -> RunRequest:
"""Create a RunRequest from message and options."""
correlation_id = self.generate_unique_id()
# Create a copy to avoid modifying the caller's dict
opts = dict(options) if options else {}
# Extract and REMOVE known keys from options copy
response_format = opts.pop("response_format", None)
enable_tool_calls = opts.pop("enable_tool_calls", True)
wait_for_response = opts.pop("wait_for_response", True)
return RunRequest(
message=message,
response_format=response_format,
enable_tool_calls=enable_tool_calls,
wait_for_response=wait_for_response,
correlation_id=correlation_id,
options=opts,
)
def _create_acceptance_response(self, correlation_id: str) -> AgentResponse:
"""Create an acceptance response for fire-and-forget mode.
Args:
correlation_id: Correlation ID for tracking the request
Returns:
AgentResponse: Acceptance response with correlation ID
"""
acceptance_message = ChatMessage(
role=Role.SYSTEM,
contents=[
Content.from_text(
f"Request accepted for processing (correlation_id: {correlation_id}). "
f"Agent is executing in the background. "
f"Retrieve response via your configured streaming or callback mechanism."
)
],
)
return AgentResponse(
messages=[acceptance_message],
created_at=datetime.now(timezone.utc).isoformat(),
)
class ClientAgentExecutor(DurableAgentExecutor[AgentResponse]):
"""Execution strategy for external clients.
Note: Returns AgentResponse directly since the execution
is blocking until response is available via polling
as per the design of TaskHubGrpcClient.
"""
def __init__(
self,
client: TaskHubGrpcClient,
max_poll_retries: int = DEFAULT_MAX_POLL_RETRIES,
poll_interval_seconds: float = DEFAULT_POLL_INTERVAL_SECONDS,
):
self._client = client
self.max_poll_retries = max_poll_retries
self.poll_interval_seconds = poll_interval_seconds
def run_durable_agent(
self,
agent_name: str,
run_request: RunRequest,
thread: AgentThread | None = None,
) -> AgentResponse:
"""Execute the agent via the durabletask client.
Signals the agent entity with a message request, then polls the entity
state to retrieve the response once processing is complete.
Note: This is a blocking/synchronous operation (in line with how
TaskHubGrpcClient works) that polls until a response is available or
timeout occurs.
Args:
agent_name: Name of the agent to execute
run_request: The run request containing message and optional response format
thread: Optional conversation thread (creates new if not provided)
Returns:
AgentResponse: The agent's response after execution completes, or an immediate
acknowledgement if wait_for_response is False
"""
# Signal the entity with the request
entity_id = self._signal_agent_entity(agent_name, run_request, thread)
# If fire-and-forget mode, return immediately without polling
if not run_request.wait_for_response:
logger.info(
"[ClientAgentExecutor] Fire-and-forget mode: request signaled (correlation: %s)",
run_request.correlation_id,
)
return self._create_acceptance_response(run_request.correlation_id)
# Poll for the response
agent_response = self._poll_for_agent_response(entity_id, run_request.correlation_id)
# Handle and return the result
return self._handle_agent_response(agent_response, run_request.response_format, run_request.correlation_id)
def _signal_agent_entity(
self,
agent_name: str,
run_request: RunRequest,
thread: AgentThread | None,
) -> EntityInstanceId:
"""Signal the agent entity with a run request.
Args:
agent_name: Name of the agent to execute
run_request: The run request containing message and optional response format
thread: Optional conversation thread
Returns:
entity_id
"""
# Get or create session ID
session_id = self._create_session_id(agent_name, thread)
# Create the entity ID
entity_id = EntityInstanceId(
entity=session_id.entity_name,
key=session_id.key,
)
logger.debug(
"[ClientAgentExecutor] Signaling entity '%s' (session: %s, correlation: %s)",
agent_name,
session_id,
run_request.correlation_id,
)
self._client.signal_entity(entity_id, "run", run_request.to_dict())
return entity_id
def _poll_for_agent_response(
self,
entity_id: EntityInstanceId,
correlation_id: str,
) -> AgentResponse | None:
"""Poll the entity for a response with retries.
Args:
entity_id: Entity instance identifier
correlation_id: Correlation ID to track the request
Returns:
The agent response if found, None if timeout occurs
"""
agent_response = None
for attempt in range(1, self.max_poll_retries + 1):
# Initial sleep is intentional - give the entity time to process before first poll
time.sleep(self.poll_interval_seconds)
agent_response = self._poll_entity_for_response(entity_id, correlation_id)
if agent_response is not None:
logger.info(
"[ClientAgentExecutor] Found response (attempt %d/%d, correlation: %s)",
attempt,
self.max_poll_retries,
correlation_id,
)
break
logger.debug(
"[ClientAgentExecutor] Response not ready (attempt %d/%d)",
attempt,
self.max_poll_retries,
)
return agent_response
def _handle_agent_response(
self,
agent_response: AgentResponse | None,
response_format: type[BaseModel] | None,
correlation_id: str,
) -> AgentResponse:
"""Handle the agent response or create an error response.
Args:
agent_response: The response from polling, or None if timeout
response_format: Optional response format for validation
correlation_id: Correlation ID for logging
Returns:
AgentResponse with either the agent's response or an error message
"""
if agent_response is not None:
try:
# Validate response format if specified
if response_format is not None:
ensure_response_format(
response_format,
correlation_id,
agent_response,
)
return agent_response
except Exception as e:
logger.exception(
"[ClientAgentExecutor] Error converting response for correlation: %s",
correlation_id,
)
error_message = ChatMessage(
role=Role.SYSTEM,
contents=[
Content.from_error(
message=f"Error processing agent response: {e}",
error_code="response_processing_error",
)
],
)
else:
logger.warning(
"[ClientAgentExecutor] Timeout after %d attempts (correlation: %s)",
self.max_poll_retries,
correlation_id,
)
error_message = ChatMessage(
role=Role.SYSTEM,
contents=[
Content.from_error(
message=f"Timeout waiting for agent response after {self.max_poll_retries} attempts",
error_code="response_timeout",
)
],
)
return AgentResponse(
messages=[error_message],
created_at=datetime.now(timezone.utc).isoformat(),
)
def _poll_entity_for_response(
self,
entity_id: EntityInstanceId,
correlation_id: str,
) -> AgentResponse | None:
"""Poll the entity state for a response matching the correlation ID.
Args:
entity_id: Entity instance identifier
correlation_id: Correlation ID to search for
Returns:
Response AgentResponse, None otherwise
"""
try:
entity_metadata = self._client.get_entity(entity_id, include_state=True)
if entity_metadata is None:
return None
state_json = entity_metadata.get_state()
if not state_json:
return None
state = DurableAgentState.from_json(state_json)
# Use the helper method to get response by correlation ID
return state.try_get_agent_response(correlation_id)
except Exception as e:
logger.warning(
"[ClientAgentExecutor] Error reading entity state: %s",
e,
)
return None
class OrchestrationAgentExecutor(DurableAgentExecutor[DurableAgentTask]):
"""Execution strategy for orchestrations (sync/yield)."""
def __init__(self, context: OrchestrationContext):
self._context = context
logger.debug("[OrchestrationAgentExecutor] Initialized")
def generate_unique_id(self) -> str:
"""Create a new UUID that is safe for replay within an orchestration or operation."""
return self._context.new_uuid()
def get_run_request(
self,
message: str,
*,
options: dict[str, Any] | None = None,
) -> RunRequest:
"""Get the current run request from the orchestration context.
Returns:
RunRequest: The current run request
"""
request = super().get_run_request(
message,
options=options,
)
request.orchestration_id = self._context.instance_id
return request
def run_durable_agent(
self,
agent_name: str,
run_request: RunRequest,
thread: AgentThread | None = None,
) -> DurableAgentTask:
"""Execute the agent via orchestration context.
Calls the agent entity and returns a DurableAgentTask that can be yielded
in orchestrations to wait for the entity's response.
Args:
agent_name: Name of the agent to execute
run_request: The run request containing message and optional response format
thread: Optional conversation thread (creates new if not provided)
Returns:
DurableAgentTask: A task wrapping the entity call that yields AgentResponse
"""
# Resolve session
session_id = self._create_session_id(agent_name, thread)
# Create the entity ID
entity_id = EntityInstanceId(
entity=session_id.entity_name,
key=session_id.key,
)
logger.debug(
"[OrchestrationAgentExecutor] correlation_id: %s entity_id: %s session_id: %s",
run_request.correlation_id,
entity_id,
session_id,
)
# Branch based on wait_for_response
if not run_request.wait_for_response:
# Fire-and-forget mode: signal entity and return pre-completed task
logger.info(
"[OrchestrationAgentExecutor] Fire-and-forget mode: signaling entity (correlation: %s)",
run_request.correlation_id,
)
self._context.signal_entity(entity_id, "run", run_request.to_dict())
# Create a pre-completed task with acceptance response
acceptance_response = self._create_acceptance_response(run_request.correlation_id)
entity_task: CompletableTask[AgentResponse] = CompletableTask() # type: ignore[no-untyped-call]
entity_task.complete(acceptance_response)
else:
# Blocking mode: call entity and wait for response
entity_task = self._context.call_entity(entity_id, "run", run_request.to_dict()) # type: ignore
# Wrap in DurableAgentTask for response transformation
return DurableAgentTask(
entity_task=entity_task,
response_format=run_request.response_format,
correlation_id=run_request.correlation_id,
)
@@ -0,0 +1,340 @@
# Copyright (c) Microsoft. All rights reserved.
"""Data models for Durable Agent Framework.
This module defines the request and response models used by the framework.
"""
from __future__ import annotations
import inspect
import json
import uuid
from collections.abc import MutableMapping
from dataclasses import dataclass, field
from datetime import datetime, timezone
from importlib import import_module
from typing import TYPE_CHECKING, Any, cast
from agent_framework import AgentThread, Role
from ._constants import REQUEST_RESPONSE_FORMAT_TEXT
if TYPE_CHECKING: # pragma: no cover - type checking imports only
from pydantic import BaseModel
_PydanticBaseModel: type[BaseModel] | None
try:
from pydantic import BaseModel as _RuntimeBaseModel
except ImportError: # pragma: no cover - optional dependency
_PydanticBaseModel = None
else:
_PydanticBaseModel = _RuntimeBaseModel
def serialize_response_format(response_format: type[BaseModel] | None) -> Any:
"""Serialize response format for transport across durable function boundaries."""
if response_format is None:
return None
if _PydanticBaseModel is None:
raise RuntimeError("pydantic is required to use structured response formats")
if not inspect.isclass(response_format) or not issubclass(response_format, _PydanticBaseModel):
raise TypeError("response_format must be a Pydantic BaseModel type")
return {
"__response_schema_type__": "pydantic_model",
"module": response_format.__module__,
"qualname": response_format.__qualname__,
}
def _deserialize_response_format(response_format: Any) -> type[BaseModel] | None:
"""Deserialize response format back into actionable type if possible."""
if response_format is None:
return None
if (
_PydanticBaseModel is not None
and inspect.isclass(response_format)
and issubclass(response_format, _PydanticBaseModel)
):
return response_format
if not isinstance(response_format, dict):
return None
response_dict = cast(dict[str, Any], response_format)
if response_dict.get("__response_schema_type__") != "pydantic_model":
return None
module_name = response_dict.get("module")
qualname = response_dict.get("qualname")
if not module_name or not qualname:
return None
try:
module = import_module(module_name)
except ImportError: # pragma: no cover - user provided module missing
return None
attr: Any = module
for part in qualname.split("."):
try:
attr = getattr(attr, part)
except AttributeError: # pragma: no cover - invalid qualname
return None
if _PydanticBaseModel is not None and inspect.isclass(attr) and issubclass(attr, _PydanticBaseModel):
return attr
return None
@dataclass
class RunRequest:
"""Represents a request to run an agent with a specific message and configuration.
Attributes:
message: The message to send to the agent
request_response_format: The desired response format (e.g., "text" or "json")
role: The role of the message sender (user, system, or assistant)
response_format: Optional Pydantic BaseModel type describing the structured response format
enable_tool_calls: Whether to enable tool calls for this request
wait_for_response: If True (default), caller will wait for agent response. If False,
returns immediately after signaling (fire-and-forget mode)
correlation_id: Correlation ID for tracking the response to this specific request
created_at: Optional timestamp when the request was created
orchestration_id: Optional ID of the orchestration that initiated this request
options: Optional options dictionary forwarded to the agent
"""
message: str
request_response_format: str
correlation_id: str
role: Role = Role.USER
response_format: type[BaseModel] | None = None
enable_tool_calls: bool = True
wait_for_response: bool = True
created_at: datetime | None = None
orchestration_id: str | None = None
options: dict[str, Any] = field(default_factory=lambda: {})
def __init__(
self,
message: str,
correlation_id: str,
request_response_format: str = REQUEST_RESPONSE_FORMAT_TEXT,
role: Role | str | None = Role.USER,
response_format: type[BaseModel] | None = None,
enable_tool_calls: bool = True,
wait_for_response: bool = True,
created_at: datetime | None = None,
orchestration_id: str | None = None,
options: dict[str, Any] | None = None,
) -> None:
self.message = message
self.correlation_id = correlation_id
self.role = self.coerce_role(role)
self.response_format = response_format
self.request_response_format = request_response_format
self.enable_tool_calls = enable_tool_calls
self.wait_for_response = wait_for_response
self.created_at = created_at if created_at is not None else datetime.now(tz=timezone.utc)
self.orchestration_id = orchestration_id
self.options = options if options is not None else {}
@staticmethod
def coerce_role(value: Role | str | None) -> Role:
"""Normalize various role representations into a Role instance."""
if isinstance(value, Role):
return value
if isinstance(value, str):
normalized = value.strip()
if not normalized:
return Role.USER
return Role(value=normalized.lower())
return Role.USER
def to_dict(self) -> dict[str, Any]:
"""Convert to dictionary for JSON serialization."""
result = {
"message": self.message,
"enable_tool_calls": self.enable_tool_calls,
"wait_for_response": self.wait_for_response,
"role": self.role.value,
"request_response_format": self.request_response_format,
"correlationId": self.correlation_id,
"options": self.options,
}
if self.response_format:
result["response_format"] = serialize_response_format(self.response_format)
if self.created_at:
result["created_at"] = self.created_at.isoformat()
if self.orchestration_id:
result["orchestrationId"] = self.orchestration_id
return result
@classmethod
def from_json(cls, data: str) -> RunRequest:
"""Create RunRequest from JSON string."""
try:
dict_data = json.loads(data)
except json.JSONDecodeError as e:
raise ValueError("The durable agent state is not valid JSON.") from e
return cls.from_dict(dict_data)
@classmethod
def from_dict(cls, data: dict[str, Any]) -> RunRequest:
"""Create RunRequest from dictionary."""
created_at = data.get("created_at")
if isinstance(created_at, str):
try:
created_at = datetime.fromisoformat(created_at)
except ValueError:
created_at = None
correlation_id = data.get("correlationId")
if not correlation_id:
raise ValueError("correlationId is required in RunRequest data")
options = data.get("options")
return cls(
message=data.get("message", ""),
correlation_id=correlation_id,
request_response_format=data.get("request_response_format", REQUEST_RESPONSE_FORMAT_TEXT),
role=cls.coerce_role(data.get("role")),
response_format=_deserialize_response_format(data.get("response_format")),
wait_for_response=data.get("wait_for_response", True),
enable_tool_calls=data.get("enable_tool_calls", True),
created_at=created_at,
orchestration_id=data.get("orchestrationId"),
options=cast(dict[str, Any], options) if isinstance(options, dict) else {},
)
@dataclass
class AgentSessionId:
"""Represents an agent session identifier (name + key)."""
name: str
key: str
ENTITY_NAME_PREFIX: str = "dafx-"
@staticmethod
def to_entity_name(name: str) -> str:
return f"{AgentSessionId.ENTITY_NAME_PREFIX}{name}"
@staticmethod
def with_random_key(name: str) -> AgentSessionId:
return AgentSessionId(name=name, key=uuid.uuid4().hex)
@property
def entity_name(self) -> str:
return self.to_entity_name(self.name)
def __str__(self) -> str:
return f"@{self.name}@{self.key}"
def __repr__(self) -> str:
return f"AgentSessionId(name='{self.name}', key='{self.key}')"
@staticmethod
def parse(session_id_string: str, agent_name: str | None = None) -> AgentSessionId:
"""Parses a string representation of an agent session ID.
Args:
session_id_string: A string in the form @name@key, or a plain key string
when agent_name is provided.
agent_name: Optional agent name to use instead of parsing from the string.
If provided, only the key portion is extracted from session_id_string
(for @name@key format) or the entire string is used as the key
(for plain strings).
Returns:
AgentSessionId instance
Raises:
ValueError: If the string format is invalid and agent_name is not provided
"""
# Check if string is in @name@key format
if session_id_string.startswith("@") and "@" in session_id_string[1:]:
parts = session_id_string[1:].split("@", 1)
name = agent_name if agent_name is not None else parts[0]
return AgentSessionId(name=name, key=parts[1])
# Plain string format - only valid when agent_name is provided
if agent_name is not None:
return AgentSessionId(name=agent_name, key=session_id_string)
raise ValueError(f"Invalid agent session ID format: {session_id_string}")
class DurableAgentThread(AgentThread):
"""Durable agent thread that tracks the owning :class:`AgentSessionId`."""
_SERIALIZED_SESSION_ID_KEY = "durable_session_id"
def __init__(
self,
*,
session_id: AgentSessionId | None = None,
**kwargs: Any,
) -> None:
super().__init__(**kwargs)
self._session_id: AgentSessionId | None = session_id
@property
def session_id(self) -> AgentSessionId | None:
return self._session_id
@session_id.setter
def session_id(self, value: AgentSessionId | None) -> None:
self._session_id = value
@classmethod
def from_session_id(
cls,
session_id: AgentSessionId,
**kwargs: Any,
) -> DurableAgentThread:
return cls(session_id=session_id, **kwargs)
async def serialize(self, **kwargs: Any) -> dict[str, Any]:
state = await super().serialize(**kwargs)
if self._session_id is not None:
state[self._SERIALIZED_SESSION_ID_KEY] = str(self._session_id)
return state
@classmethod
async def deserialize(
cls,
serialized_thread_state: MutableMapping[str, Any],
*,
message_store: Any = None,
**kwargs: Any,
) -> DurableAgentThread:
state_payload = dict(serialized_thread_state)
session_id_value = state_payload.pop(cls._SERIALIZED_SESSION_ID_KEY, None)
thread = await super().deserialize(
state_payload,
message_store=message_store,
**kwargs,
)
if not isinstance(thread, DurableAgentThread):
raise TypeError("Deserialized thread is not a DurableAgentThread instance")
if session_id_value is None:
return thread
if not isinstance(session_id_value, str):
raise ValueError("durable_session_id must be a string when present in serialized state")
thread.session_id = AgentSessionId.parse(session_id_value)
return thread
@@ -0,0 +1,75 @@
# Copyright (c) Microsoft. All rights reserved.
"""Orchestration context wrapper for Durable Task Agent Framework.
This module provides the DurableAIAgentOrchestrationContext class for use inside
orchestration functions to interact with durable agents.
"""
from __future__ import annotations
from agent_framework import get_logger
from durabletask.task import OrchestrationContext
from ._executors import DurableAgentTask, OrchestrationAgentExecutor
from ._shim import DurableAgentProvider, DurableAIAgent
logger = get_logger("agent_framework.durabletask.orchestration_context")
class DurableAIAgentOrchestrationContext(DurableAgentProvider[DurableAgentTask]):
"""Orchestration context wrapper for interacting with durable agents internally.
This class wraps a durabletask OrchestrationContext and provides a convenient
interface for retrieving and executing durable agents from within orchestration
functions.
Example:
```python
from durabletask import Orchestration
from agent_framework.azure import DurableAIAgentOrchestrationContext
def my_orchestration(context: OrchestrationContext):
# Wrap the context
agent_context = DurableAIAgentOrchestrationContext(context)
# Get an agent reference
agent = agent_context.get_agent("assistant")
# Run the agent (returns a Task to be yielded)
result = yield agent.run("Hello, how are you?")
return result.text
```
"""
def __init__(self, context: OrchestrationContext):
"""Initialize the orchestration context wrapper.
Args:
context: The durabletask orchestration context to wrap
"""
self._context = context
self._executor = OrchestrationAgentExecutor(self._context)
logger.debug("[DurableAIAgentOrchestrationContext] Initialized")
def get_agent(self, agent_name: str) -> DurableAIAgent[DurableAgentTask]:
"""Retrieve a DurableAIAgent shim for the specified agent.
This method returns a proxy object that can be used to execute the agent
within an orchestration. The agent's run() method will return a Task that
must be yielded.
Args:
agent_name: Name of the agent to retrieve (without the dafx- prefix)
Returns:
DurableAIAgent instance that can be used to run the agent
Note:
Validation is deferred to execution time. The entity must be registered
on a worker with the name f"dafx-{agent_name}".
"""
logger.debug("[DurableAIAgentOrchestrationContext] Creating agent proxy for: %s", agent_name)
return DurableAIAgent(self._executor, agent_name)
@@ -0,0 +1,72 @@
# Copyright (c) Microsoft. All rights reserved.
"""Shared utilities for handling AgentResponse parsing and validation."""
from typing import Any
from agent_framework import AgentResponse, get_logger
from pydantic import BaseModel
logger = get_logger("agent_framework.durabletask.response_utils")
def load_agent_response(agent_response: AgentResponse | dict[str, Any] | None) -> AgentResponse:
"""Convert raw payloads into AgentResponse instance.
Args:
agent_response: The response to convert, can be an AgentResponse, dict, or None
Returns:
AgentResponse: The converted response object
Raises:
ValueError: If agent_response is None
TypeError: If agent_response is an unsupported type
"""
if agent_response is None:
raise ValueError("agent_response cannot be None")
logger.debug("[load_agent_response] Loading agent response of type: %s", type(agent_response))
if isinstance(agent_response, AgentResponse):
return agent_response
if isinstance(agent_response, dict):
logger.debug("[load_agent_response] Converting dict payload using AgentResponse.from_dict")
return AgentResponse.from_dict(agent_response)
raise TypeError(f"Unsupported type for agent_response: {type(agent_response)}")
def ensure_response_format(
response_format: type[BaseModel] | None,
correlation_id: str,
response: AgentResponse,
) -> None:
"""Ensure the AgentResponse value is parsed into the expected response_format.
This function modifies the response in-place by parsing its value attribute
into the specified Pydantic model format.
Args:
response_format: Optional Pydantic model class to parse the response value into
correlation_id: Correlation ID for logging purposes
response: The AgentResponse object to validate and parse
Raises:
ValueError: If response_format is specified but response.value cannot be parsed
"""
if response_format is not None and not isinstance(response.value, response_format):
response.try_parse_value(response_format)
# Validate that parsing succeeded
if not isinstance(response.value, response_format):
raise ValueError(
f"Response value could not be parsed into required format {response_format.__name__} "
f"for correlation_id {correlation_id}"
)
logger.debug(
"[ensure_response_format] Loaded AgentResponse.value for correlation_id %s with type: %s",
correlation_id,
type(response.value).__name__,
)
@@ -0,0 +1,177 @@
# Copyright (c) Microsoft. All rights reserved.
"""Durable Agent Shim for Durable Task Framework.
This module provides the DurableAIAgent shim that implements AgentProtocol
and provides a consistent interface for both Client and Orchestration contexts.
The actual execution is delegated to the context-specific providers.
"""
from __future__ import annotations
from abc import ABC, abstractmethod
from collections.abc import AsyncIterator
from typing import Any, Generic, TypeVar
from agent_framework import AgentProtocol, AgentResponseUpdate, AgentThread, ChatMessage
from ._executors import DurableAgentExecutor
from ._models import DurableAgentThread
# TypeVar for the task type returned by executors
# Covariant because TaskT only appears in return positions (output)
TaskT = TypeVar("TaskT", covariant=True)
class DurableAgentProvider(ABC, Generic[TaskT]):
"""Abstract provider for constructing durable agent proxies.
Implemented by context-specific wrappers (client/orchestration) to return a
`DurableAIAgent` shim backed by their respective `DurableAgentExecutor`
implementation, ensuring a consistent `get_agent` entry point regardless of
execution context.
"""
@abstractmethod
def get_agent(self, agent_name: str) -> DurableAIAgent[TaskT]:
"""Retrieve a DurableAIAgent shim for the specified agent.
Args:
agent_name: Name of the agent to retrieve
Returns:
DurableAIAgent instance that can be used to run the agent
Raises:
NotImplementedError: Must be implemented by subclasses
"""
raise NotImplementedError("Subclasses must implement get_agent()")
class DurableAIAgent(AgentProtocol, Generic[TaskT]):
"""A durable agent proxy that delegates execution to the provider.
This class implements AgentProtocol but with one critical difference:
- AgentProtocol.run() returns a Coroutine (async, must await)
- DurableAIAgent.run() returns TaskT (sync Task object - must yield
or the AgentResponse directly in the case of TaskHubGrpcClient)
This represents fundamentally different execution models but maintains the same
interface contract for all other properties and methods.
The underlying provider determines how execution occurs (entity calls, HTTP requests, etc.)
and what type of Task object is returned.
Type Parameters:
TaskT: The task type returned by this agent (e.g., AgentResponse, DurableAgentTask, AgentTask)
"""
id: str
name: str
display_name: str
description: str | None
def __init__(self, executor: DurableAgentExecutor[TaskT], name: str, *, agent_id: str | None = None):
"""Initialize the shim with a provider and agent name.
Args:
executor: The execution provider (Client or OrchestrationContext)
name: The name of the agent to execute
agent_id: Optional unique identifier for the agent (defaults to name)
"""
self._executor = executor
self.name = name # pyright: ignore[reportIncompatibleVariableOverride]
self.id = agent_id if agent_id is not None else name
self.display_name = name
self.description = f"Durable agent proxy for {name}"
def run( # type: ignore[override]
self,
messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None,
*,
thread: AgentThread | None = None,
options: dict[str, Any] | None = None,
) -> TaskT:
"""Execute the agent via the injected provider.
Args:
messages: The message(s) to send to the agent
thread: Optional agent thread for conversation context
options: Optional options dictionary. Supported keys include
``response_format``, ``enable_tool_calls``, and ``wait_for_response``.
Additional keys are forwarded to the agent execution.
Note:
This method overrides AgentProtocol.run() with a different return type:
- AgentProtocol.run() returns Coroutine[Any, Any, AgentResponse] (async)
- DurableAIAgent.run() returns TaskT (Task object for yielding)
This is intentional to support orchestration contexts that use yield patterns
instead of async/await patterns.
Returns:
TaskT: The task type specific to the executor
Raises:
ValueError: If wait_for_response=False is used in an unsupported context
"""
message_str = self._normalize_messages(messages)
run_request = self._executor.get_run_request(
message=message_str,
options=options,
)
return self._executor.run_durable_agent(
agent_name=self.name,
run_request=run_request,
thread=thread,
)
def run_stream( # type: ignore[override]
self,
messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None,
*,
thread: AgentThread | None = None,
**kwargs: Any,
) -> AsyncIterator[AgentResponseUpdate]:
"""Run the agent with streaming (not supported for durable agents).
Args:
messages: The message(s) to send to the agent
thread: Optional agent thread for conversation context
**kwargs: Additional arguments
Raises:
NotImplementedError: Streaming is not supported for durable agents
"""
raise NotImplementedError("Streaming is not supported for durable agents")
def get_new_thread(self, **kwargs: Any) -> DurableAgentThread:
"""Create a new agent thread via the provider."""
return self._executor.get_new_thread(self.name, **kwargs)
def _normalize_messages(self, messages: str | ChatMessage | list[str] | list[ChatMessage] | None) -> str:
"""Convert supported message inputs to a single string.
Args:
messages: The messages to normalize
Returns:
A single string representation of the messages
"""
if messages is None:
return ""
if isinstance(messages, str):
return messages
if isinstance(messages, ChatMessage):
return messages.text or ""
if isinstance(messages, list):
if not messages:
return ""
first_item = messages[0]
if isinstance(first_item, str):
return "\n".join(messages) # type: ignore[arg-type]
# List of ChatMessage
return "\n".join([msg.text or "" for msg in messages]) # type: ignore[union-attr]
return ""
@@ -0,0 +1,200 @@
# Copyright (c) Microsoft. All rights reserved.
"""Worker wrapper for Durable Task Agent Framework.
This module provides the DurableAIAgentWorker class that wraps a durabletask worker
and enables registration of agents as durable entities.
"""
from __future__ import annotations
import asyncio
from typing import Any
from agent_framework import AgentProtocol, get_logger
from durabletask.worker import TaskHubGrpcWorker
from ._callbacks import AgentResponseCallbackProtocol
from ._entities import AgentEntity, DurableTaskEntityStateProvider
logger = get_logger("agent_framework.durabletask.worker")
class DurableAIAgentWorker:
"""Wrapper for durabletask worker that enables agent registration.
This class wraps an existing TaskHubGrpcWorker instance and provides
a convenient interface for registering agents as durable entities.
Example:
```python
from durabletask import TaskHubGrpcWorker
from agent_framework import ChatAgent
from agent_framework.azure import DurableAIAgentWorker
# Create the underlying worker
worker = TaskHubGrpcWorker(host_address="localhost:4001")
# Wrap it with the agent worker
agent_worker = DurableAIAgentWorker(worker)
# Register agents
my_agent = ChatAgent(chat_client=client, name="assistant")
agent_worker.add_agent(my_agent)
# Start the worker
worker.start()
```
"""
def __init__(
self,
worker: TaskHubGrpcWorker,
callback: AgentResponseCallbackProtocol | None = None,
):
"""Initialize the worker wrapper.
Args:
worker: The durabletask worker instance to wrap
callback: Optional callback for agent response notifications
"""
self._worker = worker
self._callback = callback
self._registered_agents: dict[str, AgentProtocol] = {}
logger.debug("[DurableAIAgentWorker] Initialized with worker type: %s", type(worker).__name__)
def add_agent(
self,
agent: AgentProtocol,
callback: AgentResponseCallbackProtocol | None = None,
) -> None:
"""Register an agent with the worker.
This method creates a durable entity class for the agent and registers
it with the underlying durabletask worker. The entity will be accessible
by the name "dafx-{agent_name}".
Args:
agent: The agent to register (must have a name)
callback: Optional callback for this specific agent (overrides worker-level callback)
Raises:
ValueError: If the agent doesn't have a name or is already registered
"""
agent_name = agent.name
if not agent_name:
raise ValueError("Agent must have a name to be registered")
if agent_name in self._registered_agents:
raise ValueError(f"Agent '{agent_name}' is already registered")
logger.info("[DurableAIAgentWorker] Registering agent: %s as entity: dafx-%s", agent_name, agent_name)
# Store the agent reference
self._registered_agents[agent_name] = agent
# Use agent-specific callback if provided, otherwise use worker-level callback
effective_callback = callback or self._callback
# Create a configured entity class using the factory
entity_class = self.__create_agent_entity(agent, effective_callback)
# Register the entity class with the worker
# The worker.add_entity method takes a class
entity_registered: str = self._worker.add_entity(entity_class) # pyright: ignore[reportUnknownMemberType]
logger.debug(
"[DurableAIAgentWorker] Successfully registered entity class %s for agent: %s",
entity_registered,
agent_name,
)
def start(self) -> None:
"""Start the worker to begin processing tasks.
Note:
This method delegates to the underlying worker's start method.
The worker will block until stopped.
"""
logger.info("[DurableAIAgentWorker] Starting worker with %d registered agents", len(self._registered_agents))
self._worker.start() # type: ignore[no-untyped-call]
def stop(self) -> None:
"""Stop the worker gracefully.
Note:
This method delegates to the underlying worker's stop method.
"""
logger.info("[DurableAIAgentWorker] Stopping worker")
self._worker.stop() # type: ignore[no-untyped-call]
@property
def registered_agent_names(self) -> list[str]:
"""Get the names of all registered agents.
Returns:
List of agent names (without the dafx- prefix)
"""
return list(self._registered_agents.keys())
def __create_agent_entity(
self,
agent: AgentProtocol,
callback: AgentResponseCallbackProtocol | None = None,
) -> type[DurableTaskEntityStateProvider]:
"""Factory function to create a DurableEntity class configured with an agent.
This factory creates a new class that combines the entity state provider
with the agent execution logic. Each agent gets its own entity class.
Args:
agent: The agent instance to wrap
callback: Optional callback for agent responses
Returns:
A new DurableEntity subclass configured for this agent
"""
agent_name = agent.name or type(agent).__name__
entity_name = f"dafx-{agent_name}"
class ConfiguredAgentEntity(DurableTaskEntityStateProvider):
"""Durable entity configured with a specific agent instance."""
def __init__(self) -> None:
super().__init__()
# Create the AgentEntity with this state provider
self._agent_entity = AgentEntity(
agent=agent,
callback=callback,
state_provider=self,
)
logger.debug(
"[ConfiguredAgentEntity] Initialized entity for agent: %s (entity name: %s)",
agent_name,
entity_name,
)
def run(self, request: Any) -> Any:
"""Handle run requests from clients or orchestrations.
Args:
request: RunRequest as dict or string
Returns:
AgentResponse as dict
"""
logger.debug("[ConfiguredAgentEntity.run] Executing agent: %s", agent_name)
response = asyncio.run(self._agent_entity.run(request))
return response.to_dict()
def reset(self) -> None:
"""Reset the agent's conversation history."""
logger.debug("[ConfiguredAgentEntity.reset] Resetting agent: %s", agent_name)
self._agent_entity.reset()
# Set the entity name to match the prefixed agent name
# This is used by durabletask to register the entity
ConfiguredAgentEntity.__name__ = entity_name
ConfiguredAgentEntity.__qualname__ = entity_name
return ConfiguredAgentEntity
+101
View File
@@ -0,0 +1,101 @@
[project]
name = "agent-framework-durabletask"
description = "Durable Task integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "0.0.2b260126"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
urls.release_notes = "https://github.com/microsoft/agent-framework/releases?q=tag%3Apython-1&expanded=true"
urls.issues = "https://github.com/microsoft/agent-framework/issues"
classifiers = [
"License :: OSI Approved :: MIT License",
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Typing :: Typed",
]
dependencies = [
"agent-framework-core",
"durabletask>=1.3.0",
"durabletask-azuremanaged>=1.3.0"
]
[dependency-groups]
dev = [
"types-python-dateutil>=2.9.0",
]
[tool.uv]
prerelease = "if-necessary-or-explicit"
environments = [
"sys_platform == 'darwin'",
"sys_platform == 'linux'",
"sys_platform == 'win32'"
]
[tool.uv-dynamic-versioning]
fallback-version = "0.0.0"
[tool.pytest.ini_options]
testpaths = 'tests'
addopts = "-ra -q -r fEX"
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"
filterwarnings = [
"ignore:Support for class-based `config` is deprecated:DeprecationWarning:pydantic.*"
]
timeout = 120
markers = [
"integration: marks tests as integration tests",
"integration_test: marks tests as integration tests (alternative marker)",
"sample: marks tests as sample tests",
"requires_azure_openai: marks tests that require Azure OpenAI",
"requires_dts: marks tests that require Durable Task Scheduler",
"requires_redis: marks tests that require Redis"
]
[tool.ruff]
extend = "../../pyproject.toml"
[tool.coverage.run]
omit = [
"**/__init__.py"
]
[tool.pyright]
extends = "../../pyproject.toml"
[tool.mypy]
plugins = ['pydantic.mypy']
strict = true
python_version = "3.10"
ignore_missing_imports = true
disallow_untyped_defs = true
no_implicit_optional = true
check_untyped_defs = true
warn_return_any = true
show_error_codes = true
warn_unused_ignores = false
disallow_incomplete_defs = true
disallow_untyped_decorators = true
[tool.bandit]
targets = ["agent_framework_durabletask"]
exclude_dirs = ["tests"]
[tool.poe]
executor.type = "uv"
include = "../../shared_tasks.toml"
[tool.poe.tasks]
mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_durabletask"
test = "pytest --cov=agent_framework_durabletask --cov-report=term-missing:skip-covered tests"
[build-system]
requires = ["flit-core >= 3.11,<4.0"]
build-backend = "flit_core.buildapi"
@@ -0,0 +1,17 @@
# Azure OpenAI Configuration
AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com/
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME=your-deployment-name
# Optional: Use Azure CLI authentication if not provided
# AZURE_OPENAI_API_KEY=your-api-key
# Durable Task Scheduler Configuration
ENDPOINT=http://localhost:8080
TASKHUB=default
# Redis Configuration (for streaming tests)
REDIS_CONNECTION_STRING=redis://localhost:6379
REDIS_STREAM_TTL_MINUTES=10
# Integration Test Control
# Set to 'true' to enable integration tests
RUN_INTEGRATION_TESTS=true
@@ -0,0 +1,111 @@
# Sample Integration Tests
Integration tests that validate the Durable Agent Framework samples by running them against a Durable Task Scheduler (DTS) instance.
## Setup
### 1. Create `.env` file
Copy `.env.example` to `.env` and fill in your Azure credentials:
```bash
cp .env.example .env
```
Required variables:
- `AZURE_OPENAI_ENDPOINT`
- `AZURE_OPENAI_CHAT_DEPLOYMENT_NAME`
- `AZURE_OPENAI_API_KEY` (optional if using Azure CLI authentication)
- `RUN_INTEGRATION_TESTS` (set to `true`)
- `ENDPOINT` (default: http://localhost:8080)
- `TASKHUB` (default: default)
Optional variables (for streaming tests):
- `REDIS_CONNECTION_STRING` (default: redis://localhost:6379)
- `REDIS_STREAM_TTL_MINUTES` (default: 10)
### 2. Start required services
**Durable Task Scheduler:**
```bash
docker run -d --name dts-emulator -p 8080:8080 -p 8082:8082 mcr.microsoft.com/dts/dts-emulator:latest
```
- Port 8080: gRPC endpoint (used by tests)
- Port 8082: Web dashboard (optional, for monitoring)
**Redis (for streaming tests):**
```bash
docker run -d --name redis -p 6379:6379 redis:latest
```
- Port 6379: Redis server endpoint
## Running Tests
The tests automatically start and stop worker processes for each sample.
### Run all sample tests
```bash
uv run pytest packages/durabletask/tests/integration_tests -v
```
### Run specific sample
```bash
uv run pytest packages/durabletask/tests/integration_tests/test_01_single_agent.py -v
```
### Run with verbose output
```bash
uv run pytest packages/durabletask/tests/integration_tests -sv
```
## How It Works
Each test file uses pytest markers to automatically configure and start the worker process:
```python
pytestmark = [
pytest.mark.sample("03_single_agent_streaming"),
pytest.mark.integration_test,
pytest.mark.requires_azure_openai,
pytest.mark.requires_dts,
pytest.mark.requires_redis,
]
```
## Troubleshooting
**Tests are skipped:**
Ensure `RUN_INTEGRATION_TESTS=true` is set in your `.env` file.
**DTS connection failed:**
Check that the DTS emulator container is running: `docker ps | grep dts-emulator`
**Redis connection failed:**
Check that Redis is running: `docker ps | grep redis`
**Missing environment variables:**
Ensure your `.env` file contains all required variables from `.env.example`.
**Tests timeout:**
Check that Azure OpenAI credentials are valid and the service is accessible.
If you see "DTS emulator is not available":
- Ensure Docker container is running: `docker ps | grep dts-emulator`
- Check port 8080 is not in use by another process
- Restart the container if needed
### Azure OpenAI Errors
If you see authentication or deployment errors:
- Verify your `AZURE_OPENAI_ENDPOINT` is correct
- Confirm `AZURE_OPENAI_CHAT_DEPLOYMENT_NAME` matches your deployment
- If using API key, check `AZURE_OPENAI_API_KEY` is valid
- If using Azure CLI, ensure you're logged in: `az login`
## CI/CD
For automated testing in CI/CD pipelines:
1. Use Docker Compose to start DTS emulator
2. Set environment variables via CI/CD secrets
3. Run tests with appropriate markers: `pytest -m integration_test`
@@ -0,0 +1,234 @@
# Copyright (c) Microsoft. All rights reserved.
"""Pytest configuration and fixtures for durabletask integration tests."""
import asyncio
import logging
import os
import subprocess
import sys
import time
import uuid
from collections.abc import Generator
from pathlib import Path
from typing import Any, cast
import pytest
import redis.asyncio as aioredis
from dotenv import load_dotenv
from durabletask.azuremanaged.client import DurableTaskSchedulerClient
# Add the integration_tests directory to the path so testutils can be imported
sys.path.insert(0, str(Path(__file__).parent))
# Load environment variables from .env file
load_dotenv(Path(__file__).parent / ".env")
# Configure logging to reduce noise during tests
logging.basicConfig(level=logging.WARNING)
def _get_dts_endpoint() -> str:
"""Get the DTS endpoint from environment or use default."""
return os.getenv("ENDPOINT", "http://localhost:8080")
def _check_dts_available(endpoint: str | None = None) -> bool:
"""Check if DTS emulator is available at the given endpoint."""
try:
resolved_endpoint: str = _get_dts_endpoint() if endpoint is None else endpoint
DurableTaskSchedulerClient(
host_address=resolved_endpoint,
secure_channel=False,
taskhub="test",
token_credential=None,
)
return True
except Exception:
return False
def _check_redis_available() -> bool:
"""Check if Redis is available at the default connection string."""
try:
async def test_connection() -> bool:
redis_url = os.getenv("REDIS_CONNECTION_STRING", "redis://localhost:6379")
try:
client = aioredis.from_url(redis_url, socket_timeout=2) # type: ignore[reportUnknownMemberType]
await client.ping() # type: ignore[reportUnknownMemberType]
await client.aclose() # type: ignore[reportUnknownMemberType]
return True
except Exception:
return False
return asyncio.run(test_connection())
except Exception:
return False
def pytest_configure(config: pytest.Config) -> None:
"""Register custom markers."""
config.addinivalue_line("markers", "integration_test: mark test as integration test")
config.addinivalue_line("markers", "requires_dts: mark test as requiring DTS emulator")
config.addinivalue_line("markers", "requires_azure_openai: mark test as requiring Azure OpenAI")
config.addinivalue_line("markers", "requires_redis: mark test as requiring Redis")
config.addinivalue_line(
"markers",
"sample(path): specify the sample directory name for the test (e.g., @pytest.mark.sample('01_single_agent'))",
)
def pytest_collection_modifyitems(config: pytest.Config, items: list[pytest.Item]) -> None:
"""Skip tests based on markers and environment availability."""
run_integration = os.getenv("RUN_INTEGRATION_TESTS", "false").lower() == "true"
skip_integration = pytest.mark.skip(reason="RUN_INTEGRATION_TESTS not set to 'true'")
# Check Azure OpenAI environment variables
azure_openai_vars = ["AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"]
azure_openai_available = all(os.getenv(var) for var in azure_openai_vars)
skip_azure_openai = pytest.mark.skip(
reason=f"Missing required environment variables: {', '.join(azure_openai_vars)}"
)
# Check DTS availability
dts_available = _check_dts_available()
skip_dts = pytest.mark.skip(reason=f"DTS emulator is not available at {_get_dts_endpoint()}")
# Check Redis availability
redis_available = _check_redis_available()
skip_redis = pytest.mark.skip(reason="Redis is not available at redis://localhost:6379")
for item in items:
if "integration_test" in item.keywords and not run_integration:
item.add_marker(skip_integration)
if "requires_azure_openai" in item.keywords and not azure_openai_available:
item.add_marker(skip_azure_openai)
if "requires_dts" in item.keywords and not dts_available:
item.add_marker(skip_dts)
if "requires_redis" in item.keywords and not redis_available:
item.add_marker(skip_redis)
@pytest.fixture(scope="session")
def dts_endpoint() -> str:
"""Get the DTS endpoint from environment or use default."""
return _get_dts_endpoint()
@pytest.fixture(scope="session")
def dts_available(dts_endpoint: str) -> bool:
"""Check if DTS emulator is available and responding."""
if _check_dts_available(dts_endpoint):
return True
pytest.skip(f"DTS emulator is not available at {dts_endpoint}")
return False
@pytest.fixture(scope="session")
def check_azure_openai_env() -> None:
"""Verify Azure OpenAI environment variables are set."""
required_vars = ["AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"]
missing = [var for var in required_vars if not os.getenv(var)]
if missing:
pytest.skip(f"Missing required environment variables: {', '.join(missing)}")
@pytest.fixture(scope="module")
def unique_taskhub() -> str:
"""Generate a unique task hub name for test isolation."""
# Use a shorter UUID to avoid naming issues
return f"test-{uuid.uuid4().hex[:8]}"
@pytest.fixture(scope="module")
def worker_process(
dts_available: bool,
check_azure_openai_env: None,
dts_endpoint: str,
unique_taskhub: str,
request: pytest.FixtureRequest,
) -> Generator[dict[str, Any], None, None]:
"""
Start a worker process for the current test module by running the sample worker.py.
This fixture:
1. Determines which sample to run from @pytest.mark.sample()
2. Starts the sample's worker.py as a subprocess
3. Waits for the worker to be ready
4. Tears down the worker after tests complete
Usage:
@pytest.mark.sample("01_single_agent")
class TestSingleAgent:
...
"""
# Get sample path from marker
sample_marker = request.node.get_closest_marker("sample") # type: ignore[union-attr]
if not sample_marker:
pytest.fail("Test class must have @pytest.mark.sample() marker")
sample_name: str = cast(str, sample_marker.args[0]) # type: ignore[union-attr]
sample_path: Path = Path(__file__).parents[4] / "samples" / "getting_started" / "durabletask" / sample_name
worker_file: Path = sample_path / "worker.py"
if not worker_file.exists():
pytest.fail(f"Sample worker not found: {worker_file}")
# Set up environment for worker subprocess
env = os.environ.copy()
env["ENDPOINT"] = dts_endpoint
env["TASKHUB"] = unique_taskhub
# Start worker subprocess
try:
# On Windows, use CREATE_NEW_PROCESS_GROUP to allow proper termination
# shell=True only on Windows to handle PATH resolution
if sys.platform == "win32":
process = subprocess.Popen(
[sys.executable, str(worker_file)],
cwd=str(sample_path),
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP,
shell=True,
env=env,
text=True,
)
# On Unix, don't use shell=True to avoid shell wrapper issues
else:
process = subprocess.Popen(
[sys.executable, str(worker_file)],
cwd=str(sample_path),
env=env,
text=True,
)
except Exception as e:
pytest.fail(f"Failed to start worker subprocess: {e}")
# Wait for worker to initialize
time.sleep(2)
# Check if process is still running
if process.poll() is not None:
stderr_output = process.stderr.read() if process.stderr else ""
pytest.fail(f"Worker process exited prematurely. stderr: {stderr_output}")
# Provide worker info to tests
worker_info = {
"process": process,
"endpoint": dts_endpoint,
"taskhub": unique_taskhub,
}
try:
yield worker_info
finally:
# Cleanup: terminate worker subprocess
try:
process.terminate()
try:
process.wait(timeout=5)
except subprocess.TimeoutExpired:
process.kill()
process.wait()
except Exception as e:
logging.warning(f"Error during worker process cleanup: {e}")
@@ -0,0 +1,205 @@
# Copyright (c) Microsoft. All rights reserved.
"""Test utilities for durabletask integration tests."""
import json
import time
from typing import Any
from durabletask.azuremanaged.client import DurableTaskSchedulerClient
from durabletask.client import OrchestrationStatus
from agent_framework_durabletask import DurableAIAgentClient
def create_dts_client(endpoint: str, taskhub: str) -> DurableTaskSchedulerClient:
"""
Create a DurableTaskSchedulerClient with common configuration.
Args:
endpoint: The DTS endpoint address
taskhub: The task hub name
Returns:
A configured DurableTaskSchedulerClient instance
"""
return DurableTaskSchedulerClient(
host_address=endpoint,
secure_channel=False,
taskhub=taskhub,
token_credential=None,
)
def create_agent_client(
endpoint: str,
taskhub: str,
max_poll_retries: int = 90,
) -> tuple[DurableTaskSchedulerClient, DurableAIAgentClient]:
"""
Create a DurableAIAgentClient with the underlying DTS client.
Args:
endpoint: The DTS endpoint address
taskhub: The task hub name
max_poll_retries: Max poll retries for the agent client
Returns:
A tuple of (DurableTaskSchedulerClient, DurableAIAgentClient)
"""
dts_client = create_dts_client(endpoint, taskhub)
agent_client = DurableAIAgentClient(dts_client, max_poll_retries=max_poll_retries)
return dts_client, agent_client
class OrchestrationHelper:
"""Helper class for orchestration-related test operations."""
def __init__(self, dts_client: DurableTaskSchedulerClient):
"""
Initialize the orchestration helper.
Args:
dts_client: The DurableTaskSchedulerClient instance to use
"""
self.client = dts_client
def wait_for_orchestration(
self,
instance_id: str,
timeout: float = 60.0,
) -> Any:
"""
Wait for an orchestration to complete.
Args:
instance_id: The orchestration instance ID
timeout: Maximum time to wait in seconds
Returns:
The final OrchestrationMetadata
Raises:
TimeoutError: If the orchestration doesn't complete within timeout
RuntimeError: If the orchestration fails
"""
# Use the built-in wait_for_orchestration_completion method
metadata = self.client.wait_for_orchestration_completion(
instance_id=instance_id,
timeout=int(timeout),
)
if metadata is None:
raise TimeoutError(f"Orchestration {instance_id} did not complete within {timeout} seconds")
# Check if failed or terminated
if metadata.runtime_status == OrchestrationStatus.FAILED:
raise RuntimeError(f"Orchestration {instance_id} failed: {metadata.serialized_custom_status}")
if metadata.runtime_status == OrchestrationStatus.TERMINATED:
raise RuntimeError(f"Orchestration {instance_id} was terminated")
return metadata
def wait_for_orchestration_with_output(
self,
instance_id: str,
timeout: float = 60.0,
) -> tuple[Any, Any]:
"""
Wait for an orchestration to complete and return its output.
Args:
instance_id: The orchestration instance ID
timeout: Maximum time to wait in seconds
Returns:
A tuple of (OrchestrationMetadata, output)
Raises:
TimeoutError: If the orchestration doesn't complete within timeout
RuntimeError: If the orchestration fails
"""
metadata = self.wait_for_orchestration(instance_id, timeout)
# The output should be available in the metadata
return metadata, metadata.serialized_output
def get_orchestration_status(self, instance_id: str) -> Any | None:
"""
Get the current status of an orchestration.
Args:
instance_id: The orchestration instance ID
Returns:
The OrchestrationMetadata or None if not found
"""
try:
# Try to wait with a short timeout to get current status
return self.client.wait_for_orchestration_completion(
instance_id=instance_id,
timeout=1, # Very short timeout, just checking status
)
except Exception:
return None
def raise_event(
self,
instance_id: str,
event_name: str,
event_data: Any = None,
) -> None:
"""
Raise an external event to an orchestration.
Args:
instance_id: The orchestration instance ID
event_name: The name of the event
event_data: The event data payload
"""
self.client.raise_orchestration_event(instance_id, event_name, data=event_data)
def wait_for_notification(self, instance_id: str, timeout_seconds: int = 30) -> bool:
"""Wait for the orchestration to reach a notification point.
Polls the orchestration status until it appears to be waiting for approval.
Args:
instance_id: The orchestration instance ID
timeout_seconds: Maximum time to wait
Returns:
True if notification detected, False if timeout
"""
start_time = time.time()
while time.time() - start_time < timeout_seconds:
try:
metadata = self.client.get_orchestration_state(
instance_id=instance_id,
)
if metadata:
# Check if we're waiting for approval by examining custom status
if metadata.serialized_custom_status:
try:
custom_status = json.loads(metadata.serialized_custom_status)
# Handle both string and dict custom status
status_str = custom_status if isinstance(custom_status, str) else str(custom_status)
if status_str.lower().startswith("requesting human feedback"):
return True
except (json.JSONDecodeError, AttributeError):
# If it's not JSON, treat as plain string
if metadata.serialized_custom_status.lower().startswith("requesting human feedback"):
return True
# Check for terminal states
if metadata.runtime_status.name == "COMPLETED" or metadata.runtime_status.name == "FAILED":
return False
except Exception:
# Silently ignore transient errors during polling (e.g., network issues, service unavailable).
# The loop will retry until timeout, allowing the service to recover.
pass
time.sleep(1)
return False
@@ -0,0 +1,89 @@
# Copyright (c) Microsoft. All rights reserved.
"""Integration tests for single agent functionality.
Tests basic agent operations including:
- Agent registration and retrieval
- Single agent interactions
- Conversation continuity across multiple messages
- Multi-threaded agent usage
- Empty thread ID handling
"""
from typing import Any
import pytest
from dt_testutils import create_agent_client
# Module-level markers - applied to all tests in this module
pytestmark = [
pytest.mark.sample("01_single_agent"),
pytest.mark.integration_test,
pytest.mark.requires_azure_openai,
pytest.mark.requires_dts,
]
class TestSingleAgent:
"""Test suite for single agent functionality."""
@pytest.fixture(autouse=True)
def setup(self, worker_process: dict[str, Any], dts_endpoint: str) -> None:
"""Setup test fixtures."""
self.endpoint: str = dts_endpoint
self.taskhub: str = str(worker_process["taskhub"])
# Create agent client
_, self.agent_client = create_agent_client(self.endpoint, self.taskhub)
def test_agent_registration(self) -> None:
"""Test that the Joker agent is registered and accessible."""
agent = self.agent_client.get_agent("Joker")
assert agent is not None
assert agent.name == "Joker"
def test_single_interaction(self):
"""Test a single interaction with the agent."""
agent = self.agent_client.get_agent("Joker")
thread = agent.get_new_thread()
response = agent.run("Tell me a short joke about programming.", thread=thread)
assert response is not None
assert response.text is not None
assert len(response.text) > 0
def test_conversation_continuity(self):
"""Test that conversation context is maintained across turns."""
agent = self.agent_client.get_agent("Joker")
thread = agent.get_new_thread()
# First turn: Ask for a joke about a specific topic
response1 = agent.run("Tell me a joke about cats.", thread=thread)
assert response1 is not None
assert len(response1.text) > 0
# Second turn: Ask a follow-up that requires context
response2 = agent.run("Can you make it funnier?", thread=thread)
assert response2 is not None
assert len(response2.text) > 0
# The agent should understand "it" refers to the previous joke
def test_multiple_threads(self):
"""Test that different threads maintain separate contexts."""
agent = self.agent_client.get_agent("Joker")
# Create two separate threads
thread1 = agent.get_new_thread()
thread2 = agent.get_new_thread()
assert thread1.session_id != thread2.session_id
# Send different messages to each thread
response1 = agent.run("Tell me a joke about dogs.", thread=thread1)
response2 = agent.run("Tell me a joke about birds.", thread=thread2)
assert response1 is not None
assert response2 is not None
assert response1.text != response2.text
@@ -0,0 +1,103 @@
# Copyright (c) Microsoft. All rights reserved.
"""Integration tests for multi-agent functionality.
Tests operations with multiple specialized agents:
- Multiple agent registration
- Agent-specific tool usage
- Independent thread management per agent
- Concurrent agent operations
- Agent isolation and tool routing
"""
from typing import Any
import pytest
from dt_testutils import create_agent_client
# Agent names from the 02_multi_agent sample
WEATHER_AGENT_NAME: str = "WeatherAgent"
MATH_AGENT_NAME: str = "MathAgent"
# Module-level markers - applied to all tests in this module
pytestmark = [
pytest.mark.sample("02_multi_agent"),
pytest.mark.integration_test,
pytest.mark.requires_azure_openai,
pytest.mark.requires_dts,
]
class TestMultiAgent:
"""Test suite for multi-agent functionality."""
@pytest.fixture(autouse=True)
def setup(self, worker_process: dict[str, Any], dts_endpoint: str) -> None:
"""Setup test fixtures."""
self.endpoint: str = dts_endpoint
self.taskhub: str = str(worker_process["taskhub"])
# Create agent client
_, self.agent_client = create_agent_client(self.endpoint, self.taskhub)
def test_multiple_agents_registered(self) -> None:
"""Test that both agents are registered and accessible."""
weather_agent = self.agent_client.get_agent(WEATHER_AGENT_NAME)
math_agent = self.agent_client.get_agent(MATH_AGENT_NAME)
assert weather_agent is not None
assert weather_agent.name == WEATHER_AGENT_NAME
assert math_agent is not None
assert math_agent.name == MATH_AGENT_NAME
def test_weather_agent_with_tool(self):
"""Test weather agent with weather tool execution."""
agent = self.agent_client.get_agent(WEATHER_AGENT_NAME)
thread = agent.get_new_thread()
response = agent.run("What's the weather in Seattle?", thread=thread)
assert response is not None
assert response.text is not None
# Should contain weather information from the tool
assert len(response.text) > 0
# Verify that the get_weather tool was actually invoked
tool_calls = [
content for msg in response.messages for content in msg.contents if content.type == "function_call"
]
assert len(tool_calls) > 0, "Expected at least one tool call"
assert any(call.name == "get_weather" for call in tool_calls), "Expected get_weather tool to be called"
def test_math_agent_with_tool(self):
"""Test math agent with calculation tool execution."""
agent = self.agent_client.get_agent(MATH_AGENT_NAME)
thread = agent.get_new_thread()
response = agent.run("Calculate a 20% tip on a $50 bill.", thread=thread)
assert response is not None
assert response.text is not None
# Should contain calculation results from the tool
assert len(response.text) > 0
# Verify that the calculate_tip tool was actually invoked
tool_calls = [
content for msg in response.messages for content in msg.contents if content.type == "function_call"
]
assert len(tool_calls) > 0, "Expected at least one tool call"
assert any(call.name == "calculate_tip" for call in tool_calls), "Expected calculate_tip tool to be called"
def test_multiple_calls_to_same_agent(self):
"""Test multiple sequential calls to the same agent."""
agent = self.agent_client.get_agent(WEATHER_AGENT_NAME)
thread = agent.get_new_thread()
# Multiple weather queries
response1 = agent.run("What's the weather in Chicago?", thread=thread)
response2 = agent.run("And what about Los Angeles?", thread=thread)
assert response1 is not None
assert response2 is not None
assert len(response1.text) > 0
assert len(response2.text) > 0
@@ -0,0 +1,226 @@
# Copyright (c) Microsoft. All rights reserved.
"""
Integration Tests for Reliable Streaming Sample
Tests the reliable streaming sample using Redis Streams for persistent message delivery.
The worker process is automatically started by the test fixture.
Prerequisites:
- Azure OpenAI credentials configured (see packages/durabletask/tests/integration_tests/.env.example)
- DTS emulator running (docker run -d -p 8080:8080 mcr.microsoft.com/durabletask/emulator:latest)
- Redis running (docker run -d --name redis -p 6379:6379 redis:latest)
Usage:
uv run pytest packages/durabletask/tests/integration_tests/test_03_single_agent_streaming.py -v
"""
import asyncio
import os
import sys
import time
from datetime import timedelta
from pathlib import Path
from typing import Any
import pytest
import redis.asyncio as aioredis
from dt_testutils import OrchestrationHelper, create_agent_client
# Add sample directory to path to import RedisStreamResponseHandler
SAMPLE_DIR = Path(__file__).parents[4] / "samples" / "getting_started" / "durabletask" / "03_single_agent_streaming"
sys.path.insert(0, str(SAMPLE_DIR))
from redis_stream_response_handler import RedisStreamResponseHandler # type: ignore[reportMissingImports] # noqa: E402
# Module-level markers - applied to all tests in this file
pytestmark = [
pytest.mark.sample("03_single_agent_streaming"),
pytest.mark.integration_test,
pytest.mark.requires_azure_openai,
pytest.mark.requires_dts,
pytest.mark.requires_redis,
]
class TestSampleReliableStreaming:
"""Tests for 03_single_agent_streaming sample."""
@pytest.fixture(autouse=True)
def setup(self, worker_process: dict[str, Any], dts_endpoint: str) -> None:
"""Setup test fixtures."""
self.endpoint: str = dts_endpoint
self.taskhub: str = str(worker_process["taskhub"])
# Create agent client
dts_client, self.agent_client = create_agent_client(self.endpoint, self.taskhub)
self.helper = OrchestrationHelper(dts_client)
# Redis configuration
self.redis_connection_string = os.environ.get("REDIS_CONNECTION_STRING", "redis://localhost:6379")
self.redis_stream_ttl_minutes = int(os.environ.get("REDIS_STREAM_TTL_MINUTES", "10"))
async def _get_stream_handler(self) -> RedisStreamResponseHandler: # type: ignore[reportMissingTypeStubs]
"""Create a new Redis stream handler for each request."""
redis_client = aioredis.from_url( # type: ignore[reportUnknownMemberType]
self.redis_connection_string,
encoding="utf-8",
decode_responses=False,
)
return RedisStreamResponseHandler( # type: ignore[reportUnknownMemberType]
redis_client=redis_client,
stream_ttl=timedelta(minutes=self.redis_stream_ttl_minutes),
)
async def _stream_from_redis(
self,
thread_id: str,
cursor: str | None = None,
timeout: float = 30.0,
) -> tuple[str, bool, str]:
"""
Stream responses from Redis using the sample's RedisStreamResponseHandler.
Args:
thread_id: The conversation/thread ID to stream from
cursor: Optional cursor to resume from
timeout: Maximum time to wait for stream completion
Returns:
Tuple of (accumulated text, completion status, last entry_id)
"""
accumulated_text = ""
is_complete = False
last_entry_id = cursor if cursor else "0-0"
start_time = time.time()
async with await self._get_stream_handler() as stream_handler: # type: ignore[reportUnknownMemberType]
try:
async for chunk in stream_handler.read_stream(thread_id, cursor): # type: ignore[reportUnknownMemberType]
if time.time() - start_time > timeout:
break
last_entry_id = chunk.entry_id # type: ignore[reportUnknownMemberType]
if chunk.error: # type: ignore[reportUnknownMemberType]
# Stream not found or timeout - this is expected if agent hasn't written yet
# Don't raise an error, just return what we have
break
if chunk.is_done: # type: ignore[reportUnknownMemberType]
is_complete = True
break
if chunk.text: # type: ignore[reportUnknownMemberType]
accumulated_text += chunk.text # type: ignore[reportUnknownMemberType]
except Exception as ex:
# For test purposes, we catch exceptions and return what we have
if "timed out" not in str(ex).lower():
raise
return accumulated_text, is_complete, last_entry_id # type: ignore[reportReturnType]
def test_agent_run_and_stream(self) -> None:
"""Test agent execution with Redis streaming."""
# Get the TravelPlanner agent
travel_planner = self.agent_client.get_agent("TravelPlanner")
assert travel_planner is not None
assert travel_planner.name == "TravelPlanner"
# Create a new thread
thread = travel_planner.get_new_thread()
assert thread.session_id is not None
assert thread.session_id.key is not None
thread_id = str(thread.session_id.key)
# Start agent run with wait_for_response=False for non-blocking execution
travel_planner.run(
"Plan a 1-day trip to Seattle in 1 sentence", thread=thread, options={"wait_for_response": False}
)
# Poll Redis stream with retries to handle race conditions
# The agent may take a few seconds to process and start writing to Redis
# We use cursor-based resumption to continue reading from where we left off
max_retries = 20
retry_count = 0
accumulated_text = ""
is_complete = False
cursor: str | None = None
while retry_count < max_retries and not is_complete:
text, is_complete, last_cursor = asyncio.run(
self._stream_from_redis(thread_id, cursor=cursor, timeout=10.0)
)
accumulated_text += text
cursor = last_cursor # Resume from last position on next read
if is_complete:
# Stream completed successfully
break
if len(accumulated_text) > 0:
# Got content but not completion marker yet - keep reading without delay
# The agent may still be streaming or about to write completion marker
continue
# No content yet - wait before retrying
time.sleep(2)
retry_count += 1
# Verify we got content
assert len(accumulated_text) > 0, (
f"Expected text content but got empty string for thread_id: {thread_id} after {retry_count} retries"
)
assert "seattle" in accumulated_text.lower(), f"Expected 'seattle' in response but got: {accumulated_text}"
assert is_complete, "Expected stream to be complete"
def test_stream_with_cursor_resumption(self) -> None:
"""Test streaming with cursor-based resumption."""
# Get the TravelPlanner agent
travel_planner = self.agent_client.get_agent("TravelPlanner")
thread = travel_planner.get_new_thread()
assert thread.session_id is not None
assert thread.session_id.key is not None
thread_id = str(thread.session_id.key)
# Start agent run
travel_planner.run("What's the weather like?", thread=thread, options={"wait_for_response": False})
# Wait for agent to start writing
time.sleep(3)
# Read partial stream to get a cursor
async def get_partial_stream() -> tuple[str, str]:
async with await self._get_stream_handler() as stream_handler: # type: ignore[reportUnknownMemberType]
accumulated_text = ""
last_entry_id = "0-0"
chunk_count = 0
# Read just first 2 chunks
async for chunk in stream_handler.read_stream(thread_id): # type: ignore[reportUnknownMemberType]
last_entry_id = chunk.entry_id # type: ignore[reportUnknownMemberType]
if chunk.text: # type: ignore[reportUnknownMemberType]
accumulated_text += chunk.text # type: ignore[reportUnknownMemberType]
chunk_count += 1
if chunk_count >= 2:
break
return accumulated_text, last_entry_id # type: ignore[reportReturnType]
partial_text, cursor = asyncio.run(get_partial_stream())
# Resume from cursor
remaining_text, _, _ = asyncio.run(self._stream_from_redis(thread_id, cursor=cursor))
# Verify we got some initial content
assert len(partial_text) > 0
# Combined text should be coherent
full_text = partial_text + remaining_text
assert len(full_text) > 0
if __name__ == "__main__":
pytest.main([__file__, "-v"])
@@ -0,0 +1,105 @@
# Copyright (c) Microsoft. All rights reserved.
"""Integration tests for single agent orchestration with chaining.
Tests orchestration patterns with sequential agent calls:
- Orchestration registration and execution
- Sequential agent calls on same thread
- Conversation continuity in orchestrations
- Thread context preservation
"""
import json
import logging
from typing import Any
import pytest
from dt_testutils import OrchestrationHelper, create_agent_client
from durabletask.client import OrchestrationStatus
# Agent name from the 04_single_agent_orchestration_chaining sample
WRITER_AGENT_NAME: str = "WriterAgent"
# Configure logging
logging.basicConfig(level=logging.WARNING)
# Module-level markers - applied to all tests in this module
pytestmark = [
pytest.mark.sample("04_single_agent_orchestration_chaining"),
pytest.mark.integration_test,
pytest.mark.requires_azure_openai,
pytest.mark.requires_dts,
]
class TestSingleAgentOrchestrationChaining:
"""Test suite for single agent orchestration with chaining."""
@pytest.fixture(autouse=True)
def setup(self, worker_process: dict[str, Any], dts_endpoint: str) -> None:
"""Setup test fixtures."""
self.endpoint: str = dts_endpoint
self.taskhub: str = str(worker_process["taskhub"])
# Create agent client and DTS client
self.dts_client, self.agent_client = create_agent_client(self.endpoint, self.taskhub)
# Create orchestration helper
self.orch_helper = OrchestrationHelper(self.dts_client)
def test_agent_registered(self):
"""Test that the Writer agent is registered."""
agent = self.agent_client.get_agent(WRITER_AGENT_NAME)
assert agent is not None
assert agent.name == WRITER_AGENT_NAME
def test_chaining_context_preserved(self):
"""Test that context is preserved across agent runs in orchestration."""
# Start the orchestration
instance_id = self.dts_client.schedule_new_orchestration(
orchestrator="single_agent_chaining_orchestration",
input="",
)
# Wait for completion with output
metadata, output = self.orch_helper.wait_for_orchestration_with_output(
instance_id=instance_id,
timeout=120.0,
)
assert metadata is not None
assert output is not None
# The final output should be a refined sentence
final_text = json.loads(output)
# Should be a meaningful sentence (not empty or error message)
assert len(final_text) > 10
assert not final_text.startswith("Error")
def test_multiple_orchestration_instances(self):
"""Test that multiple orchestration instances can run independently."""
# Start two orchestrations
instance_id_1 = self.dts_client.schedule_new_orchestration(
orchestrator="single_agent_chaining_orchestration",
input="",
)
instance_id_2 = self.dts_client.schedule_new_orchestration(
orchestrator="single_agent_chaining_orchestration",
input="",
)
assert instance_id_1 != instance_id_2
# Both should complete
metadata_1 = self.orch_helper.wait_for_orchestration(
instance_id=instance_id_1,
timeout=120.0,
)
metadata_2 = self.orch_helper.wait_for_orchestration(
instance_id=instance_id_2,
timeout=120.0,
)
assert metadata_1.runtime_status == OrchestrationStatus.COMPLETED
assert metadata_2.runtime_status == OrchestrationStatus.COMPLETED
@@ -0,0 +1,81 @@
# Copyright (c) Microsoft. All rights reserved.
"""Integration tests for multi-agent orchestration with concurrency.
Tests concurrent execution patterns:
- Parallel agent execution
- Concurrent orchestration tasks
- Independent thread management in parallel
- Result aggregation from concurrent calls
"""
import json
import logging
from typing import Any
import pytest
from dt_testutils import OrchestrationHelper, create_agent_client
from durabletask.client import OrchestrationStatus
# Agent names from the 05_multi_agent_orchestration_concurrency sample
PHYSICIST_AGENT_NAME: str = "PhysicistAgent"
CHEMIST_AGENT_NAME: str = "ChemistAgent"
# Configure logging
logging.basicConfig(level=logging.WARNING)
# Module-level markers
pytestmark = [
pytest.mark.sample("05_multi_agent_orchestration_concurrency"),
pytest.mark.integration_test,
pytest.mark.requires_dts,
]
class TestMultiAgentOrchestrationConcurrency:
"""Test suite for multi-agent orchestration with concurrency."""
@pytest.fixture(autouse=True)
def setup(self, worker_process: dict[str, Any], dts_endpoint: str) -> None:
"""Setup test fixtures."""
self.endpoint = dts_endpoint
self.taskhub = worker_process["taskhub"]
# Create agent client and DTS client
self.dts_client, self.agent_client = create_agent_client(self.endpoint, self.taskhub)
# Create orchestration helper
self.orch_helper = OrchestrationHelper(self.dts_client)
def test_agents_registered(self):
"""Test that both agents are registered."""
physicist = self.agent_client.get_agent(PHYSICIST_AGENT_NAME)
chemist = self.agent_client.get_agent(CHEMIST_AGENT_NAME)
assert physicist is not None
assert physicist.name == PHYSICIST_AGENT_NAME
assert chemist is not None
assert chemist.name == CHEMIST_AGENT_NAME
def test_different_prompts(self):
"""Test concurrent orchestration with different prompts."""
prompts = [
"What is temperature?",
"Explain molecules.",
]
for prompt in prompts:
instance_id = self.dts_client.schedule_new_orchestration(
orchestrator="multi_agent_concurrent_orchestration",
input=prompt,
)
metadata, output = self.orch_helper.wait_for_orchestration_with_output(
instance_id=instance_id,
timeout=120.0,
)
assert metadata.runtime_status == OrchestrationStatus.COMPLETED
result = json.loads(output)
assert "physicist" in result
assert "chemist" in result
@@ -0,0 +1,95 @@
# Copyright (c) Microsoft. All rights reserved.
"""Integration tests for multi-agent orchestration with conditionals.
Tests conditional orchestration patterns:
- Conditional branching in orchestrations
- Agent-based decision making
- Activity function execution
- Structured output handling
- Conditional routing based on agent responses
"""
import logging
from typing import Any
import pytest
from dt_testutils import OrchestrationHelper, create_agent_client
from durabletask.client import OrchestrationStatus
# Agent names from the 06_multi_agent_orchestration_conditionals sample
SPAM_AGENT_NAME: str = "SpamDetectionAgent"
EMAIL_AGENT_NAME: str = "EmailAssistantAgent"
# Configure logging
logging.basicConfig(level=logging.WARNING)
# Module-level markers
pytestmark = [
pytest.mark.sample("06_multi_agent_orchestration_conditionals"),
pytest.mark.integration_test,
pytest.mark.requires_dts,
]
class TestMultiAgentOrchestrationConditionals:
"""Test suite for multi-agent orchestration with conditionals."""
@pytest.fixture(autouse=True)
def setup(self, worker_process: dict[str, Any], dts_endpoint: str) -> None:
"""Setup test fixtures."""
self.endpoint: str = dts_endpoint
self.taskhub: str = str(worker_process["taskhub"])
# Create agent client and DTS client
self.dts_client, self.agent_client = create_agent_client(self.endpoint, self.taskhub)
# Create orchestration helper
self.orch_helper = OrchestrationHelper(self.dts_client)
def test_agents_registered(self):
"""Test that both agents are registered."""
spam_agent = self.agent_client.get_agent(SPAM_AGENT_NAME)
email_agent = self.agent_client.get_agent(EMAIL_AGENT_NAME)
assert spam_agent is not None
assert spam_agent.name == SPAM_AGENT_NAME
assert email_agent is not None
assert email_agent.name == EMAIL_AGENT_NAME
def test_conditional_branching(self):
"""Test that conditional branching works correctly."""
# Test with obvious spam
spam_payload = {
"email_id": "spam-001",
"email_content": "Buy cheap medications online! No prescription needed! Limited time offer!",
}
spam_instance_id = self.dts_client.schedule_new_orchestration(
orchestrator="spam_detection_orchestration",
input=spam_payload,
)
# Test with legitimate email
legit_payload = {
"email_id": "legit-001",
"email_content": "Hi team, please review the attached document before our meeting tomorrow.",
}
legit_instance_id = self.dts_client.schedule_new_orchestration(
orchestrator="spam_detection_orchestration",
input=legit_payload,
)
# Both should complete successfully (different branches)
spam_metadata = self.orch_helper.wait_for_orchestration(
instance_id=spam_instance_id,
timeout=120.0,
)
legit_metadata = self.orch_helper.wait_for_orchestration(
instance_id=legit_instance_id,
timeout=120.0,
)
assert spam_metadata.runtime_status == OrchestrationStatus.COMPLETED
assert legit_metadata.runtime_status == OrchestrationStatus.COMPLETED
@@ -0,0 +1,170 @@
# Copyright (c) Microsoft. All rights reserved.
"""Integration tests for single agent orchestration with human-in-the-loop.
Tests human-in-the-loop (HITL) patterns:
- External event waiting and handling
- Timeout handling in orchestrations
- Iterative refinement with human feedback
- Activity function integration
- Approval workflow patterns
"""
import logging
from typing import Any
import pytest
from dt_testutils import OrchestrationHelper, create_agent_client
from durabletask.client import OrchestrationStatus
# Constants from the 07_single_agent_orchestration_hitl sample
WRITER_AGENT_NAME: str = "WriterAgent"
HUMAN_APPROVAL_EVENT: str = "HumanApproval"
# Configure logging
logging.basicConfig(level=logging.WARNING)
# Module-level markers
pytestmark = [
pytest.mark.sample("07_single_agent_orchestration_hitl"),
pytest.mark.integration_test,
pytest.mark.requires_dts,
]
class TestSingleAgentOrchestrationHITL:
"""Test suite for single agent orchestration with human-in-the-loop."""
@pytest.fixture(autouse=True)
def setup(self, worker_process: dict[str, Any], dts_endpoint: str) -> None:
"""Setup test fixtures."""
self.endpoint: str = str(worker_process["endpoint"])
self.taskhub: str = str(worker_process["taskhub"])
logging.info(f"Using taskhub: {self.taskhub} at endpoint: {self.endpoint}")
# Create agent client and DTS client
self.dts_client, self.agent_client = create_agent_client(self.endpoint, self.taskhub)
# Create orchestration helper
self.orch_helper = OrchestrationHelper(self.dts_client)
def test_agent_registered(self):
"""Test that the Writer agent is registered."""
agent = self.agent_client.get_agent(WRITER_AGENT_NAME)
assert agent is not None
assert agent.name == WRITER_AGENT_NAME
def test_hitl_orchestration_with_approval(self):
"""Test HITL orchestration with immediate approval."""
payload = {
"topic": "The benefits of continuous learning",
"max_review_attempts": 3,
"approval_timeout_seconds": 60,
}
# Start the orchestration
instance_id = self.dts_client.schedule_new_orchestration(
orchestrator="content_generation_hitl_orchestration",
input=payload,
)
assert instance_id is not None
# Wait for orchestration to reach notification point
notification_received = self.orch_helper.wait_for_notification(instance_id, timeout_seconds=90)
assert notification_received, "Failed to receive notification from orchestration"
# Send approval event
approval_data = {"approved": True, "feedback": ""}
self.orch_helper.raise_event(
instance_id=instance_id,
event_name=HUMAN_APPROVAL_EVENT,
event_data=approval_data,
)
# Wait for completion
metadata = self.orch_helper.wait_for_orchestration(
instance_id=instance_id,
timeout=90.0,
)
assert metadata is not None
assert metadata.runtime_status == OrchestrationStatus.COMPLETED
def test_hitl_orchestration_with_rejection_and_feedback(self):
"""Test HITL orchestration with rejection and iterative refinement."""
payload = {
"topic": "Artificial Intelligence in healthcare",
"max_review_attempts": 3,
"approval_timeout_seconds": 60,
}
# Start the orchestration
instance_id = self.dts_client.schedule_new_orchestration(
orchestrator="content_generation_hitl_orchestration",
input=payload,
)
# Wait for orchestration to reach notification point
notification_received = self.orch_helper.wait_for_notification(instance_id, timeout_seconds=90)
assert notification_received, "Failed to receive notification from orchestration"
# First rejection with feedback
rejection_data = {
"approved": False,
"feedback": "Please make it more concise and add specific examples.",
}
self.orch_helper.raise_event(
instance_id=instance_id,
event_name=HUMAN_APPROVAL_EVENT,
event_data=rejection_data,
)
# Wait for orchestration to refine and reach notification point again
notification_received = self.orch_helper.wait_for_notification(instance_id, timeout_seconds=90)
assert notification_received, "Failed to receive notification after refinement"
# Second approval
approval_data = {"approved": True, "feedback": ""}
self.orch_helper.raise_event(
instance_id=instance_id,
event_name=HUMAN_APPROVAL_EVENT,
event_data=approval_data,
)
# Wait for completion
metadata = self.orch_helper.wait_for_orchestration(
instance_id=instance_id,
timeout=90.0,
)
assert metadata is not None
assert metadata.runtime_status == OrchestrationStatus.COMPLETED
def test_hitl_orchestration_timeout(self):
"""Test HITL orchestration timeout behavior."""
payload = {
"topic": "Cloud computing fundamentals",
"max_review_attempts": 1,
"approval_timeout_seconds": 0.1, # Short timeout for testing
}
# Start the orchestration
instance_id = self.dts_client.schedule_new_orchestration(
orchestrator="content_generation_hitl_orchestration",
input=payload,
)
# Don't send any approval - let it timeout
# The orchestration should fail due to timeout
try:
metadata = self.orch_helper.wait_for_orchestration(
instance_id=instance_id,
timeout=90.0,
)
# If it completes, it should be failed status due to timeout
assert metadata.runtime_status == OrchestrationStatus.FAILED
except (RuntimeError, TimeoutError):
# Expected - orchestration should timeout and fail
pass
@@ -0,0 +1,300 @@
# Copyright (c) Microsoft. All rights reserved.
"""Unit tests for AgentSessionId and DurableAgentThread."""
import pytest
from agent_framework import AgentThread
from agent_framework_durabletask._models import AgentSessionId, DurableAgentThread
class TestAgentSessionId:
"""Test suite for AgentSessionId."""
def test_init_creates_session_id(self) -> None:
"""Test that AgentSessionId initializes correctly."""
session_id = AgentSessionId(name="AgentEntity", key="test-key-123")
assert session_id.name == "AgentEntity"
assert session_id.key == "test-key-123"
def test_with_random_key_generates_guid(self) -> None:
"""Test that with_random_key generates a GUID."""
session_id = AgentSessionId.with_random_key(name="AgentEntity")
assert session_id.name == "AgentEntity"
assert len(session_id.key) == 32 # UUID hex is 32 chars
# Verify it's a valid hex string
int(session_id.key, 16)
def test_with_random_key_unique_keys(self) -> None:
"""Test that with_random_key generates unique keys."""
session_id1 = AgentSessionId.with_random_key(name="AgentEntity")
session_id2 = AgentSessionId.with_random_key(name="AgentEntity")
assert session_id1.key != session_id2.key
def test_str_representation(self) -> None:
"""Test string representation."""
session_id = AgentSessionId(name="AgentEntity", key="test-key-123")
str_repr = str(session_id)
assert str_repr == "@AgentEntity@test-key-123"
def test_repr_representation(self) -> None:
"""Test repr representation."""
session_id = AgentSessionId(name="AgentEntity", key="test-key")
repr_str = repr(session_id)
assert "AgentSessionId" in repr_str
assert "AgentEntity" in repr_str
assert "test-key" in repr_str
def test_parse_valid_session_id(self) -> None:
"""Test parsing valid session ID string."""
session_id = AgentSessionId.parse("@AgentEntity@test-key-123")
assert session_id.name == "AgentEntity"
assert session_id.key == "test-key-123"
def test_parse_invalid_format_no_prefix(self) -> None:
"""Test parsing invalid format without @ prefix."""
with pytest.raises(ValueError) as exc_info:
AgentSessionId.parse("AgentEntity@test-key")
assert "Invalid agent session ID format" in str(exc_info.value)
def test_parse_invalid_format_single_part(self) -> None:
"""Test parsing invalid format with single part."""
with pytest.raises(ValueError) as exc_info:
AgentSessionId.parse("@AgentEntity")
assert "Invalid agent session ID format" in str(exc_info.value)
def test_parse_with_multiple_at_signs_in_key(self) -> None:
"""Test parsing with @ signs in the key."""
session_id = AgentSessionId.parse("@AgentEntity@key-with@symbols")
assert session_id.name == "AgentEntity"
assert session_id.key == "key-with@symbols"
def test_parse_round_trip(self) -> None:
"""Test round-trip parse and string conversion."""
original = AgentSessionId(name="AgentEntity", key="test-key")
str_repr = str(original)
parsed = AgentSessionId.parse(str_repr)
assert parsed.name == original.name
assert parsed.key == original.key
def test_to_entity_name_adds_prefix(self) -> None:
"""Test that to_entity_name adds the dafx- prefix."""
entity_name = AgentSessionId.to_entity_name("TestAgent")
assert entity_name == "dafx-TestAgent"
def test_parse_with_agent_name_override(self) -> None:
"""Test parsing @name@key format with agent_name parameter overrides the name."""
session_id = AgentSessionId.parse("@OriginalAgent@test-key-123", agent_name="OverriddenAgent")
assert session_id.name == "OverriddenAgent"
assert session_id.key == "test-key-123"
def test_parse_without_agent_name_uses_parsed_name(self) -> None:
"""Test parsing @name@key format without agent_name uses name from string."""
session_id = AgentSessionId.parse("@ParsedAgent@test-key-123")
assert session_id.name == "ParsedAgent"
assert session_id.key == "test-key-123"
def test_parse_plain_string_with_agent_name(self) -> None:
"""Test parsing plain string with agent_name uses entire string as key."""
session_id = AgentSessionId.parse("simple-thread-123", agent_name="TestAgent")
assert session_id.name == "TestAgent"
assert session_id.key == "simple-thread-123"
def test_parse_plain_string_without_agent_name_raises(self) -> None:
"""Test parsing plain string without agent_name raises ValueError."""
with pytest.raises(ValueError) as exc_info:
AgentSessionId.parse("simple-thread-123")
assert "Invalid agent session ID format" in str(exc_info.value)
class TestDurableAgentThread:
"""Test suite for DurableAgentThread."""
def test_init_with_session_id(self) -> None:
"""Test DurableAgentThread initialization with session ID."""
session_id = AgentSessionId(name="TestAgent", key="test-key")
thread = DurableAgentThread(session_id=session_id)
assert thread.session_id is not None
assert thread.session_id == session_id
def test_init_without_session_id(self) -> None:
"""Test DurableAgentThread initialization without session ID."""
thread = DurableAgentThread()
assert thread.session_id is None
def test_session_id_setter(self) -> None:
"""Test setting a session ID to an existing thread."""
thread = DurableAgentThread()
assert thread.session_id is None
session_id = AgentSessionId(name="TestAgent", key="test-key")
thread.session_id = session_id
assert thread.session_id is not None
assert thread.session_id == session_id
assert thread.session_id.name == "TestAgent"
def test_from_session_id(self) -> None:
"""Test creating DurableAgentThread from session ID."""
session_id = AgentSessionId(name="TestAgent", key="test-key")
thread = DurableAgentThread.from_session_id(session_id)
assert isinstance(thread, DurableAgentThread)
assert thread.session_id is not None
assert thread.session_id == session_id
assert thread.session_id.name == "TestAgent"
assert thread.session_id.key == "test-key"
def test_from_session_id_with_service_thread_id(self) -> None:
"""Test creating DurableAgentThread with service thread ID."""
session_id = AgentSessionId(name="TestAgent", key="test-key")
thread = DurableAgentThread.from_session_id(session_id, service_thread_id="service-123")
assert thread.session_id is not None
assert thread.session_id == session_id
assert thread.service_thread_id == "service-123"
async def test_serialize_with_session_id(self) -> None:
"""Test serialization includes session ID."""
session_id = AgentSessionId(name="TestAgent", key="test-key")
thread = DurableAgentThread(session_id=session_id)
serialized = await thread.serialize()
assert isinstance(serialized, dict)
assert "durable_session_id" in serialized
assert serialized["durable_session_id"] == "@TestAgent@test-key"
async def test_serialize_without_session_id(self) -> None:
"""Test serialization without session ID."""
thread = DurableAgentThread()
serialized = await thread.serialize()
assert isinstance(serialized, dict)
assert "durable_session_id" not in serialized
async def test_deserialize_with_session_id(self) -> None:
"""Test deserialization restores session ID."""
serialized = {
"service_thread_id": "thread-123",
"durable_session_id": "@TestAgent@test-key",
}
thread = await DurableAgentThread.deserialize(serialized)
assert isinstance(thread, DurableAgentThread)
assert thread.session_id is not None
assert thread.session_id.name == "TestAgent"
assert thread.session_id.key == "test-key"
assert thread.service_thread_id == "thread-123"
async def test_deserialize_without_session_id(self) -> None:
"""Test deserialization without session ID."""
serialized = {
"service_thread_id": "thread-456",
}
thread = await DurableAgentThread.deserialize(serialized)
assert isinstance(thread, DurableAgentThread)
assert thread.session_id is None
assert thread.service_thread_id == "thread-456"
async def test_round_trip_serialization(self) -> None:
"""Test round-trip serialization preserves session ID."""
session_id = AgentSessionId(name="TestAgent", key="test-key-789")
original = DurableAgentThread(session_id=session_id)
serialized = await original.serialize()
restored = await DurableAgentThread.deserialize(serialized)
assert isinstance(restored, DurableAgentThread)
assert restored.session_id is not None
assert restored.session_id.name == session_id.name
assert restored.session_id.key == session_id.key
async def test_deserialize_invalid_session_id_type(self) -> None:
"""Test deserialization with invalid session ID type raises error."""
serialized = {
"service_thread_id": "thread-123",
"durable_session_id": 12345, # Invalid type
}
with pytest.raises(ValueError, match="durable_session_id must be a string"):
await DurableAgentThread.deserialize(serialized)
class TestAgentThreadCompatibility:
"""Test suite for compatibility between AgentThread and DurableAgentThread."""
async def test_agent_thread_serialize(self) -> None:
"""Test that base AgentThread can be serialized."""
thread = AgentThread()
serialized = await thread.serialize()
assert isinstance(serialized, dict)
assert "service_thread_id" in serialized
async def test_agent_thread_deserialize(self) -> None:
"""Test that base AgentThread can be deserialized."""
thread = AgentThread()
serialized = await thread.serialize()
restored = await AgentThread.deserialize(serialized)
assert isinstance(restored, AgentThread)
assert restored.service_thread_id == thread.service_thread_id
async def test_durable_thread_is_agent_thread(self) -> None:
"""Test that DurableAgentThread is an AgentThread."""
thread = DurableAgentThread()
assert isinstance(thread, AgentThread)
assert isinstance(thread, DurableAgentThread)
class TestModelIntegration:
"""Test suite for integration between models."""
def test_session_id_string_format(self) -> None:
"""Test that AgentSessionId string format is consistent."""
session_id = AgentSessionId.with_random_key("AgentEntity")
session_id_str = str(session_id)
assert session_id_str.startswith("@AgentEntity@")
async def test_thread_with_session_preserves_on_serialization(self) -> None:
"""Test that thread with session ID preserves it through serialization."""
session_id = AgentSessionId(name="TestAgent", key="preserved-key")
thread = DurableAgentThread.from_session_id(session_id)
# Serialize and deserialize
serialized = await thread.serialize()
restored = await DurableAgentThread.deserialize(serialized)
# Session ID should be preserved
assert restored.session_id is not None
assert restored.session_id.name == "TestAgent"
assert restored.session_id.key == "preserved-key"
if __name__ == "__main__":
pytest.main([__file__, "-v", "--tb=short"])
@@ -0,0 +1,142 @@
# Copyright (c) Microsoft. All rights reserved.
"""Unit tests for DurableAIAgentClient.
Focuses on critical client workflows: agent retrieval, protocol compliance, and integration.
Run with: pytest tests/test_client.py -v
"""
from unittest.mock import Mock
import pytest
from agent_framework import AgentProtocol
from agent_framework_durabletask import DurableAgentThread, DurableAIAgentClient
from agent_framework_durabletask._constants import DEFAULT_MAX_POLL_RETRIES, DEFAULT_POLL_INTERVAL_SECONDS
from agent_framework_durabletask._shim import DurableAIAgent
@pytest.fixture
def mock_grpc_client() -> Mock:
"""Create a mock TaskHubGrpcClient for testing."""
return Mock()
@pytest.fixture
def agent_client(mock_grpc_client: Mock) -> DurableAIAgentClient:
"""Create a DurableAIAgentClient with mock gRPC client."""
return DurableAIAgentClient(mock_grpc_client)
@pytest.fixture
def agent_client_with_custom_polling(mock_grpc_client: Mock) -> DurableAIAgentClient:
"""Create a DurableAIAgentClient with custom polling parameters."""
return DurableAIAgentClient(
mock_grpc_client,
max_poll_retries=15,
poll_interval_seconds=0.5,
)
class TestDurableAIAgentClientGetAgent:
"""Test core workflow: retrieving agents from the client."""
def test_get_agent_returns_durable_agent_shim(self, agent_client: DurableAIAgentClient) -> None:
"""Verify get_agent returns a DurableAIAgent instance."""
agent = agent_client.get_agent("assistant")
assert isinstance(agent, DurableAIAgent)
assert isinstance(agent, AgentProtocol)
def test_get_agent_shim_has_correct_name(self, agent_client: DurableAIAgentClient) -> None:
"""Verify retrieved agent has the correct name."""
agent = agent_client.get_agent("my_agent")
assert agent.name == "my_agent"
def test_get_agent_multiple_times_returns_new_instances(self, agent_client: DurableAIAgentClient) -> None:
"""Verify multiple get_agent calls return independent instances."""
agent1 = agent_client.get_agent("assistant")
agent2 = agent_client.get_agent("assistant")
assert agent1 is not agent2 # Different object instances
def test_get_agent_different_agents(self, agent_client: DurableAIAgentClient) -> None:
"""Verify client can retrieve multiple different agents."""
agent1 = agent_client.get_agent("agent1")
agent2 = agent_client.get_agent("agent2")
assert agent1.name == "agent1"
assert agent2.name == "agent2"
class TestDurableAIAgentClientIntegration:
"""Test integration scenarios between client and agent shim."""
def test_client_agent_has_working_run_method(self, agent_client: DurableAIAgentClient) -> None:
"""Verify agent from client has callable run method (even if not yet implemented)."""
agent = agent_client.get_agent("assistant")
assert hasattr(agent, "run")
assert callable(agent.run)
def test_client_agent_can_create_threads(self, agent_client: DurableAIAgentClient) -> None:
"""Verify agent from client can create DurableAgentThread instances."""
agent = agent_client.get_agent("assistant")
thread = agent.get_new_thread()
assert isinstance(thread, DurableAgentThread)
def test_client_agent_thread_with_parameters(self, agent_client: DurableAIAgentClient) -> None:
"""Verify agent can create threads with custom parameters."""
agent = agent_client.get_agent("assistant")
thread = agent.get_new_thread(service_thread_id="client-session-123")
assert isinstance(thread, DurableAgentThread)
assert thread.service_thread_id == "client-session-123"
class TestDurableAIAgentClientPollingConfiguration:
"""Test polling configuration parameters for DurableAIAgentClient."""
def test_client_uses_default_polling_parameters(self, agent_client: DurableAIAgentClient) -> None:
"""Verify client initializes with default polling parameters."""
assert agent_client.max_poll_retries == DEFAULT_MAX_POLL_RETRIES
assert agent_client.poll_interval_seconds == DEFAULT_POLL_INTERVAL_SECONDS
def test_client_accepts_custom_polling_parameters(
self, agent_client_with_custom_polling: DurableAIAgentClient
) -> None:
"""Verify client accepts and stores custom polling parameters."""
assert agent_client_with_custom_polling.max_poll_retries == 15
assert agent_client_with_custom_polling.poll_interval_seconds == 0.5
def test_client_validates_max_poll_retries(self, mock_grpc_client: Mock) -> None:
"""Verify client validates and normalizes max_poll_retries."""
# Test with zero - should enforce minimum of 1
client = DurableAIAgentClient(mock_grpc_client, max_poll_retries=0)
assert client.max_poll_retries == 1
# Test with negative - should enforce minimum of 1
client = DurableAIAgentClient(mock_grpc_client, max_poll_retries=-5)
assert client.max_poll_retries == 1
def test_client_validates_poll_interval_seconds(self, mock_grpc_client: Mock) -> None:
"""Verify client validates and normalizes poll_interval_seconds."""
# Test with zero - should use default
client = DurableAIAgentClient(mock_grpc_client, poll_interval_seconds=0)
assert client.poll_interval_seconds == DEFAULT_POLL_INTERVAL_SECONDS
# Test with negative - should use default
client = DurableAIAgentClient(mock_grpc_client, poll_interval_seconds=-0.5)
assert client.poll_interval_seconds == DEFAULT_POLL_INTERVAL_SECONDS
# Test with valid float
client = DurableAIAgentClient(mock_grpc_client, poll_interval_seconds=2.5)
assert client.poll_interval_seconds == 2.5
if __name__ == "__main__":
pytest.main([__file__, "-v", "--tb=short"])
@@ -0,0 +1,377 @@
# Copyright (c) Microsoft. All rights reserved.
"""Unit tests for DurableAgentState and related classes."""
from datetime import datetime
import pytest
from agent_framework import UsageDetails
from agent_framework_durabletask._durable_agent_state import (
DurableAgentState,
DurableAgentStateMessage,
DurableAgentStateRequest,
DurableAgentStateTextContent,
DurableAgentStateUsage,
)
from agent_framework_durabletask._models import RunRequest
class TestDurableAgentStateRequestOrchestrationId:
"""Test suite for DurableAgentStateRequest orchestration_id field."""
def test_request_with_orchestration_id(self) -> None:
"""Test creating a request with an orchestration_id."""
request = DurableAgentStateRequest(
correlation_id="corr-123",
created_at=datetime.now(),
messages=[
DurableAgentStateMessage(
role="user",
contents=[DurableAgentStateTextContent(text="test")],
)
],
orchestration_id="orch-456",
)
assert request.orchestration_id == "orch-456"
def test_request_to_dict_includes_orchestration_id(self) -> None:
"""Test that to_dict includes orchestrationId when set."""
request = DurableAgentStateRequest(
correlation_id="corr-123",
created_at=datetime.now(),
messages=[
DurableAgentStateMessage(
role="user",
contents=[DurableAgentStateTextContent(text="test")],
)
],
orchestration_id="orch-789",
)
data = request.to_dict()
assert "orchestrationId" in data
assert data["orchestrationId"] == "orch-789"
def test_request_to_dict_excludes_orchestration_id_when_none(self) -> None:
"""Test that to_dict excludes orchestrationId when not set."""
request = DurableAgentStateRequest(
correlation_id="corr-123",
created_at=datetime.now(),
messages=[
DurableAgentStateMessage(
role="user",
contents=[DurableAgentStateTextContent(text="test")],
)
],
)
data = request.to_dict()
assert "orchestrationId" not in data
def test_request_from_dict_with_orchestration_id(self) -> None:
"""Test from_dict correctly parses orchestrationId."""
data = {
"$type": "request",
"correlationId": "corr-123",
"createdAt": "2024-01-01T00:00:00Z",
"messages": [{"role": "user", "contents": [{"$type": "text", "text": "test"}]}],
"orchestrationId": "orch-from-dict",
}
request = DurableAgentStateRequest.from_dict(data)
assert request.orchestration_id == "orch-from-dict"
def test_request_from_run_request_with_orchestration_id(self) -> None:
"""Test from_run_request correctly transfers orchestration_id."""
run_request = RunRequest(
message="test message",
correlation_id="corr-run",
orchestration_id="orch-from-run-request",
)
durable_request = DurableAgentStateRequest.from_run_request(run_request)
assert durable_request.orchestration_id == "orch-from-run-request"
def test_request_from_run_request_without_orchestration_id(self) -> None:
"""Test from_run_request correctly handles missing orchestration_id."""
run_request = RunRequest(
message="test message",
correlation_id="corr-run",
)
durable_request = DurableAgentStateRequest.from_run_request(run_request)
assert durable_request.orchestration_id is None
class TestDurableAgentStateMessageCreatedAt:
"""Test suite for DurableAgentStateMessage created_at field handling."""
def test_message_from_run_request_without_created_at_preserves_none(self) -> None:
"""Test from_run_request handles auto-populated created_at from RunRequest.
When a RunRequest is created with None for created_at, RunRequest defaults it to
current UTC time. The resulting DurableAgentStateMessage should have this timestamp.
"""
run_request = RunRequest(
message="test message",
correlation_id="corr-run",
created_at=None, # RunRequest will default this to current time
)
durable_message = DurableAgentStateMessage.from_run_request(run_request)
# RunRequest auto-populates created_at, so it should not be None
assert durable_message.created_at is not None
def test_message_from_run_request_with_created_at_parses_correctly(self) -> None:
"""Test from_run_request correctly parses a valid created_at timestamp."""
run_request = RunRequest(
message="test message",
correlation_id="corr-run",
created_at=datetime(2024, 1, 15, 10, 30, 0),
)
durable_message = DurableAgentStateMessage.from_run_request(run_request)
assert durable_message.created_at is not None
assert durable_message.created_at.year == 2024
assert durable_message.created_at.month == 1
assert durable_message.created_at.day == 15
class TestDurableAgentState:
"""Test suite for DurableAgentState."""
def test_schema_version(self) -> None:
"""Test that schema version is set correctly."""
state = DurableAgentState()
assert state.schema_version == "1.1.0"
def test_to_dict_serialization(self) -> None:
"""Test that to_dict produces correct structure."""
state = DurableAgentState()
data = state.to_dict()
assert "schemaVersion" in data
assert "data" in data
assert data["schemaVersion"] == "1.1.0"
assert "conversationHistory" in data["data"]
def test_from_dict_deserialization(self) -> None:
"""Test that from_dict restores state correctly."""
original_data = {
"schemaVersion": "1.1.0",
"data": {
"conversationHistory": [
{
"$type": "request",
"correlationId": "test-123",
"createdAt": "2024-01-01T00:00:00Z",
"messages": [
{
"role": "user",
"contents": [{"$type": "text", "text": "Hello"}],
}
],
}
]
},
}
state = DurableAgentState.from_dict(original_data)
assert state.schema_version == "1.1.0"
assert len(state.data.conversation_history) == 1
assert isinstance(state.data.conversation_history[0], DurableAgentStateRequest)
def test_round_trip_serialization(self) -> None:
"""Test that round-trip serialization preserves data."""
state = DurableAgentState()
state.data.conversation_history.append(
DurableAgentStateRequest(
correlation_id="test-456",
created_at=datetime.now(),
messages=[
DurableAgentStateMessage(
role="user",
contents=[DurableAgentStateTextContent(text="Test message")],
)
],
)
)
data = state.to_dict()
restored = DurableAgentState.from_dict(data)
assert restored.schema_version == state.schema_version
assert len(restored.data.conversation_history) == len(state.data.conversation_history)
assert restored.data.conversation_history[0].correlation_id == "test-456"
class TestDurableAgentStateUsage:
"""Test suite for DurableAgentStateUsage."""
def test_usage_init_with_defaults(self) -> None:
"""Test creating usage with default values."""
usage = DurableAgentStateUsage()
assert usage.input_token_count is None
assert usage.output_token_count is None
assert usage.total_token_count is None
assert usage.extensionData is None
def test_usage_init_with_values(self) -> None:
"""Test creating usage with specific values."""
usage = DurableAgentStateUsage(
input_token_count=100,
output_token_count=200,
total_token_count=300,
extensionData={"custom_field": "value"},
)
assert usage.input_token_count == 100
assert usage.output_token_count == 200
assert usage.total_token_count == 300
assert usage.extensionData == {"custom_field": "value"}
def test_usage_to_dict(self) -> None:
"""Test that to_dict produces correct structure."""
usage = DurableAgentStateUsage(
input_token_count=50,
output_token_count=75,
total_token_count=125,
)
data = usage.to_dict()
assert data["inputTokenCount"] == 50
assert data["outputTokenCount"] == 75
assert data["totalTokenCount"] == 125
def test_usage_to_dict_with_extension_data(self) -> None:
"""Test that to_dict includes extensionData when present."""
usage = DurableAgentStateUsage(
input_token_count=10,
output_token_count=20,
total_token_count=30,
extensionData={"provider_specific": 123},
)
data = usage.to_dict()
assert "extensionData" in data
assert data["extensionData"] == {"provider_specific": 123}
def test_usage_from_dict(self) -> None:
"""Test that from_dict restores usage correctly."""
data = {
"inputTokenCount": 100,
"outputTokenCount": 200,
"totalTokenCount": 300,
"extensionData": {"extra": "data"},
}
usage = DurableAgentStateUsage.from_dict(data)
assert usage.input_token_count == 100
assert usage.output_token_count == 200
assert usage.total_token_count == 300
assert usage.extensionData == {"extra": "data"}
def test_usage_from_usage_details(self) -> None:
"""Test creating DurableAgentStateUsage from UsageDetails."""
usage_details: UsageDetails = {
"input_token_count": 150,
"output_token_count": 250,
"total_token_count": 400,
}
usage = DurableAgentStateUsage.from_usage(usage_details)
assert usage is not None
assert usage.input_token_count == 150
assert usage.output_token_count == 250
assert usage.total_token_count == 400
def test_usage_from_usage_details_with_extension_fields(self) -> None:
"""Test that non-standard fields are captured in extensionData."""
usage_details: UsageDetails = {
"input_token_count": 100,
"output_token_count": 200,
"total_token_count": 300,
}
# Add provider-specific fields (UsageDetails is a TypedDict but allows extra keys)
usage_details["prompt_tokens"] = 100 # type: ignore[typeddict-unknown-key]
usage_details["completion_tokens"] = 200 # type: ignore[typeddict-unknown-key]
usage = DurableAgentStateUsage.from_usage(usage_details)
assert usage is not None
assert usage.extensionData is not None
assert usage.extensionData["prompt_tokens"] == 100
assert usage.extensionData["completion_tokens"] == 200
def test_usage_from_usage_none(self) -> None:
"""Test that from_usage returns None for None input."""
usage = DurableAgentStateUsage.from_usage(None)
assert usage is None
def test_usage_to_usage_details(self) -> None:
"""Test converting back to UsageDetails."""
usage = DurableAgentStateUsage(
input_token_count=100,
output_token_count=200,
total_token_count=300,
)
details = usage.to_usage_details()
assert details.get("input_token_count") == 100
assert details.get("output_token_count") == 200
assert details.get("total_token_count") == 300
def test_usage_to_usage_details_with_extension_data(self) -> None:
"""Test that extensionData is merged into UsageDetails."""
usage = DurableAgentStateUsage(
input_token_count=50,
output_token_count=75,
total_token_count=125,
extensionData={"prompt_tokens": 50, "completion_tokens": 75},
)
details = usage.to_usage_details()
assert details.get("input_token_count") == 50
assert details.get("output_token_count") == 75
assert details.get("total_token_count") == 125
# Extension data should be merged into the result
assert details.get("prompt_tokens") == 50
assert details.get("completion_tokens") == 75
def test_usage_round_trip(self) -> None:
"""Test round-trip conversion from UsageDetails to DurableAgentStateUsage and back."""
original: UsageDetails = {
"input_token_count": 100,
"output_token_count": 200,
"total_token_count": 300,
}
usage = DurableAgentStateUsage.from_usage(original)
assert usage is not None
restored = usage.to_usage_details()
assert restored.get("input_token_count") == original.get("input_token_count")
assert restored.get("output_token_count") == original.get("output_token_count")
assert restored.get("total_token_count") == original.get("total_token_count")
if __name__ == "__main__":
pytest.main([__file__, "-v", "--tb=short"])
@@ -0,0 +1,695 @@
# Copyright (c) Microsoft. All rights reserved.
"""Unit tests for AgentEntity.
Run with: pytest tests/test_entities.py -v
"""
from collections.abc import AsyncIterator
from datetime import datetime
from typing import Any, TypeVar
from unittest.mock import AsyncMock, Mock
import pytest
from agent_framework import AgentResponse, AgentResponseUpdate, ChatMessage, Content, Role
from pydantic import BaseModel
from agent_framework_durabletask import (
AgentEntity,
AgentEntityStateProviderMixin,
DurableAgentState,
DurableAgentStateData,
DurableAgentStateMessage,
DurableAgentStateRequest,
DurableAgentStateTextContent,
RunRequest,
)
from agent_framework_durabletask._entities import DurableTaskEntityStateProvider
TState = TypeVar("TState")
class MockEntityContext:
"""Minimal durabletask EntityContext shim for tests."""
def __init__(self, initial_state: Any = None) -> None:
self._state = initial_state
def get_state(
self,
intended_type: type[TState] | None = None,
default: TState | None = None,
) -> Any:
del intended_type
if self._state is None:
return default
return self._state
def set_state(self, new_state: Any) -> None:
self._state = new_state
class _InMemoryStateProvider(AgentEntityStateProviderMixin):
"""Test-only state provider for AgentEntity."""
def __init__(self, *, thread_id: str, initial_state: dict[str, Any] | None = None) -> None:
self._thread_id = thread_id
self._state_dict: dict[str, Any] = initial_state or {}
def _get_state_dict(self) -> dict[str, Any]:
return self._state_dict
def _set_state_dict(self, state: dict[str, Any]) -> None:
self._state_dict = state
def _get_thread_id_from_entity(self) -> str:
return self._thread_id
def _make_entity(agent: Any, callback: Any = None, *, thread_id: str = "test-thread") -> AgentEntity:
return AgentEntity(agent, callback=callback, state_provider=_InMemoryStateProvider(thread_id=thread_id))
def _role_value(chat_message: DurableAgentStateMessage) -> str:
"""Helper to extract the string role from a ChatMessage."""
role = getattr(chat_message, "role", None)
role_value = getattr(role, "value", role)
if role_value is None:
return ""
return str(role_value)
def _agent_response(text: str | None) -> AgentResponse:
"""Create an AgentResponse with a single assistant message."""
message = (
ChatMessage(role="assistant", text=text) if text is not None else ChatMessage(role="assistant", contents=[])
)
return AgentResponse(messages=[message])
class RecordingCallback:
"""Callback implementation capturing streaming and final responses for assertions."""
def __init__(self):
self.stream_mock = AsyncMock()
self.response_mock = AsyncMock()
async def on_streaming_response_update(
self,
update: AgentResponseUpdate,
context: Any,
) -> None:
await self.stream_mock(update, context)
async def on_agent_response(self, response: AgentResponse, context: Any) -> None:
await self.response_mock(response, context)
class EntityStructuredResponse(BaseModel):
answer: float
class TestAgentEntityInit:
"""Test suite for AgentEntity initialization."""
def test_init_creates_entity(self) -> None:
"""Test that AgentEntity initializes correctly."""
mock_agent = Mock()
entity = _make_entity(mock_agent)
assert entity.agent == mock_agent
assert len(entity.state.data.conversation_history) == 0
assert entity.state.data.extension_data is None
assert entity.state.schema_version == DurableAgentState.SCHEMA_VERSION
def test_init_stores_agent_reference(self) -> None:
"""Test that the agent reference is stored correctly."""
mock_agent = Mock()
mock_agent.name = "TestAgent"
entity = _make_entity(mock_agent)
assert entity.agent.name == "TestAgent"
def test_init_with_different_agent_types(self) -> None:
"""Test initialization with different agent types."""
agent1 = Mock()
agent1.__class__.__name__ = "AzureOpenAIAgent"
agent2 = Mock()
agent2.__class__.__name__ = "CustomAgent"
entity1 = _make_entity(agent1)
entity2 = _make_entity(agent2)
assert entity1.agent.__class__.__name__ == "AzureOpenAIAgent"
assert entity2.agent.__class__.__name__ == "CustomAgent"
class TestDurableTaskEntityStateProvider:
"""Tests for DurableTaskEntityStateProvider wrapper behavior and persistence wiring."""
def _make_durabletask_entity_provider(
self,
agent: Any,
*,
initial_state: dict[str, Any] | None = None,
) -> tuple[DurableTaskEntityStateProvider, MockEntityContext]:
"""Create a DurableTaskEntityStateProvider wired to an in-memory durabletask context."""
entity = DurableTaskEntityStateProvider()
ctx = MockEntityContext(initial_state)
# DurableEntity provides this hook; required for get_state/set_state to work in unit tests.
entity._initialize_entity_context(ctx) # type: ignore[attr-defined]
return entity, ctx
def test_reset_persists_cleared_state(self) -> None:
mock_agent = Mock()
existing_state = {
"schemaVersion": "1.0.0",
"data": {
"conversationHistory": [
{
"$type": "request",
"correlationId": "corr-existing-1",
"createdAt": "2024-01-01T00:00:00Z",
"messages": [{"role": "user", "contents": [{"$type": "text", "text": "msg1"}]}],
}
]
},
}
entity, ctx = self._make_durabletask_entity_provider(mock_agent, initial_state=existing_state)
entity.reset()
persisted = ctx.get_state(dict, default={})
assert isinstance(persisted, dict)
assert persisted["data"]["conversationHistory"] == []
class TestAgentEntityRunAgent:
"""Test suite for the run_agent operation."""
async def test_run_executes_agent(self) -> None:
"""Test that run executes the agent."""
mock_agent = Mock()
mock_response = _agent_response("Test response")
mock_agent.run = AsyncMock(return_value=mock_response)
entity = _make_entity(mock_agent)
result = await entity.run({
"message": "Test message",
"correlationId": "corr-entity-1",
})
# Verify agent.run was called
mock_agent.run.assert_called_once()
_, kwargs = mock_agent.run.call_args
sent_messages: list[Any] = kwargs.get("messages")
assert len(sent_messages) == 1
sent_message = sent_messages[0]
assert isinstance(sent_message, ChatMessage)
assert getattr(sent_message, "text", None) == "Test message"
assert getattr(sent_message.role, "value", sent_message.role) == "user"
# Verify result
assert isinstance(result, AgentResponse)
assert result.text == "Test response"
async def test_run_agent_streaming_callbacks_invoked(self) -> None:
"""Ensure streaming updates trigger callbacks and run() is not used."""
updates = [
AgentResponseUpdate(text="Hello"),
AgentResponseUpdate(text=" world"),
]
async def update_generator() -> AsyncIterator[AgentResponseUpdate]:
for update in updates:
yield update
mock_agent = Mock()
mock_agent.name = "StreamingAgent"
mock_agent.run_stream = Mock(return_value=update_generator())
mock_agent.run = AsyncMock(side_effect=AssertionError("run() should not be called when streaming succeeds"))
callback = RecordingCallback()
entity = _make_entity(mock_agent, callback=callback, thread_id="session-1")
result = await entity.run(
{
"message": "Tell me something",
"correlationId": "corr-stream-1",
},
)
assert isinstance(result, AgentResponse)
assert "Hello" in result.text
assert callback.stream_mock.await_count == len(updates)
assert callback.response_mock.await_count == 1
mock_agent.run.assert_not_called()
# Validate callback arguments
stream_calls = callback.stream_mock.await_args_list
for expected_update, recorded_call in zip(updates, stream_calls, strict=True):
assert recorded_call.args[0] is expected_update
context = recorded_call.args[1]
assert context.agent_name == "StreamingAgent"
assert context.correlation_id == "corr-stream-1"
assert context.thread_id == "session-1"
assert context.request_message == "Tell me something"
final_call = callback.response_mock.await_args
assert final_call is not None
final_response, final_context = final_call.args
assert final_context.agent_name == "StreamingAgent"
assert final_context.correlation_id == "corr-stream-1"
assert final_context.thread_id == "session-1"
assert final_context.request_message == "Tell me something"
assert getattr(final_response, "text", "").strip()
async def test_run_agent_final_callback_without_streaming(self) -> None:
"""Ensure the final callback fires even when streaming is unavailable."""
mock_agent = Mock()
mock_agent.name = "NonStreamingAgent"
mock_agent.run_stream = None
agent_response = _agent_response("Final response")
mock_agent.run = AsyncMock(return_value=agent_response)
callback = RecordingCallback()
entity = _make_entity(mock_agent, callback=callback, thread_id="session-2")
result = await entity.run(
{
"message": "Hi",
"correlationId": "corr-final-1",
},
)
assert isinstance(result, AgentResponse)
assert result.text == "Final response"
assert callback.stream_mock.await_count == 0
assert callback.response_mock.await_count == 1
final_call = callback.response_mock.await_args
assert final_call is not None
assert final_call.args[0] is agent_response
final_context = final_call.args[1]
assert final_context.agent_name == "NonStreamingAgent"
assert final_context.correlation_id == "corr-final-1"
assert final_context.thread_id == "session-2"
assert final_context.request_message == "Hi"
async def test_run_agent_updates_conversation_history(self) -> None:
"""Test that run_agent updates the conversation history."""
mock_agent = Mock()
mock_response = _agent_response("Agent response")
mock_agent.run = AsyncMock(return_value=mock_response)
entity = _make_entity(mock_agent)
await entity.run({"message": "User message", "correlationId": "corr-entity-2"})
# Should have 2 entries: user message + assistant response
user_history = entity.state.data.conversation_history[0].messages
assistant_history = entity.state.data.conversation_history[1].messages
assert len(user_history) == 1
user_msg = user_history[0]
assert _role_value(user_msg) == "user"
assert user_msg.text == "User message"
assistant_msg = assistant_history[0]
assert _role_value(assistant_msg) == "assistant"
assert assistant_msg.text == "Agent response"
async def test_run_agent_increments_message_count(self) -> None:
"""Test that run_agent increments the message count."""
mock_agent = Mock()
mock_agent.run = AsyncMock(return_value=_agent_response("Response"))
entity = _make_entity(mock_agent)
assert len(entity.state.data.conversation_history) == 0
await entity.run({"message": "Message 1", "correlationId": "corr-entity-3a"})
assert len(entity.state.data.conversation_history) == 2
await entity.run({"message": "Message 2", "correlationId": "corr-entity-3b"})
assert len(entity.state.data.conversation_history) == 4
await entity.run({"message": "Message 3", "correlationId": "corr-entity-3c"})
assert len(entity.state.data.conversation_history) == 6
async def test_run_requires_entity_thread_id(self) -> None:
"""Test that AgentEntity.run rejects missing entity thread identifiers."""
mock_agent = Mock()
mock_agent.run = AsyncMock(return_value=_agent_response("Response"))
entity = _make_entity(mock_agent, thread_id="")
with pytest.raises(ValueError, match="thread_id"):
await entity.run({"message": "Message", "correlationId": "corr-entity-5"})
async def test_run_agent_multiple_conversations(self) -> None:
"""Test that run_agent maintains history across multiple messages."""
mock_agent = Mock()
mock_agent.run = AsyncMock(return_value=_agent_response("Response"))
entity = _make_entity(mock_agent)
# Send multiple messages
await entity.run({"message": "Message 1", "correlationId": "corr-entity-8a"})
await entity.run({"message": "Message 2", "correlationId": "corr-entity-8b"})
await entity.run({"message": "Message 3", "correlationId": "corr-entity-8c"})
history = entity.state.data.conversation_history
assert len(history) == 6
assert entity.state.message_count == 6
class TestAgentEntityReset:
"""Test suite for the reset operation."""
def test_reset_clears_conversation_history(self) -> None:
"""Test that reset clears the conversation history."""
mock_agent = Mock()
entity = _make_entity(mock_agent)
# Add some history with proper DurableAgentStateEntry objects
entity.state.data.conversation_history = [
DurableAgentStateRequest(
correlation_id="test-1",
created_at=datetime.now(),
messages=[
DurableAgentStateMessage(
role="user",
contents=[DurableAgentStateTextContent(text="msg1")],
)
],
),
]
entity.reset()
assert entity.state.data.conversation_history == []
def test_reset_with_extension_data(self) -> None:
"""Test that reset works when entity has extension data."""
mock_agent = Mock()
entity = _make_entity(mock_agent)
# Set up some initial state with conversation history
entity.state.data = DurableAgentStateData(conversation_history=[], extension_data={"some_key": "some_value"})
entity.reset()
assert len(entity.state.data.conversation_history) == 0
def test_reset_clears_message_count(self) -> None:
"""Test that reset clears the message count."""
mock_agent = Mock()
entity = _make_entity(mock_agent)
entity.reset()
assert len(entity.state.data.conversation_history) == 0
async def test_reset_after_conversation(self) -> None:
"""Test reset after a full conversation."""
mock_agent = Mock()
mock_agent.run = AsyncMock(return_value=_agent_response("Response"))
entity = _make_entity(mock_agent)
# Have a conversation
await entity.run({"message": "Message 1", "correlationId": "corr-entity-10a"})
await entity.run({"message": "Message 2", "correlationId": "corr-entity-10b"})
# Verify state before reset
assert entity.state.message_count == 4
assert len(entity.state.data.conversation_history) == 4
# Reset
entity.reset()
# Verify state after reset
assert entity.state.message_count == 0
assert len(entity.state.data.conversation_history) == 0
class TestErrorHandling:
"""Test suite for error handling in entities."""
async def test_run_agent_handles_agent_exception(self) -> None:
"""Test that run_agent handles agent exceptions."""
mock_agent = Mock()
mock_agent.run = AsyncMock(side_effect=Exception("Agent failed"))
entity = _make_entity(mock_agent)
result = await entity.run({"message": "Message", "correlationId": "corr-entity-error-1"})
assert isinstance(result, AgentResponse)
assert len(result.messages) == 1
content = result.messages[0].contents[0]
assert isinstance(content, Content)
assert "Agent failed" in (content.message or "")
assert content.error_code == "Exception"
async def test_run_agent_handles_value_error(self) -> None:
"""Test that run_agent handles ValueError instances."""
mock_agent = Mock()
mock_agent.run = AsyncMock(side_effect=ValueError("Invalid input"))
entity = _make_entity(mock_agent)
result = await entity.run({"message": "Message", "correlationId": "corr-entity-error-2"})
assert isinstance(result, AgentResponse)
assert len(result.messages) == 1
content = result.messages[0].contents[0]
assert isinstance(content, Content)
assert content.error_code == "ValueError"
assert "Invalid input" in str(content.message)
async def test_run_agent_handles_timeout_error(self) -> None:
"""Test that run_agent handles TimeoutError instances."""
mock_agent = Mock()
mock_agent.run = AsyncMock(side_effect=TimeoutError("Request timeout"))
entity = _make_entity(mock_agent)
result = await entity.run({"message": "Message", "correlationId": "corr-entity-error-3"})
assert isinstance(result, AgentResponse)
assert len(result.messages) == 1
content = result.messages[0].contents[0]
assert isinstance(content, Content)
assert content.error_code == "TimeoutError"
async def test_run_agent_preserves_message_on_error(self) -> None:
"""Test that run_agent preserves message information on error."""
mock_agent = Mock()
mock_agent.run = AsyncMock(side_effect=Exception("Error"))
entity = _make_entity(mock_agent)
result = await entity.run(
{"message": "Test message", "correlationId": "corr-entity-error-4"},
)
# Even on error, message info should be preserved
assert isinstance(result, AgentResponse)
assert len(result.messages) == 1
content = result.messages[0].contents[0]
assert isinstance(content, Content)
class TestConversationHistory:
"""Test suite for conversation history tracking."""
async def test_conversation_history_has_timestamps(self) -> None:
"""Test that conversation history entries include timestamps."""
mock_agent = Mock()
mock_agent.run = AsyncMock(return_value=_agent_response("Response"))
entity = _make_entity(mock_agent)
await entity.run({"message": "Message", "correlationId": "corr-entity-history-1"})
# Check both user and assistant messages have timestamps
for entry in entity.state.data.conversation_history:
timestamp = entry.created_at
assert timestamp is not None
# Verify timestamp is in ISO format
datetime.fromisoformat(str(timestamp))
async def test_conversation_history_ordering(self) -> None:
"""Test that conversation history maintains the correct order."""
mock_agent = Mock()
entity = _make_entity(mock_agent)
# Send multiple messages with different responses
mock_agent.run = AsyncMock(return_value=_agent_response("Response 1"))
await entity.run(
{"message": "Message 1", "correlationId": "corr-entity-history-2a"},
)
mock_agent.run = AsyncMock(return_value=_agent_response("Response 2"))
await entity.run(
{"message": "Message 2", "correlationId": "corr-entity-history-2b"},
)
mock_agent.run = AsyncMock(return_value=_agent_response("Response 3"))
await entity.run(
{"message": "Message 3", "correlationId": "corr-entity-history-2c"},
)
# Verify order
history = entity.state.data.conversation_history
# Each conversation turn creates 2 entries: request and response
assert history[0].messages[0].text == "Message 1" # Request 1
assert history[1].messages[0].text == "Response 1" # Response 1
assert history[2].messages[0].text == "Message 2" # Request 2
assert history[3].messages[0].text == "Response 2" # Response 2
assert history[4].messages[0].text == "Message 3" # Request 3
assert history[5].messages[0].text == "Response 3" # Response 3
async def test_conversation_history_role_alternation(self) -> None:
"""Test that conversation history alternates between user and assistant roles."""
mock_agent = Mock()
mock_agent.run = AsyncMock(return_value=_agent_response("Response"))
entity = _make_entity(mock_agent)
await entity.run(
{"message": "Message 1", "correlationId": "corr-entity-history-3a"},
)
await entity.run(
{"message": "Message 2", "correlationId": "corr-entity-history-3b"},
)
# Check role alternation
history = entity.state.data.conversation_history
# Each conversation turn creates 2 entries: request and response
assert history[0].messages[0].role == "user" # Request 1
assert history[1].messages[0].role == "assistant" # Response 1
assert history[2].messages[0].role == "user" # Request 2
assert history[3].messages[0].role == "assistant" # Response 2
class TestRunRequestSupport:
"""Test suite for RunRequest support in entities."""
async def test_run_agent_with_run_request_object(self) -> None:
"""Test run_agent with a RunRequest object."""
mock_agent = Mock()
mock_agent.run = AsyncMock(return_value=_agent_response("Response"))
entity = _make_entity(mock_agent)
request = RunRequest(
message="Test message",
role=Role.USER,
enable_tool_calls=True,
correlation_id="corr-runreq-1",
)
result = await entity.run(request)
assert isinstance(result, AgentResponse)
assert result.text == "Response"
async def test_run_agent_with_dict_request(self) -> None:
"""Test run_agent with a dictionary request."""
mock_agent = Mock()
mock_agent.run = AsyncMock(return_value=_agent_response("Response"))
entity = _make_entity(mock_agent)
request_dict = {
"message": "Test message",
"role": "system",
"enable_tool_calls": False,
"correlationId": "corr-runreq-2",
}
result = await entity.run(request_dict)
assert isinstance(result, AgentResponse)
assert result.text == "Response"
async def test_run_agent_with_string_raises_without_correlation(self) -> None:
"""Test that run_agent rejects legacy string input without correlation ID."""
mock_agent = Mock()
mock_agent.run = AsyncMock(return_value=_agent_response("Response"))
entity = _make_entity(mock_agent)
with pytest.raises(ValueError):
await entity.run("Simple message")
async def test_run_agent_stores_role_in_history(self) -> None:
"""Test that run_agent stores the role in conversation history."""
mock_agent = Mock()
mock_agent.run = AsyncMock(return_value=_agent_response("Response"))
entity = _make_entity(mock_agent)
# Send as system role
request = RunRequest(
message="System message",
role=Role.SYSTEM,
correlation_id="corr-runreq-3",
)
await entity.run(request)
# Check that system role was stored
history = entity.state.data.conversation_history
assert history[0].messages[0].role == "system"
assert history[0].messages[0].text == "System message"
async def test_run_agent_with_response_format(self) -> None:
"""Test run_agent with a JSON response format."""
mock_agent = Mock()
# Return JSON response
mock_agent.run = AsyncMock(return_value=_agent_response('{"answer": 42}'))
entity = _make_entity(mock_agent)
request = RunRequest(
message="What is the answer?",
response_format=EntityStructuredResponse,
correlation_id="corr-runreq-4",
)
result = await entity.run(request)
assert isinstance(result, AgentResponse)
assert result.text == '{"answer": 42}'
assert result.value is None
async def test_run_agent_disable_tool_calls(self) -> None:
"""Test run_agent with tool calls disabled."""
mock_agent = Mock()
mock_agent.run = AsyncMock(return_value=_agent_response("Response"))
entity = _make_entity(mock_agent)
request = RunRequest(message="Test", enable_tool_calls=False, correlation_id="corr-runreq-5")
result = await entity.run(request)
assert isinstance(result, AgentResponse)
# Agent should have been called (tool disabling is framework-dependent)
mock_agent.run.assert_called_once()
if __name__ == "__main__":
pytest.main([__file__, "-v", "--tb=short"])
@@ -0,0 +1,571 @@
# Copyright (c) Microsoft. All rights reserved.
"""Unit tests for DurableAgentExecutor implementations.
Focuses on critical behavioral flows for executor strategies.
Run with: pytest tests/test_executors.py -v
"""
import time
from typing import Any
from unittest.mock import Mock
import pytest
from agent_framework import AgentResponse, Role
from durabletask.entities import EntityInstanceId
from durabletask.task import Task
from pydantic import BaseModel
from agent_framework_durabletask import DurableAgentThread
from agent_framework_durabletask._constants import DEFAULT_MAX_POLL_RETRIES, DEFAULT_POLL_INTERVAL_SECONDS
from agent_framework_durabletask._executors import (
ClientAgentExecutor,
DurableAgentTask,
OrchestrationAgentExecutor,
)
from agent_framework_durabletask._models import AgentSessionId, RunRequest
# Fixtures
@pytest.fixture
def mock_client() -> Mock:
"""Provide a mock client for ClientAgentExecutor tests."""
client = Mock()
client.signal_entity = Mock()
client.get_entity = Mock(return_value=None)
return client
@pytest.fixture
def mock_entity_task() -> Mock:
"""Provide a mock entity task."""
task = Mock(spec=Task)
task.is_complete = False
task.is_failed = False
return task
@pytest.fixture
def mock_orchestration_context(mock_entity_task: Mock) -> Mock:
"""Provide a mock orchestration context with call_entity configured."""
context = Mock()
context.call_entity = Mock(return_value=mock_entity_task)
return context
@pytest.fixture
def sample_run_request() -> RunRequest:
"""Provide a sample RunRequest for tests."""
return RunRequest(message="test message", correlation_id="test-123")
@pytest.fixture
def client_executor(mock_client: Mock) -> ClientAgentExecutor:
"""Provide a ClientAgentExecutor with minimal polling for fast tests."""
return ClientAgentExecutor(mock_client, max_poll_retries=1, poll_interval_seconds=0.01)
@pytest.fixture
def orchestration_executor(mock_orchestration_context: Mock) -> OrchestrationAgentExecutor:
"""Provide an OrchestrationAgentExecutor."""
return OrchestrationAgentExecutor(mock_orchestration_context)
@pytest.fixture
def successful_agent_response() -> dict[str, Any]:
"""Provide a successful agent response dictionary."""
return {
"messages": [{"role": "assistant", "contents": [{"type": "text", "text": "Hello!"}]}],
"created_at": "2025-12-30T10:00:00Z",
}
@pytest.fixture
def configure_successful_entity_task(mock_entity_task: Mock) -> Any:
"""Provide a helper to configure mock_entity_task with a successful response."""
def _configure(response: dict[str, Any]) -> Mock:
mock_entity_task.is_failed = False
mock_entity_task.is_complete = False
mock_entity_task.get_result = Mock(return_value=response)
return mock_entity_task
return _configure
@pytest.fixture
def configure_failed_entity_task(mock_entity_task: Mock) -> Any:
"""Provide a helper to configure mock_entity_task with a failure."""
def _configure(exception: Exception) -> Mock:
mock_entity_task.is_failed = True
mock_entity_task.is_complete = True
mock_entity_task.get_exception = Mock(return_value=exception)
return mock_entity_task
return _configure
class TestExecutorThreadCreation:
"""Test that executors properly create DurableAgentThread with parameters."""
def test_client_executor_creates_durable_thread(self, mock_client: Mock) -> None:
"""Verify ClientAgentExecutor creates DurableAgentThread instances."""
executor = ClientAgentExecutor(mock_client)
thread = executor.get_new_thread("test_agent")
assert isinstance(thread, DurableAgentThread)
def test_client_executor_forwards_kwargs_to_thread(self, mock_client: Mock) -> None:
"""Verify ClientAgentExecutor forwards kwargs to DurableAgentThread creation."""
executor = ClientAgentExecutor(mock_client)
thread = executor.get_new_thread("test_agent", service_thread_id="client-123")
assert isinstance(thread, DurableAgentThread)
assert thread.service_thread_id == "client-123"
def test_orchestration_executor_creates_durable_thread(
self, orchestration_executor: OrchestrationAgentExecutor
) -> None:
"""Verify OrchestrationAgentExecutor creates DurableAgentThread instances."""
thread = orchestration_executor.get_new_thread("test_agent")
assert isinstance(thread, DurableAgentThread)
def test_orchestration_executor_forwards_kwargs_to_thread(
self, orchestration_executor: OrchestrationAgentExecutor
) -> None:
"""Verify OrchestrationAgentExecutor forwards kwargs to DurableAgentThread creation."""
thread = orchestration_executor.get_new_thread("test_agent", service_thread_id="orch-456")
assert isinstance(thread, DurableAgentThread)
assert thread.service_thread_id == "orch-456"
class TestClientAgentExecutorRun:
"""Test that ClientAgentExecutor.run_durable_agent works as implemented."""
def test_client_executor_run_returns_response(
self, client_executor: ClientAgentExecutor, sample_run_request: RunRequest
) -> None:
"""Verify ClientAgentExecutor.run_durable_agent returns AgentResponse (synchronous)."""
result = client_executor.run_durable_agent("test_agent", sample_run_request)
# Verify it returns an AgentResponse (synchronous, not a coroutine)
assert isinstance(result, AgentResponse)
assert result is not None
class TestClientAgentExecutorPollingConfiguration:
"""Test polling configuration parameters for ClientAgentExecutor."""
def test_executor_uses_default_polling_parameters(self, mock_client: Mock) -> None:
"""Verify executor initializes with default polling parameters."""
executor = ClientAgentExecutor(mock_client)
assert executor.max_poll_retries == DEFAULT_MAX_POLL_RETRIES
assert executor.poll_interval_seconds == DEFAULT_POLL_INTERVAL_SECONDS
def test_executor_accepts_custom_polling_parameters(self, mock_client: Mock) -> None:
"""Verify executor accepts and stores custom polling parameters."""
executor = ClientAgentExecutor(mock_client, max_poll_retries=20, poll_interval_seconds=0.5)
assert executor.max_poll_retries == 20
assert executor.poll_interval_seconds == 0.5
def test_executor_respects_custom_max_poll_retries(self, mock_client: Mock, sample_run_request: RunRequest) -> None:
"""Verify executor respects custom max_poll_retries during polling."""
# Create executor with only 2 retries
executor = ClientAgentExecutor(mock_client, max_poll_retries=2, poll_interval_seconds=0.01)
# Run the agent
result = executor.run_durable_agent("test_agent", sample_run_request)
# Verify it returns AgentResponse (should timeout after 2 attempts)
assert isinstance(result, AgentResponse)
# Verify get_entity was called 2 times (max_poll_retries)
assert mock_client.get_entity.call_count == 2
def test_executor_respects_custom_poll_interval(self, mock_client: Mock, sample_run_request: RunRequest) -> None:
"""Verify executor respects custom poll_interval_seconds during polling."""
# Create executor with very short interval
executor = ClientAgentExecutor(mock_client, max_poll_retries=3, poll_interval_seconds=0.01)
# Measure time taken
start = time.time()
result = executor.run_durable_agent("test_agent", sample_run_request)
elapsed = time.time() - start
# Should take roughly 3 * 0.01 = 0.03 seconds (plus overhead)
# Be generous with timing to avoid flakiness
assert elapsed < 0.2 # Should be quick with 0.01 interval
assert isinstance(result, AgentResponse)
class TestClientAgentExecutorFireAndForget:
"""Test fire-and-forget mode (wait_for_response=False) for ClientAgentExecutor."""
def test_fire_and_forget_returns_immediately(self, mock_client: Mock) -> None:
"""Verify wait_for_response=False returns immediately without polling."""
executor = ClientAgentExecutor(mock_client, max_poll_retries=10, poll_interval_seconds=0.1)
# Create a request with wait_for_response=False
request = RunRequest(message="test message", correlation_id="test-123", wait_for_response=False)
# Measure time taken
start = time.time()
result = executor.run_durable_agent("test_agent", request)
elapsed = time.time() - start
# Should return immediately without polling (elapsed time should be very small)
assert elapsed < 0.1 # Much faster than any polling would take
# Should return an AgentResponse
assert isinstance(result, AgentResponse)
# Should have signaled the entity but not polled
assert mock_client.signal_entity.call_count == 1
assert mock_client.get_entity.call_count == 0 # No polling occurred
def test_fire_and_forget_returns_empty_response(self, mock_client: Mock) -> None:
"""Verify wait_for_response=False returns an acceptance message with correlation ID."""
executor = ClientAgentExecutor(mock_client)
request = RunRequest(message="test message", correlation_id="test-456", wait_for_response=False)
result = executor.run_durable_agent("test_agent", request)
# Verify it contains an acceptance message
assert isinstance(result, AgentResponse)
assert len(result.messages) == 1
assert result.messages[0].role == Role.SYSTEM
# Check message contains key information
message_text = result.messages[0].text
assert "accepted" in message_text.lower()
assert "test-456" in message_text # Contains correlation ID
assert "background" in message_text.lower()
class TestOrchestrationAgentExecutorFireAndForget:
"""Test fire-and-forget mode for OrchestrationAgentExecutor."""
def test_orchestration_fire_and_forget_calls_signal_entity(self, mock_orchestration_context: Mock) -> None:
"""Verify wait_for_response=False calls signal_entity instead of call_entity."""
executor = OrchestrationAgentExecutor(mock_orchestration_context)
mock_orchestration_context.signal_entity = Mock()
request = RunRequest(message="test", correlation_id="test-123", wait_for_response=False)
result = executor.run_durable_agent("test_agent", request)
# Verify signal_entity was called and call_entity was not
assert mock_orchestration_context.signal_entity.call_count == 1
assert mock_orchestration_context.call_entity.call_count == 0
# Should still return a DurableAgentTask
assert isinstance(result, DurableAgentTask)
def test_orchestration_fire_and_forget_returns_completed_task(self, mock_orchestration_context: Mock) -> None:
"""Verify wait_for_response=False returns pre-completed DurableAgentTask."""
executor = OrchestrationAgentExecutor(mock_orchestration_context)
mock_orchestration_context.signal_entity = Mock()
request = RunRequest(message="test", correlation_id="test-456", wait_for_response=False)
result = executor.run_durable_agent("test_agent", request)
# Task should be immediately complete
assert isinstance(result, DurableAgentTask)
assert result.is_complete
def test_orchestration_fire_and_forget_returns_acceptance_response(self, mock_orchestration_context: Mock) -> None:
"""Verify wait_for_response=False returns acceptance response."""
executor = OrchestrationAgentExecutor(mock_orchestration_context)
mock_orchestration_context.signal_entity = Mock()
request = RunRequest(message="test", correlation_id="test-789", wait_for_response=False)
result = executor.run_durable_agent("test_agent", request)
# Get the result
response = result.get_result()
assert isinstance(response, AgentResponse)
assert len(response.messages) == 1
assert response.messages[0].role == Role.SYSTEM
assert "test-789" in response.messages[0].text
def test_orchestration_blocking_mode_calls_call_entity(self, mock_orchestration_context: Mock) -> None:
"""Verify wait_for_response=True uses call_entity as before."""
executor = OrchestrationAgentExecutor(mock_orchestration_context)
mock_orchestration_context.signal_entity = Mock()
request = RunRequest(message="test", correlation_id="test-abc", wait_for_response=True)
result = executor.run_durable_agent("test_agent", request)
# Verify call_entity was called and signal_entity was not
assert mock_orchestration_context.call_entity.call_count == 1
assert mock_orchestration_context.signal_entity.call_count == 0
# Should return a DurableAgentTask
assert isinstance(result, DurableAgentTask)
class TestOrchestrationAgentExecutorRun:
"""Test OrchestrationAgentExecutor.run_durable_agent implementation."""
def test_orchestration_executor_run_returns_durable_agent_task(
self, orchestration_executor: OrchestrationAgentExecutor, sample_run_request: RunRequest
) -> None:
"""Verify OrchestrationAgentExecutor.run_durable_agent returns DurableAgentTask."""
result = orchestration_executor.run_durable_agent("test_agent", sample_run_request)
assert isinstance(result, DurableAgentTask)
def test_orchestration_executor_calls_entity_with_correct_parameters(
self,
mock_orchestration_context: Mock,
orchestration_executor: OrchestrationAgentExecutor,
sample_run_request: RunRequest,
) -> None:
"""Verify call_entity is invoked with correct entity ID and request."""
orchestration_executor.run_durable_agent("test_agent", sample_run_request)
# Verify call_entity was called once
assert mock_orchestration_context.call_entity.call_count == 1
# Get the call arguments
call_args = mock_orchestration_context.call_entity.call_args
entity_id_arg = call_args[0][0]
operation_arg = call_args[0][1]
request_dict_arg = call_args[0][2]
# Verify entity ID
assert isinstance(entity_id_arg, EntityInstanceId)
assert entity_id_arg.entity == "dafx-test_agent"
# Verify operation name
assert operation_arg == "run"
# Verify request dict
assert request_dict_arg == sample_run_request.to_dict()
def test_orchestration_executor_uses_thread_session_id(
self,
mock_orchestration_context: Mock,
orchestration_executor: OrchestrationAgentExecutor,
sample_run_request: RunRequest,
) -> None:
"""Verify executor uses thread's session ID when provided."""
# Create thread with specific session ID
session_id = AgentSessionId(name="test_agent", key="specific-key-123")
thread = DurableAgentThread.from_session_id(session_id)
result = orchestration_executor.run_durable_agent("test_agent", sample_run_request, thread=thread)
# Verify call_entity was called with the specific key
call_args = mock_orchestration_context.call_entity.call_args
entity_id_arg = call_args[0][0]
assert entity_id_arg.key == "specific-key-123"
assert isinstance(result, DurableAgentTask)
class TestDurableAgentTask:
"""Test DurableAgentTask completion and response transformation."""
def test_durable_agent_task_transforms_successful_result(
self, configure_successful_entity_task: Any, successful_agent_response: dict[str, Any]
) -> None:
"""Verify DurableAgentTask converts successful entity result to AgentResponse."""
mock_entity_task = configure_successful_entity_task(successful_agent_response)
task = DurableAgentTask(entity_task=mock_entity_task, response_format=None, correlation_id="test-123")
# Simulate child task completion
task.on_child_completed(mock_entity_task)
assert task.is_complete
result = task.get_result()
assert isinstance(result, AgentResponse)
assert len(result.messages) == 1
assert result.messages[0].role == Role.ASSISTANT
def test_durable_agent_task_propagates_failure(self, configure_failed_entity_task: Any) -> None:
"""Verify DurableAgentTask propagates task failures."""
mock_entity_task = configure_failed_entity_task(ValueError("Entity error"))
task = DurableAgentTask(entity_task=mock_entity_task, response_format=None, correlation_id="test-123")
# Simulate child task completion with failure
task.on_child_completed(mock_entity_task)
assert task.is_complete
assert task.is_failed
# The exception is wrapped in TaskFailedError by the durabletask library
exception = task.get_exception()
assert exception is not None
def test_durable_agent_task_validates_response_format(self, configure_successful_entity_task: Any) -> None:
"""Verify DurableAgentTask validates response format when provided."""
response = {
"messages": [{"role": "assistant", "contents": [{"type": "text", "text": '{"answer": "42"}'}]}],
"created_at": "2025-12-30T10:00:00Z",
}
mock_entity_task = configure_successful_entity_task(response)
class TestResponse(BaseModel):
answer: str
task = DurableAgentTask(entity_task=mock_entity_task, response_format=TestResponse, correlation_id="test-123")
# Simulate child task completion
task.on_child_completed(mock_entity_task)
assert task.is_complete
result = task.get_result()
assert isinstance(result, AgentResponse)
def test_durable_agent_task_ignores_duplicate_completion(
self, configure_successful_entity_task: Any, successful_agent_response: dict[str, Any]
) -> None:
"""Verify DurableAgentTask ignores duplicate completion calls."""
mock_entity_task = configure_successful_entity_task(successful_agent_response)
task = DurableAgentTask(entity_task=mock_entity_task, response_format=None, correlation_id="test-123")
# Simulate child task completion twice
task.on_child_completed(mock_entity_task)
first_result = task.get_result()
task.on_child_completed(mock_entity_task)
second_result = task.get_result()
# Should be the same result, get_result should only be called once
assert first_result is second_result
assert mock_entity_task.get_result.call_count == 1
def test_durable_agent_task_fails_on_malformed_response(self, configure_successful_entity_task: Any) -> None:
"""Verify DurableAgentTask fails when entity returns malformed response data."""
# Use data that will cause AgentResponse.from_dict to fail
# Using a list instead of dict, or other invalid structure
mock_entity_task = configure_successful_entity_task("invalid string response")
task = DurableAgentTask(entity_task=mock_entity_task, response_format=None, correlation_id="test-123")
# Simulate child task completion with malformed data
task.on_child_completed(mock_entity_task)
assert task.is_complete
assert task.is_failed
def test_durable_agent_task_fails_on_invalid_response_format(self, configure_successful_entity_task: Any) -> None:
"""Verify DurableAgentTask fails when response doesn't match required format."""
response = {
"messages": [{"role": "assistant", "contents": [{"type": "text", "text": '{"wrong": "field"}'}]}],
"created_at": "2025-12-30T10:00:00Z",
}
mock_entity_task = configure_successful_entity_task(response)
class StrictResponse(BaseModel):
required_field: str
task = DurableAgentTask(entity_task=mock_entity_task, response_format=StrictResponse, correlation_id="test-123")
# Simulate child task completion with wrong format
task.on_child_completed(mock_entity_task)
assert task.is_complete
assert task.is_failed
def test_durable_agent_task_handles_empty_response(self, configure_successful_entity_task: Any) -> None:
"""Verify DurableAgentTask handles response with empty messages list."""
response: dict[str, str | list[Any]] = {
"messages": [],
"created_at": "2025-12-30T10:00:00Z",
}
mock_entity_task = configure_successful_entity_task(response)
task = DurableAgentTask(entity_task=mock_entity_task, response_format=None, correlation_id="test-123")
# Simulate child task completion
task.on_child_completed(mock_entity_task)
assert task.is_complete
result = task.get_result()
assert isinstance(result, AgentResponse)
assert len(result.messages) == 0
def test_durable_agent_task_handles_multiple_messages(self, configure_successful_entity_task: Any) -> None:
"""Verify DurableAgentTask correctly processes response with multiple messages."""
response = {
"messages": [
{"role": "assistant", "contents": [{"type": "text", "text": "First message"}]},
{"role": "assistant", "contents": [{"type": "text", "text": "Second message"}]},
],
"created_at": "2025-12-30T10:00:00Z",
}
mock_entity_task = configure_successful_entity_task(response)
task = DurableAgentTask(entity_task=mock_entity_task, response_format=None, correlation_id="test-123")
# Simulate child task completion
task.on_child_completed(mock_entity_task)
assert task.is_complete
result = task.get_result()
assert isinstance(result, AgentResponse)
assert len(result.messages) == 2
assert result.messages[0].role == Role.ASSISTANT
assert result.messages[1].role == Role.ASSISTANT
def test_durable_agent_task_is_not_complete_initially(self, mock_entity_task: Mock) -> None:
"""Verify DurableAgentTask is not complete when first created."""
task = DurableAgentTask(entity_task=mock_entity_task, response_format=None, correlation_id="test-123")
assert not task.is_complete
assert not task.is_failed
def test_durable_agent_task_completes_with_complex_response_format(
self, configure_successful_entity_task: Any
) -> None:
"""Verify DurableAgentTask validates complex nested response formats correctly."""
response = {
"messages": [
{
"role": "assistant",
"contents": [
{
"type": "text",
"text": '{"name": "test", "count": 42, "items": ["a", "b", "c"]}',
}
],
}
],
"created_at": "2025-12-30T10:00:00Z",
}
mock_entity_task = configure_successful_entity_task(response)
class ComplexResponse(BaseModel):
name: str
count: int
items: list[str]
task = DurableAgentTask(
entity_task=mock_entity_task, response_format=ComplexResponse, correlation_id="test-123"
)
# Simulate child task completion
task.on_child_completed(mock_entity_task)
assert task.is_complete
assert not task.is_failed
result = task.get_result()
assert isinstance(result, AgentResponse)
if __name__ == "__main__":
pytest.main([__file__, "-v", "--tb=short"])
@@ -0,0 +1,310 @@
# Copyright (c) Microsoft. All rights reserved.
"""Unit tests for data models (RunRequest)."""
import pytest
from agent_framework import Role
from pydantic import BaseModel
from agent_framework_durabletask._models import RunRequest
class ModuleStructuredResponse(BaseModel):
value: int
class TestRunRequest:
"""Test suite for RunRequest."""
def test_init_with_defaults(self) -> None:
"""Test RunRequest initialization with defaults."""
request = RunRequest(message="Hello", correlation_id="corr-001")
assert request.message == "Hello"
assert request.correlation_id == "corr-001"
assert request.role == Role.USER
assert request.response_format is None
assert request.enable_tool_calls is True
assert request.wait_for_response is True
def test_init_with_all_fields(self) -> None:
"""Test RunRequest initialization with all fields."""
schema = ModuleStructuredResponse
request = RunRequest(
message="Hello",
correlation_id="corr-002",
role=Role.SYSTEM,
response_format=schema,
enable_tool_calls=False,
wait_for_response=False,
)
assert request.message == "Hello"
assert request.correlation_id == "corr-002"
assert request.role == Role.SYSTEM
assert request.response_format is schema
assert request.enable_tool_calls is False
assert request.wait_for_response is False
def test_init_coerces_string_role(self) -> None:
"""Ensure string role values are coerced into Role instances."""
request = RunRequest(message="Hello", correlation_id="corr-003", role="system") # type: ignore[arg-type]
assert request.role == Role.SYSTEM
def test_to_dict_with_defaults(self) -> None:
"""Test to_dict with default values."""
request = RunRequest(message="Test message", correlation_id="corr-004")
data = request.to_dict()
assert data["message"] == "Test message"
assert data["enable_tool_calls"] is True
assert data["wait_for_response"] is True
assert data["role"] == "user"
assert data["correlationId"] == "corr-004"
assert "response_format" not in data or data["response_format"] is None
assert "thread_id" not in data
def test_to_dict_with_all_fields(self) -> None:
"""Test to_dict with all fields."""
schema = ModuleStructuredResponse
request = RunRequest(
message="Hello",
correlation_id="corr-005",
role=Role.ASSISTANT,
response_format=schema,
enable_tool_calls=False,
wait_for_response=False,
)
data = request.to_dict()
assert data["message"] == "Hello"
assert data["correlationId"] == "corr-005"
assert data["role"] == "assistant"
assert data["response_format"]["__response_schema_type__"] == "pydantic_model"
assert data["response_format"]["module"] == schema.__module__
assert data["response_format"]["qualname"] == schema.__qualname__
assert data["enable_tool_calls"] is False
assert data["wait_for_response"] is False
assert "thread_id" not in data
def test_from_dict_with_defaults(self) -> None:
"""Test from_dict with minimal data."""
data = {"message": "Hello", "correlationId": "corr-006"}
request = RunRequest.from_dict(data)
assert request.message == "Hello"
assert request.correlation_id == "corr-006"
assert request.role == Role.USER
assert request.enable_tool_calls is True
assert request.wait_for_response is True
def test_from_dict_ignores_thread_id_field(self) -> None:
"""Ensure legacy thread_id input does not break RunRequest parsing."""
request = RunRequest.from_dict({"message": "Hello", "correlationId": "corr-007", "thread_id": "ignored"})
assert request.message == "Hello"
def test_from_dict_with_all_fields(self) -> None:
"""Test from_dict with all fields."""
data = {
"message": "Test",
"correlationId": "corr-008",
"role": "system",
"response_format": {
"__response_schema_type__": "pydantic_model",
"module": ModuleStructuredResponse.__module__,
"qualname": ModuleStructuredResponse.__qualname__,
},
"enable_tool_calls": False,
}
request = RunRequest.from_dict(data)
assert request.message == "Test"
assert request.correlation_id == "corr-008"
assert request.role == Role.SYSTEM
assert request.response_format is ModuleStructuredResponse
assert request.enable_tool_calls is False
def test_from_dict_unknown_role_preserves_value(self) -> None:
"""Test from_dict keeps custom roles intact."""
data = {"message": "Test", "correlationId": "corr-009", "role": "reviewer"}
request = RunRequest.from_dict(data)
assert request.role.value == "reviewer"
assert request.role != Role.USER
def test_from_dict_empty_message(self) -> None:
"""Test from_dict with empty message."""
request = RunRequest.from_dict({"correlationId": "corr-010"})
assert request.message == ""
assert request.correlation_id == "corr-010"
assert request.role == Role.USER
def test_from_dict_missing_correlation_id_raises(self) -> None:
"""Test from_dict raises when correlationId is missing."""
with pytest.raises(ValueError, match="correlationId is required"):
RunRequest.from_dict({"message": "Test"})
def test_round_trip_dict_conversion(self) -> None:
"""Test round-trip to_dict and from_dict."""
original = RunRequest(
message="Test message",
correlation_id="corr-011",
role=Role.SYSTEM,
response_format=ModuleStructuredResponse,
enable_tool_calls=False,
)
data = original.to_dict()
restored = RunRequest.from_dict(data)
assert restored.message == original.message
assert restored.correlation_id == original.correlation_id
assert restored.role == original.role
assert restored.response_format is ModuleStructuredResponse
assert restored.enable_tool_calls == original.enable_tool_calls
def test_round_trip_with_pydantic_response_format(self) -> None:
"""Ensure Pydantic response formats serialize and deserialize properly."""
original = RunRequest(
message="Structured",
correlation_id="corr-012",
response_format=ModuleStructuredResponse,
)
data = original.to_dict()
assert data["response_format"]["__response_schema_type__"] == "pydantic_model"
assert data["response_format"]["module"] == ModuleStructuredResponse.__module__
assert data["response_format"]["qualname"] == ModuleStructuredResponse.__qualname__
restored = RunRequest.from_dict(data)
assert restored.response_format is ModuleStructuredResponse
def test_round_trip_with_options(self) -> None:
"""Ensure options are preserved and response_format is deserialized."""
original = RunRequest(
message="Test",
correlation_id="corr-opts-1",
response_format=ModuleStructuredResponse,
enable_tool_calls=False,
options={
"response_format": ModuleStructuredResponse,
"enable_tool_calls": False,
"custom": "value",
},
)
data = original.to_dict()
assert data["options"]["custom"] == "value"
restored = RunRequest.from_dict(data)
assert restored.options is not None
assert restored.options["custom"] == "value"
assert restored.options["response_format"] is ModuleStructuredResponse
def test_init_with_correlationId(self) -> None:
"""Test RunRequest initialization with correlationId."""
request = RunRequest(message="Test message", correlation_id="corr-123")
assert request.message == "Test message"
assert request.correlation_id == "corr-123"
def test_to_dict_with_correlationId(self) -> None:
"""Test to_dict includes correlationId."""
request = RunRequest(message="Test", correlation_id="corr-456")
data = request.to_dict()
assert data["message"] == "Test"
assert data["correlationId"] == "corr-456"
def test_from_dict_with_correlationId(self) -> None:
"""Test from_dict with correlationId."""
data = {"message": "Test", "correlationId": "corr-789"}
request = RunRequest.from_dict(data)
assert request.message == "Test"
assert request.correlation_id == "corr-789"
def test_round_trip_with_correlationId(self) -> None:
"""Test round-trip to_dict and from_dict with correlationId."""
original = RunRequest(
message="Test message",
role=Role.SYSTEM,
correlation_id="corr-124",
)
data = original.to_dict()
restored = RunRequest.from_dict(data)
assert restored.message == original.message
assert restored.role == original.role
assert restored.correlation_id == original.correlation_id
def test_init_with_orchestration_id(self) -> None:
"""Test RunRequest initialization with orchestration_id."""
request = RunRequest(
message="Test message",
correlation_id="corr-125",
orchestration_id="orch-123",
)
assert request.message == "Test message"
assert request.orchestration_id == "orch-123"
def test_to_dict_with_orchestration_id(self) -> None:
"""Test to_dict includes orchestrationId."""
request = RunRequest(
message="Test",
correlation_id="corr-126",
orchestration_id="orch-456",
)
data = request.to_dict()
assert data["message"] == "Test"
assert data["orchestrationId"] == "orch-456"
def test_to_dict_excludes_orchestration_id_when_none(self) -> None:
"""Test to_dict excludes orchestrationId when not set."""
request = RunRequest(
message="Test",
correlation_id="corr-127",
)
data = request.to_dict()
assert "orchestrationId" not in data
def test_from_dict_with_orchestration_id(self) -> None:
"""Test from_dict with orchestrationId."""
data = {
"message": "Test",
"correlationId": "corr-128",
"orchestrationId": "orch-789",
}
request = RunRequest.from_dict(data)
assert request.message == "Test"
assert request.orchestration_id == "orch-789"
def test_round_trip_with_orchestration_id(self) -> None:
"""Test round-trip to_dict and from_dict with orchestration_id."""
original = RunRequest(
message="Test message",
role=Role.SYSTEM,
correlation_id="corr-129",
orchestration_id="orch-123",
)
data = original.to_dict()
restored = RunRequest.from_dict(data)
assert restored.message == original.message
assert restored.role == original.role
assert restored.correlation_id == original.correlation_id
assert restored.orchestration_id == original.orchestration_id
if __name__ == "__main__":
pytest.main([__file__, "-v", "--tb=short"])
@@ -0,0 +1,98 @@
# Copyright (c) Microsoft. All rights reserved.
"""Unit tests for DurableAIAgentOrchestrationContext.
Focuses on critical orchestration workflows: agent retrieval and integration.
Run with: pytest tests/test_orchestration_context.py -v
"""
from unittest.mock import Mock
import pytest
from agent_framework import AgentProtocol
from agent_framework_durabletask import DurableAgentThread
from agent_framework_durabletask._orchestration_context import DurableAIAgentOrchestrationContext
from agent_framework_durabletask._shim import DurableAIAgent
@pytest.fixture
def mock_orchestration_context() -> Mock:
"""Create a mock OrchestrationContext for testing."""
return Mock()
@pytest.fixture
def agent_context(mock_orchestration_context: Mock) -> DurableAIAgentOrchestrationContext:
"""Create a DurableAIAgentOrchestrationContext with mock context."""
return DurableAIAgentOrchestrationContext(mock_orchestration_context)
class TestDurableAIAgentOrchestrationContextGetAgent:
"""Test core workflow: retrieving agents from orchestration context."""
def test_get_agent_returns_durable_agent_shim(self, agent_context: DurableAIAgentOrchestrationContext) -> None:
"""Verify get_agent returns a DurableAIAgent instance."""
agent = agent_context.get_agent("assistant")
assert isinstance(agent, DurableAIAgent)
assert isinstance(agent, AgentProtocol)
def test_get_agent_shim_has_correct_name(self, agent_context: DurableAIAgentOrchestrationContext) -> None:
"""Verify retrieved agent has the correct name."""
agent = agent_context.get_agent("my_agent")
assert agent.name == "my_agent"
def test_get_agent_multiple_times_returns_new_instances(
self, agent_context: DurableAIAgentOrchestrationContext
) -> None:
"""Verify multiple get_agent calls return independent instances."""
agent1 = agent_context.get_agent("assistant")
agent2 = agent_context.get_agent("assistant")
assert agent1 is not agent2 # Different object instances
def test_get_agent_different_agents(self, agent_context: DurableAIAgentOrchestrationContext) -> None:
"""Verify context can retrieve multiple different agents."""
agent1 = agent_context.get_agent("agent1")
agent2 = agent_context.get_agent("agent2")
assert agent1.name == "agent1"
assert agent2.name == "agent2"
class TestDurableAIAgentOrchestrationContextIntegration:
"""Test integration scenarios between orchestration context and agent shim."""
def test_orchestration_agent_has_working_run_method(
self, agent_context: DurableAIAgentOrchestrationContext
) -> None:
"""Verify agent from context has callable run method (even if not yet implemented)."""
agent = agent_context.get_agent("assistant")
assert hasattr(agent, "run")
assert callable(agent.run)
def test_orchestration_agent_can_create_threads(self, agent_context: DurableAIAgentOrchestrationContext) -> None:
"""Verify agent from context can create DurableAgentThread instances."""
agent = agent_context.get_agent("assistant")
thread = agent.get_new_thread()
assert isinstance(thread, DurableAgentThread)
def test_orchestration_agent_thread_with_parameters(
self, agent_context: DurableAIAgentOrchestrationContext
) -> None:
"""Verify agent can create threads with custom parameters."""
agent = agent_context.get_agent("assistant")
thread = agent.get_new_thread(service_thread_id="orch-session-456")
assert isinstance(thread, DurableAgentThread)
assert thread.service_thread_id == "orch-session-456"
if __name__ == "__main__":
pytest.main([__file__, "-v", "--tb=short"])
@@ -0,0 +1,213 @@
# Copyright (c) Microsoft. All rights reserved.
"""Unit tests for DurableAIAgent shim and DurableAgentProvider.
Focuses on critical message normalization, delegation, and protocol compliance.
Run with: pytest tests/test_shim.py -v
"""
from typing import Any
from unittest.mock import Mock
import pytest
from agent_framework import AgentProtocol, ChatMessage
from pydantic import BaseModel
from agent_framework_durabletask import DurableAgentThread
from agent_framework_durabletask._executors import DurableAgentExecutor
from agent_framework_durabletask._models import RunRequest
from agent_framework_durabletask._shim import DurableAgentProvider, DurableAIAgent
class ResponseFormatModel(BaseModel):
"""Test Pydantic model for response format testing."""
result: str
@pytest.fixture
def mock_executor() -> Mock:
"""Create a mock executor for testing."""
mock = Mock(spec=DurableAgentExecutor)
mock.run_durable_agent = Mock(return_value=None)
mock.get_new_thread = Mock(return_value=DurableAgentThread())
# Mock get_run_request to create actual RunRequest objects
def create_run_request(
message: str,
options: dict[str, Any] | None = None,
) -> RunRequest:
import uuid
opts = dict(options) if options else {}
response_format = opts.pop("response_format", None)
enable_tool_calls = opts.pop("enable_tool_calls", True)
wait_for_response = opts.pop("wait_for_response", True)
return RunRequest(
message=message,
correlation_id=str(uuid.uuid4()),
response_format=response_format,
enable_tool_calls=enable_tool_calls,
wait_for_response=wait_for_response,
options=opts,
)
mock.get_run_request = Mock(side_effect=create_run_request)
return mock
@pytest.fixture
def test_agent(mock_executor: Mock) -> DurableAIAgent[Any]:
"""Create a test agent with mock executor."""
return DurableAIAgent(mock_executor, "test_agent")
class TestDurableAIAgentMessageNormalization:
"""Test that DurableAIAgent properly normalizes various message input types."""
def test_run_accepts_string_message(self, test_agent: DurableAIAgent[Any], mock_executor: Mock) -> None:
"""Verify run accepts and normalizes string messages."""
test_agent.run("Hello, world!")
mock_executor.run_durable_agent.assert_called_once()
# Verify agent_name and run_request were passed correctly as kwargs
_, kwargs = mock_executor.run_durable_agent.call_args
assert kwargs["agent_name"] == "test_agent"
assert kwargs["run_request"].message == "Hello, world!"
def test_run_accepts_chat_message(self, test_agent: DurableAIAgent[Any], mock_executor: Mock) -> None:
"""Verify run accepts and normalizes ChatMessage objects."""
chat_msg = ChatMessage(role="user", text="Test message")
test_agent.run(chat_msg)
mock_executor.run_durable_agent.assert_called_once()
_, kwargs = mock_executor.run_durable_agent.call_args
assert kwargs["run_request"].message == "Test message"
def test_run_accepts_list_of_strings(self, test_agent: DurableAIAgent[Any], mock_executor: Mock) -> None:
"""Verify run accepts and joins list of strings."""
test_agent.run(["First message", "Second message"])
mock_executor.run_durable_agent.assert_called_once()
_, kwargs = mock_executor.run_durable_agent.call_args
assert kwargs["run_request"].message == "First message\nSecond message"
def test_run_accepts_list_of_chat_messages(self, test_agent: DurableAIAgent[Any], mock_executor: Mock) -> None:
"""Verify run accepts and joins list of ChatMessage objects."""
messages = [
ChatMessage(role="user", text="Message 1"),
ChatMessage(role="assistant", text="Message 2"),
]
test_agent.run(messages)
mock_executor.run_durable_agent.assert_called_once()
_, kwargs = mock_executor.run_durable_agent.call_args
assert kwargs["run_request"].message == "Message 1\nMessage 2"
def test_run_handles_none_message(self, test_agent: DurableAIAgent[Any], mock_executor: Mock) -> None:
"""Verify run handles None message gracefully."""
test_agent.run(None)
mock_executor.run_durable_agent.assert_called_once()
_, kwargs = mock_executor.run_durable_agent.call_args
assert kwargs["run_request"].message == ""
def test_run_handles_empty_list(self, test_agent: DurableAIAgent[Any], mock_executor: Mock) -> None:
"""Verify run handles empty list gracefully."""
test_agent.run([])
mock_executor.run_durable_agent.assert_called_once()
_, kwargs = mock_executor.run_durable_agent.call_args
assert kwargs["run_request"].message == ""
class TestDurableAIAgentParameterFlow:
"""Test that parameters flow correctly through the shim to executor."""
def test_run_forwards_thread_parameter(self, test_agent: DurableAIAgent[Any], mock_executor: Mock) -> None:
"""Verify run forwards thread parameter to executor."""
thread = DurableAgentThread(service_thread_id="test-thread")
test_agent.run("message", thread=thread)
mock_executor.run_durable_agent.assert_called_once()
_, kwargs = mock_executor.run_durable_agent.call_args
assert kwargs["thread"] == thread
def test_run_forwards_response_format(self, test_agent: DurableAIAgent[Any], mock_executor: Mock) -> None:
"""Verify run forwards response_format parameter to executor."""
test_agent.run("message", options={"response_format": ResponseFormatModel})
mock_executor.run_durable_agent.assert_called_once()
_, kwargs = mock_executor.run_durable_agent.call_args
assert kwargs["run_request"].response_format == ResponseFormatModel
class TestDurableAIAgentProtocolCompliance:
"""Test that DurableAIAgent implements AgentProtocol correctly."""
def test_agent_implements_protocol(self, test_agent: DurableAIAgent[Any]) -> None:
"""Verify DurableAIAgent implements AgentProtocol."""
assert isinstance(test_agent, AgentProtocol)
def test_agent_has_required_properties(self, test_agent: DurableAIAgent[Any]) -> None:
"""Verify DurableAIAgent has all required AgentProtocol properties."""
assert hasattr(test_agent, "id")
assert hasattr(test_agent, "name")
assert hasattr(test_agent, "display_name")
assert hasattr(test_agent, "description")
def test_agent_id_defaults_to_name(self, mock_executor: Mock) -> None:
"""Verify agent id defaults to name when not provided."""
agent: DurableAIAgent[Any] = DurableAIAgent(mock_executor, "my_agent")
assert agent.id == "my_agent"
assert agent.name == "my_agent"
def test_agent_id_can_be_customized(self, mock_executor: Mock) -> None:
"""Verify agent id can be set independently from name."""
agent: DurableAIAgent[Any] = DurableAIAgent(mock_executor, "my_agent", agent_id="custom-id")
assert agent.id == "custom-id"
assert agent.name == "my_agent"
class TestDurableAIAgentThreadManagement:
"""Test thread creation and management."""
def test_get_new_thread_delegates_to_executor(self, test_agent: DurableAIAgent[Any], mock_executor: Mock) -> None:
"""Verify get_new_thread delegates to executor."""
mock_thread = DurableAgentThread()
mock_executor.get_new_thread.return_value = mock_thread
thread = test_agent.get_new_thread()
mock_executor.get_new_thread.assert_called_once_with("test_agent")
assert thread == mock_thread
def test_get_new_thread_forwards_kwargs(self, test_agent: DurableAIAgent[Any], mock_executor: Mock) -> None:
"""Verify get_new_thread forwards kwargs to executor."""
mock_thread = DurableAgentThread(service_thread_id="thread-123")
mock_executor.get_new_thread.return_value = mock_thread
test_agent.get_new_thread(service_thread_id="thread-123")
mock_executor.get_new_thread.assert_called_once()
_, kwargs = mock_executor.get_new_thread.call_args
assert kwargs["service_thread_id"] == "thread-123"
class TestDurableAgentProviderInterface:
"""Test that DurableAgentProvider defines the correct interface."""
def test_provider_cannot_be_instantiated(self) -> None:
"""Verify DurableAgentProvider is abstract and cannot be instantiated."""
with pytest.raises(TypeError):
DurableAgentProvider() # type: ignore[abstract]
def test_provider_defines_get_agent_method(self) -> None:
"""Verify DurableAgentProvider defines get_agent abstract method."""
assert hasattr(DurableAgentProvider, "get_agent")
if __name__ == "__main__":
pytest.main([__file__, "-v", "--tb=short"])
@@ -0,0 +1,168 @@
# Copyright (c) Microsoft. All rights reserved.
"""Unit tests for DurableAIAgentWorker.
Focuses on critical worker flows: agent registration, validation, callbacks, and lifecycle.
"""
from unittest.mock import Mock
import pytest
from agent_framework_durabletask import DurableAIAgentWorker
@pytest.fixture
def mock_grpc_worker() -> Mock:
"""Create a mock TaskHubGrpcWorker for testing."""
mock = Mock()
mock.add_entity = Mock(return_value="dafx-test_agent")
mock.start = Mock()
mock.stop = Mock()
return mock
@pytest.fixture
def mock_agent() -> Mock:
"""Create a mock agent for testing."""
agent = Mock()
agent.name = "test_agent"
return agent
@pytest.fixture
def agent_worker(mock_grpc_worker: Mock) -> DurableAIAgentWorker:
"""Create a DurableAIAgentWorker with mock worker."""
return DurableAIAgentWorker(mock_grpc_worker)
class TestDurableAIAgentWorkerRegistration:
"""Test agent registration behavior."""
def test_add_agent_accepts_agent_with_name(
self, agent_worker: DurableAIAgentWorker, mock_agent: Mock, mock_grpc_worker: Mock
) -> None:
"""Verify that agents with names can be registered."""
agent_worker.add_agent(mock_agent)
# Verify entity was registered with underlying worker
mock_grpc_worker.add_entity.assert_called_once()
# Verify agent name is tracked
assert "test_agent" in agent_worker.registered_agent_names
def test_add_agent_rejects_agent_without_name(self, agent_worker: DurableAIAgentWorker) -> None:
"""Verify that agents without names are rejected."""
agent_no_name = Mock()
agent_no_name.name = None
with pytest.raises(ValueError, match="Agent must have a name"):
agent_worker.add_agent(agent_no_name)
def test_add_agent_rejects_empty_name(self, agent_worker: DurableAIAgentWorker) -> None:
"""Verify that agents with empty names are rejected."""
agent_empty_name = Mock()
agent_empty_name.name = ""
with pytest.raises(ValueError, match="Agent must have a name"):
agent_worker.add_agent(agent_empty_name)
def test_add_agent_rejects_duplicate_names(self, agent_worker: DurableAIAgentWorker, mock_agent: Mock) -> None:
"""Verify duplicate agent names are not allowed."""
agent_worker.add_agent(mock_agent)
# Try to register another agent with the same name
duplicate_agent = Mock()
duplicate_agent.name = "test_agent"
with pytest.raises(ValueError, match="already registered"):
agent_worker.add_agent(duplicate_agent)
def test_registered_agent_names_tracks_multiple_agents(self, agent_worker: DurableAIAgentWorker) -> None:
"""Verify registered_agent_names tracks all registered agents."""
agent1 = Mock()
agent1.name = "agent1"
agent2 = Mock()
agent2.name = "agent2"
agent3 = Mock()
agent3.name = "agent3"
agent_worker.add_agent(agent1)
agent_worker.add_agent(agent2)
agent_worker.add_agent(agent3)
registered = agent_worker.registered_agent_names
assert "agent1" in registered
assert "agent2" in registered
assert "agent3" in registered
assert len(registered) == 3
class TestDurableAIAgentWorkerCallbacks:
"""Test callback configuration behavior."""
def test_worker_level_callback_accepted(self, mock_grpc_worker: Mock) -> None:
"""Verify worker-level callback can be set."""
mock_callback = Mock()
agent_worker = DurableAIAgentWorker(mock_grpc_worker, callback=mock_callback)
assert agent_worker is not None
def test_agent_level_callback_accepted(self, agent_worker: DurableAIAgentWorker, mock_agent: Mock) -> None:
"""Verify agent-level callback can be set during registration."""
mock_callback = Mock()
# Should not raise exception
agent_worker.add_agent(mock_agent, callback=mock_callback)
assert "test_agent" in agent_worker.registered_agent_names
def test_none_callback_accepted(self, mock_grpc_worker: Mock, mock_agent: Mock) -> None:
"""Verify None callback is valid (no callbacks required)."""
agent_worker = DurableAIAgentWorker(mock_grpc_worker, callback=None)
agent_worker.add_agent(mock_agent, callback=None)
assert "test_agent" in agent_worker.registered_agent_names
class TestDurableAIAgentWorkerLifecycle:
"""Test worker lifecycle behavior."""
def test_start_delegates_to_underlying_worker(
self, agent_worker: DurableAIAgentWorker, mock_grpc_worker: Mock
) -> None:
"""Verify start() delegates to wrapped worker."""
agent_worker.start()
mock_grpc_worker.start.assert_called_once()
def test_stop_delegates_to_underlying_worker(
self, agent_worker: DurableAIAgentWorker, mock_grpc_worker: Mock
) -> None:
"""Verify stop() delegates to wrapped worker."""
agent_worker.stop()
mock_grpc_worker.stop.assert_called_once()
def test_start_works_with_no_agents(self, agent_worker: DurableAIAgentWorker, mock_grpc_worker: Mock) -> None:
"""Verify worker can start even with no agents registered."""
agent_worker.start()
mock_grpc_worker.start.assert_called_once()
def test_start_works_with_multiple_agents(self, agent_worker: DurableAIAgentWorker, mock_grpc_worker: Mock) -> None:
"""Verify worker can start with multiple agents registered."""
agent1 = Mock()
agent1.name = "agent1"
agent2 = Mock()
agent2.name = "agent2"
agent_worker.add_agent(agent1)
agent_worker.add_agent(agent2)
agent_worker.start()
mock_grpc_worker.start.assert_called_once()
assert len(agent_worker.registered_agent_names) == 2
if __name__ == "__main__":
pytest.main([__file__, "-v", "--tb=short"])