Compare commits

...
Author SHA1 Message Date
Gavin Aguiar d9f0329edd TTL 2026-01-21 11:02:19 -06:00
Laveesh RohraandGitHub 3df916064c 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
2026-01-14 10:56:11 -08:00
+35 1e36ba33c4 .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>
2026-01-09 15:58:49 -08:00
Laveesh RohraandGitHub e3eff65a6b 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
2026-01-07 13:53:21 -08:00
+29 a5b36dc379 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>
2026-01-02 13:13:37 -08:00
Laveesh RohraandGitHub a02527f00a 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
2025-12-22 12:54:24 -08:00
Laveesh RohraandGitHub 87a38bc7da Python: Rebase durable task feature branch with main (#2806) 2025-12-17 14:02:36 -08:00
Laveesh RohraandGitHub a48a8dd524 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
2025-12-17 12:46:08 -08:00
525 changed files with 48774 additions and 12752 deletions
+8
View File
@@ -0,0 +1,8 @@
blank_issues_enabled: true
contact_links:
- name: Documentation
url: https://aka.ms/agent-framework
about: Check out the official documentation for guides and API reference.
- name: Discussions
url: https://github.com/microsoft/agent-framework/discussions
about: Ask questions about Agent Framework.
+70
View File
@@ -0,0 +1,70 @@
name: .NET Bug Report
description: Report a bug in the Agent Framework .NET SDK
title: ".NET: [Bug]: "
labels: ["bug", ".NET"]
type: bug
body:
- type: textarea
id: description
attributes:
label: Description
description: Please provide a clear and detailed description of the bug.
placeholder: |
- What happened?
- What did you expect to happen?
- Steps to reproduce the issue
validations:
required: true
- type: textarea
id: code-sample
attributes:
label: Code Sample
description: If applicable, provide a minimal code sample that demonstrates the issue.
placeholder: |
```csharp
// Your code here
```
render: markdown
validations:
required: false
- type: textarea
id: error-messages
attributes:
label: Error Messages / Stack Traces
description: Include any error messages or stack traces you received.
placeholder: |
```
Paste error messages or stack traces here
```
render: markdown
validations:
required: false
- type: input
id: dotnet-packages
attributes:
label: Package Versions
description: List the Microsoft.Agents.* packages and versions you are using
placeholder: "e.g., Microsoft.Agents.AI.Abstractions: 1.0.0, Microsoft.Agents.AI.OpenAI: 1.0.0"
validations:
required: true
- type: input
id: dotnet-version
attributes:
label: .NET Version
description: What version of .NET are you using?
placeholder: "e.g., .NET 8.0"
validations:
required: false
- type: textarea
id: additional-context
attributes:
label: Additional Context
description: Add any other context or screenshots that might be helpful.
placeholder: "Any additional information..."
validations:
required: false
@@ -0,0 +1,51 @@
name: Feature Request
description: Request a new feature for Microsoft Agent Framework
title: "[Feature]: "
type: feature
body:
- type: textarea
id: description
attributes:
label: Description
description: Please describe the feature you'd like and why it would be useful.
placeholder: |
Describe the feature you're requesting:
- What problem does it solve?
- What would the expected behavior be?
- Are there any alternatives you've considered?
validations:
required: true
- type: textarea
id: code-sample
attributes:
label: Code Sample
description: If applicable, provide a code sample showing how you'd like to use this feature.
placeholder: |
```python
# Your code here
```
or
```csharp
// Your code here
```
render: markdown
validations:
required: false
- type: dropdown
id: language
attributes:
label: Language/SDK
description: Which language/SDK does this feature apply to?
options:
- Both
- .NET
- Python
- Other / Not Applicable
default: 0
validations:
required: false
+70
View File
@@ -0,0 +1,70 @@
name: Python Bug Report
description: Report a bug in the Agent Framework Python SDK
title: "Python: [Bug]: "
labels: ["bug", "Python"]
type: bug
body:
- type: textarea
id: description
attributes:
label: Description
description: Please provide a clear and detailed description of the bug.
placeholder: |
- What happened?
- What did you expect to happen?
- Steps to reproduce the issue
validations:
required: true
- type: textarea
id: code-sample
attributes:
label: Code Sample
description: If applicable, provide a minimal code sample that demonstrates the issue.
placeholder: |
```python
# Your code here
```
render: markdown
validations:
required: false
- type: textarea
id: error-messages
attributes:
label: Error Messages / Stack Traces
description: Include any error messages or stack traces you received.
placeholder: |
```
Paste error messages or stack traces here
```
render: markdown
validations:
required: false
- type: input
id: python-packages
attributes:
label: Package Versions
description: List the agent-framework-* packages and versions you are using
placeholder: "e.g., agent-framework-core: 1.0.0, agent-framework-azure-ai: 1.0.0"
validations:
required: true
- type: input
id: python-version
attributes:
label: Python Version
description: What version of Python are you using?
placeholder: "e.g., Python 3.11"
validations:
required: false
- type: textarea
id: additional-context
attributes:
label: Additional Context
description: Add any other context or screenshots that might be helpful.
placeholder: "Any additional information..."
validations:
required: false
@@ -28,6 +28,18 @@ runs:
echo "Waiting for Azurite (Azure Storage emulator) to be ready"
timeout 30 bash -c 'until curl --silent http://localhost:10000/devstoreaccount1; do sleep 1; done'
echo "Azurite (Azure Storage emulator) is ready"
- name: Start Redis
shell: bash
run: |
if [ "$(docker ps -aq -f name=redis)" ]; then
echo "Stopping and removing existing Redis"
docker rm -f redis
fi
echo "Starting Redis"
docker run -d --name redis -p 6379:6379 redis:latest
echo "Waiting for Redis to be ready"
timeout 30 bash -c 'until docker exec redis redis-cli ping | grep -q PONG; do sleep 1; done'
echo "Redis is ready"
- name: Install Azure Functions Core Tools
shell: bash
run: |
@@ -839,7 +839,7 @@ var agentOptions = new ChatClientAgentRunOptions(new ChatOptions
{
MaxOutputTokens = 8000,
// Breaking glass to access provider-specific options
RawRepresentationFactory = (_) => new OpenAI.Responses.ResponseCreationOptions()
RawRepresentationFactory = (_) => new OpenAI.Responses.CreateResponseOptions()
{
ReasoningOptions = new()
{
+1 -1
View File
@@ -32,7 +32,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
persist-credentials: false
+21 -15
View File
@@ -35,19 +35,25 @@ jobs:
contents: read
pull-requests: read
outputs:
dotnetChanges: ${{ steps.filter.outputs.dotnet}}
dotnetChanges: ${{ steps.filter.outputs.dotnet }}
cosmosDbChanges: ${{ steps.filter.outputs.cosmosdb }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
dotnet:
- 'dotnet/**'
cosmosdb:
- 'dotnet/src/Microsoft.Agents.AI.CosmosNoSql/**'
# run only if 'dotnet' files were changed
- name: dotnet tests
if: steps.filter.outputs.dotnet == 'true'
run: echo "Dotnet file"
- name: dotnet CosmosDB tests
if: steps.filter.outputs.cosmosdb == 'true'
run: echo "Dotnet CosmosDB changes"
# run only if not 'dotnet' files were changed
- name: not dotnet tests
if: steps.filter.outputs.dotnet != 'true'
@@ -68,7 +74,7 @@ jobs:
runs-on: ${{ matrix.os }}
environment: ${{ matrix.environment }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
persist-credentials: false
sparse-checkout: |
@@ -77,6 +83,16 @@ jobs:
dotnet
python
workflow-samples
# Start Cosmos DB Emulator for all integration tests and only for unit tests when CosmosDB changes happened)
- name: Start Azure Cosmos DB Emulator
if: ${{ runner.os == 'Windows' && (needs.paths-filter.outputs.cosmosDbChanges == 'true' || (github.event_name != 'pull_request' && matrix.integration-tests)) }}
shell: pwsh
run: |
Write-Host "Launching Azure Cosmos DB Emulator"
Import-Module "$env:ProgramFiles\Azure Cosmos DB Emulator\PSModules\Microsoft.Azure.CosmosDB.Emulator"
Start-CosmosDbEmulator -NoUI -Key "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="
echo "COSMOS_EMULATOR_AVAILABLE=true" >> $env:GITHUB_ENV
- name: Setup dotnet
uses: actions/setup-dotnet@v5.0.1
@@ -123,17 +139,7 @@ jobs:
popd
popd
rm -rf "$TEMP_DIR"
# Start Cosmos DB Emulator for Cosmos-based unit tests (only on Windows)
- name: Start Azure Cosmos DB Emulator
if: runner.os == 'Windows'
shell: pwsh
run: |
Write-Host "Launching Azure Cosmos DB Emulator"
Import-Module "$env:ProgramFiles\Azure Cosmos DB Emulator\PSModules\Microsoft.Azure.CosmosDB.Emulator"
Start-CosmosDbEmulator -NoUI -Key "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="
echo "COSMOS_EMULATOR_AVAILABLE=true" >> $env:GITHUB_ENV
- name: Run Unit Tests
shell: bash
run: |
@@ -225,7 +231,7 @@ jobs:
- name: Upload coverage report artifact
if: matrix.targetFramework == env.COVERAGE_FRAMEWORK
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: CoverageReport-${{ matrix.os }}-${{ matrix.targetFramework }}-${{ matrix.configuration }} # Artifact name
path: ./TestResults/Reports # Directory containing files to upload
+1 -1
View File
@@ -30,7 +30,7 @@ jobs:
steps:
- name: Check out code
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
fetch-depth: 0
persist-credentials: false
+50 -11
View File
@@ -45,19 +45,58 @@ jobs:
labels.push("triage")
}
// Check if the body or the title contains the word 'python' (case-insensitive)
if ((body != null && body.match(/python/i)) || (title != null && title.match(/python/i))) {
// Add the 'python' label to the array
labels.push("python")
// Helper function to extract field value from issue form body
// Issue forms format fields as: ### Field Name\n\nValue
function getFormFieldValue(body, fieldName) {
if (!body) return null
const regex = new RegExp(`###\\s*${fieldName}\\s*\\n\\n([^\\n#]+)`, 'i')
const match = body.match(regex)
return match ? match[1].trim() : null
}
// Check if the body or the title contains the words 'dotnet', '.net', 'c#' or 'csharp' (case-insensitive)
if ((body != null && body.match(/.net/i)) || (title != null && title.match(/.net/i)) ||
(body != null && body.match(/dotnet/i)) || (title != null && title.match(/dotnet/i)) ||
(body != null && body.match(/C#/i)) || (title != null && title.match(/C#/i)) ||
(body != null && body.match(/csharp/i)) || (title != null && title.match(/csharp/i))) {
// Add the '.NET' label to the array
labels.push(".NET")
// Check for language from issue form dropdown first
const languageField = getFormFieldValue(body, 'Language')
let languageLabelAdded = false
if (languageField) {
if (languageField === 'Python') {
labels.push("python")
languageLabelAdded = true
} else if (languageField === '.NET') {
labels.push(".NET")
languageLabelAdded = true
}
// 'None / Not Applicable' - don't add any language label
}
// Fallback: Check if the body or the title contains the word 'python' (case-insensitive)
// Only if language wasn't already determined from the form field
if (!languageLabelAdded) {
if ((body != null && body.match(/python/i)) || (title != null && title.match(/python/i))) {
// Add the 'python' label to the array
labels.push("python")
}
// Check if the body or the title contains the words 'dotnet', '.net', 'c#' or 'csharp' (case-insensitive)
if ((body != null && body.match(/\.net/i)) || (title != null && title.match(/\.net/i)) ||
(body != null && body.match(/dotnet/i)) || (title != null && title.match(/dotnet/i)) ||
(body != null && body.match(/C#/i)) || (title != null && title.match(/C#/i)) ||
(body != null && body.match(/csharp/i)) || (title != null && title.match(/csharp/i))) {
// Add the '.NET' label to the array
labels.push(".NET")
}
}
// Check for issue type from issue form dropdown
const issueTypeField = getFormFieldValue(body, 'Type of Issue')
if (issueTypeField) {
if (issueTypeField === 'Bug') {
labels.push("bug")
} else if (issueTypeField === 'Feature Request') {
labels.push("enhancement")
} else if (issueTypeField === 'Question') {
labels.push("question")
}
}
// Add the labels to the issue (only if there are labels to add)
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-22.04
# check out the latest version of the code
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
persist-credentials: false
+2 -2
View File
@@ -27,7 +27,7 @@ jobs:
env:
UV_PYTHON: ${{ matrix.python-version }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up python and install the project
@@ -39,7 +39,7 @@ jobs:
env:
# Configure a constant location for the uv cache
UV_CACHE_DIR: /tmp/.uv-cache
- uses: actions/cache@v4
- uses: actions/cache@v5
with:
path: ~/.cache/pre-commit
key: pre-commit|${{ matrix.python-version }}|${{ hashFiles('python/.pre-commit-config.yaml') }}
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Set up uv
uses: astral-sh/setup-uv@v7
with:
+2 -2
View File
@@ -24,7 +24,7 @@ jobs:
outputs:
pythonChanges: ${{ steps.filter.outputs.python}}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: dorny/paths-filter@v3
id: filter
with:
@@ -59,7 +59,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Set up python and install the project
id: python-setup
+4 -4
View File
@@ -28,7 +28,7 @@ jobs:
outputs:
pythonChanges: ${{ steps.filter.outputs.python}}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: dorny/paths-filter@v3
id: filter
with:
@@ -75,7 +75,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Set up python and install the project
id: python-setup
uses: ./.github/actions/python-setup
@@ -135,7 +135,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Set up python and install the project
id: python-setup
uses: ./.github/actions/python-setup
@@ -154,7 +154,7 @@ jobs:
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Test with pytest
timeout-minutes: 10
run: uv run poe azure-ai-tests -n logical --dist loadfile --dist worksteal --timeout 300 --retries 3 --retry-delay 10
run: uv run --directory packages/azure-ai poe integration-tests -n logical --dist loadfile --dist worksteal --timeout 300 --retries 3 --retry-delay 10
working-directory: ./python
- name: Test Azure AI samples
timeout-minutes: 10
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Set up python and install the project
id: python-setup
uses: ./.github/actions/python-setup
@@ -19,9 +19,9 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Download coverage report
uses: actions/download-artifact@v6
uses: actions/download-artifact@v7
with:
github-token: ${{ secrets.GH_ACTIONS_PR_WRITE }}
run-id: ${{ github.event.workflow_run.id }}
+2 -2
View File
@@ -20,7 +20,7 @@ jobs:
env:
UV_PYTHON: "3.10"
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
# Save the PR number to a file since the workflow_run event
# in the coverage report workflow does not have access to it
- name: Save PR number
@@ -38,7 +38,7 @@ jobs:
- name: Run all tests with coverage report
run: uv run poe all-tests-cov --cov-report=xml:python-coverage.xml -q --junitxml=pytest.xml
- name: Upload coverage report
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
path: |
python/python-coverage.xml
+1 -1
View File
@@ -27,7 +27,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Set up python and install the project
id: python-setup
uses: ./.github/actions/python-setup
+6 -5
View File
@@ -208,13 +208,14 @@ WARP.md
**/projectBrief.md
# Azurite storage emulator files
*/__azurite_db_blob__.json
*/__azurite_db_blob_extent__.json
*/__azurite_db_queue__.json
*/__azurite_db_queue_extent__.json
*/__azurite_db_table__.json
*/__azurite_db_blob__.json*
*/__azurite_db_blob_extent__.json*
*/__azurite_db_queue__.json*
*/__azurite_db_queue_extent__.json*
*/__azurite_db_table__.json*
*/__blobstorage__/
*/__queuestorage__/
*/AzuriteConfig
# Azure Functions local settings
local.settings.json
+1 -1
View File
@@ -1,3 +1,3 @@
# Declarative Agents
This folder contains sample agent definitions than be ran using the declarative agent support, for python see the [declarative agent python sample folder](../python/samples/getting_started/declarative/).
This folder contains sample agent definitions that can be run using the declarative agent support, for python see the [declarative agent python sample folder](../python/samples/getting_started/declarative/).
+5 -5
View File
@@ -10,19 +10,19 @@ model:
temperature: 0.9
topP: 0.95
connection:
kind: ApiKey
key: =Env.OPENAI_API_KEY
kind: key
apiKey: =Env.OPENAI_APIKEY
outputSchema:
properties:
language:
type: string
kind: string
required: true
description: The language of the answer.
answer:
type: string
kind: string
required: true
description: The answer text.
type:
type: string
kind: string
required: true
description: The type of the response.
+4 -4
View File
@@ -64,7 +64,7 @@ Approaches observed from the compared SDKs:
| AutoGen | **Approach 1** Separates messages into Agent-Agent (maps to Primary) and Internal (maps to Secondary) and these are returned as separate properties on the agent response object. See [types of messages](https://microsoft.github.io/autogen/stable/user-guide/agentchat-user-guide/tutorial/messages.html#types-of-messages) and [Response](https://microsoft.github.io/autogen/stable/reference/python/autogen_agentchat.base.html#autogen_agentchat.base.Response) | **Approach 2** Returns a stream of internal events and the last item is a Response object. See [ChatAgent.on_messages_stream](https://microsoft.github.io/autogen/stable/reference/python/autogen_agentchat.base.html#autogen_agentchat.base.ChatAgent.on_messages_stream) |
| OpenAI Agent SDK | **Approach 1** Separates new_items (Primary+Secondary) from final output (Primary) as separate properties on the [RunResult](https://github.com/openai/openai-agents-python/blob/main/src/agents/result.py#L39) | **Approach 1** Similar to non-streaming, has a way of streaming updates via a method on the response object which includes all data, and then a separate final output property on the response object which is populated only when the run is complete. See [RunResultStreaming](https://github.com/openai/openai-agents-python/blob/main/src/agents/result.py#L136) |
| Google ADK | **Approach 2** [Emits events](https://google.github.io/adk-docs/runtime/#step-by-step-breakdown) with [FinalResponse](https://github.com/google/adk-java/blob/main/core/src/main/java/com/google/adk/events/Event.java#L232) true (Primary) / false (Secondary) and callers have to filter out those with false to get just the final response message | **Approach 2** Similar to non-streaming except [events](https://google.github.io/adk-docs/runtime/#streaming-vs-non-streaming-output-partialtrue) are emitted with [Partial](https://github.com/google/adk-java/blob/main/core/src/main/java/com/google/adk/events/Event.java#L133) true to indicate that they are streaming messages. A final non partial event is also emitted. |
| AWS (Strands) | **Approach 3** Returns an [AgentResult](https://strandsagents.com/latest/api-reference/agent/#strands.agent.agent_result.AgentResult) (Primary) with messages and a reason for the run's completion. | **Approach 2** [Streams events](https://strandsagents.com/latest/api-reference/agent/#strands.agent.agent.Agent.stream_async) (Primary+Secondary) including, response text, current_tool_use, even data from "callbacks" (strands plugins) |
| AWS (Strands) | **Approach 3** Returns an [AgentResult](https://strandsagents.com/latest/documentation/docs/api-reference/python/agent/agent_result/) (Primary) with messages and a reason for the run's completion. | **Approach 2** [Streams events](https://strandsagents.com/latest/documentation/docs/api-reference/python/agent/agent/#strands.agent.agent.Agent.stream_async) (Primary+Secondary) including, response text, current_tool_use, even data from "callbacks" (strands plugins) |
| LangGraph | **Approach 2** A mixed list of all [messages](https://langchain-ai.github.io/langgraph/agents/run_agents/#output-format) | **Approach 2** A mixed list of all [messages](https://langchain-ai.github.io/langgraph/agents/run_agents/#output-format) |
| Agno | **Combination of various approaches** Returns a [RunResponse](https://docs.agno.com/reference/agents/run-response) object with text content, messages (essentially chat history including inputs and instructions), reasoning and thinking text properties. Secondary events could potentially be extracted from messages. | **Approach 2** Returns [RunResponseEvent](https://docs.agno.com/reference/agents/run-response#runresponseevent-types-and-attributes) objects including tool call, memory update, etc, information, where the [RunResponseCompletedEvent](https://docs.agno.com/reference/agents/run-response#runresponsecompletedevent) has similar properties to RunResponse|
| A2A | **Approach 3** Returns a [Task or Message](https://a2aproject.github.io/A2A/latest/specification/#71-messagesend) where the message is the final result (Primary) and task is a reference to a long running process. | **Approach 2** Returns a [stream](https://a2aproject.github.io/A2A/latest/specification/#72-messagestream) that contains task updates (Secondary) and a final message (Primary) |
@@ -495,8 +495,8 @@ We need to decide what AIContent types, each agent response type will be mapped
| SDK | Structured Outputs support |
|-|-|
| AutoGen | **Approach 1** Supports [configuring an agent](https://microsoft.github.io/autogen/stable/user-guide/agentchat-user-guide/tutorial/agents.html#structured-output) at agent creation. |
| Google ADK | **Approach 1** Both [input and output shemas can be specified for LLM Agents](https://google.github.io/adk-docs/agents/llm-agents/#structuring-data-input_schema-output_schema-output_key) at construction time. This option is specific to this agent type and other agent types do not necessarily support |
| AWS (Strands) | **Approach 2** Supports a special invocation method called [structured_output](https://strandsagents.com/latest/api-reference/agent/#strands.agent.agent.Agent.structured_output) |
| Google ADK | **Approach 1** Both [input and output schemas can be specified for LLM Agents](https://google.github.io/adk-docs/agents/llm-agents/#structuring-data-input_schema-output_schema-output_key) at construction time. This option is specific to this agent type and other agent types do not necessarily support |
| AWS (Strands) | **Approach 2** Supports a special invocation method called [structured_output](https://strandsagents.com/latest/documentation/docs/api-reference/python/agent/agent/#strands.agent.agent.Agent.structured_output) |
| LangGraph | **Approach 1** Supports [configuring an agent](https://langchain-ai.github.io/langgraph/agents/agents/?h=structured#6-configure-structured-output) at agent construction time, and a [structured response](https://langchain-ai.github.io/langgraph/agents/run_agents/#output-format) can be retrieved as a special property on the agent response |
| Agno | **Approach 1** Supports [configuring an agent](https://docs.agno.com/examples/getting-started/structured-output) at agent construction time |
| A2A | **Informal Approach 2** Doesn't formally support schema negotiation, but [hints can be provided via metadata](https://a2a-protocol.org/latest/specification/#97-structured-data-exchange-requesting-and-providing-json) at invocation time |
@@ -508,7 +508,7 @@ We need to decide what AIContent types, each agent response type will be mapped
|-|-|
| AutoGen | Supports a [stop reason](https://microsoft.github.io/autogen/stable/reference/python/autogen_agentchat.base.html#autogen_agentchat.base.TaskResult.stop_reason) which is a freeform text string |
| Google ADK | [No equivalent present](https://github.com/google/adk-python/blob/main/src/google/adk/events/event.py) |
| AWS (Strands) | Exposes a [stop_reason](https://strandsagents.com/latest/api-reference/types/#strands.types.event_loop.StopReason) property on the [AgentResult](https://strandsagents.com/latest/api-reference/agent/#strands.agent.agent_result.AgentResult) class with options that are tied closely to LLM operations. |
| AWS (Strands) | Exposes a [stop_reason](https://strandsagents.com/latest/documentation/docs/api-reference/python/types/event_loop/#strands.types.event_loop.StopReason) property on the [AgentResult](https://strandsagents.com/latest/documentation/docs/api-reference/python/agent/agent_result/) class with options that are tied closely to LLM operations. |
| LangGraph | No equivalent present, output contains only [messages](https://langchain-ai.github.io/langgraph/agents/run_agents/#output-format) |
| Agno | [No equivalent present](https://docs.agno.com/reference/agents/run-response) |
| A2A | No equivalent present, response only contains a [message](https://a2a-protocol.org/latest/specification/#64-message-object) or [task](https://a2a-protocol.org/latest/specification/#61-task-object). |
@@ -0,0 +1,147 @@
# Time-To-Live (TTL) for durable agent sessions
## Overview
The durable agents automatically maintain conversation history and state for each session. Without automatic cleanup, this state can accumulate indefinitely, consuming storage resources and increasing costs. The Time-To-Live (TTL) feature provides automatic cleanup of idle agent sessions, ensuring that sessions are automatically deleted after a period of inactivity.
## What is TTL?
Time-To-Live (TTL) is a configurable duration that determines how long an agent session state will be retained after its last interaction. When an agent session is idle (no messages sent to it) for longer than the TTL period, the session state is automatically deleted. Each new interaction with an agent resets the TTL timer, extending the session's lifetime.
## Benefits
- **Automatic cleanup**: No manual intervention required to clean up idle agent sessions
- **Cost optimization**: Reduces storage costs by automatically removing unused session state
- **Resource management**: Prevents unbounded growth of agent session state in storage
- **Configurable**: Set TTL globally or per-agent type to match your application's needs
## Configuration
TTL can be configured at two levels:
1. **Global default TTL**: Applies to all agent sessions unless overridden
2. **Per-agent type TTL**: Overrides the global default for specific agent types
Additionally, you can configure a **minimum deletion delay** that controls how frequently deletion operations are scheduled. The default value is 5 minutes, and the maximum allowed value is also 5 minutes.
> [!NOTE]
> Reducing the minimum deletion delay below 5 minutes can be useful for testing or for ensuring rapid cleanup of short-lived agent sessions. However, this can also increase the load on the system and should be used with caution.
### Default values
- **Default TTL**: 14 days
- **Minimum TTL deletion delay**: 5 minutes (maximum allowed value, subject to change in future releases)
### Configuration examples
#### .NET
```csharp
// Configure global default TTL and minimum signal delay
services.ConfigureDurableAgents(
options =>
{
// Set global default TTL to 7 days
options.DefaultTimeToLive = TimeSpan.FromDays(7);
// Add agents (will use global default TTL)
options.AddAIAgent(myAgent);
});
// Configure per-agent TTL
services.ConfigureDurableAgents(
options =>
{
options.DefaultTimeToLive = TimeSpan.FromDays(14); // Global default
// Agent with custom TTL of 1 day
options.AddAIAgent(shortLivedAgent, timeToLive: TimeSpan.FromDays(1));
// Agent with custom TTL of 90 days
options.AddAIAgent(longLivedAgent, timeToLive: TimeSpan.FromDays(90));
// Agent using global default (14 days)
options.AddAIAgent(defaultAgent);
});
// Disable TTL for specific agents by setting TTL to null
services.ConfigureDurableAgents(
options =>
{
options.DefaultTimeToLive = TimeSpan.FromDays(14);
// Agent with no TTL (never expires)
options.AddAIAgent(permanentAgent, timeToLive: null);
});
```
## How TTL works
The following sections describe how TTL works in detail.
### Expiration tracking
Each agent session maintains an expiration timestamp in its internally managed state that is updated whenever the session processes a message:
1. When a message is sent to an agent session, the expiration time is set to `current time + TTL`
2. The runtime schedules a delete operation for the expiration time (subject to minimum delay constraints)
3. When the delete operation runs, if the current time is past the expiration time, the session state is deleted. Otherwise, the delete operation is rescheduled for the next expiration time.
### State deletion
When an agent session expires, its entire state is deleted, including:
- Conversation history
- Any custom state data
- Expiration timestamps
After deletion, if a message is sent to the same agent session, a new session is created with a fresh conversation history.
## Behavior examples
The following examples illustrate how TTL works in different scenarios.
### Example 1: Agent session expires after TTL
1. Agent configured with 30-day TTL
2. User sends message at Day 0 → agent session created, expiration set to Day 30
3. No further messages sent
4. At Day 30 → Agent session is deleted
5. User sends message at Day 31 → New agent session created with fresh conversation history
### Example 2: TTL reset on interaction
1. Agent configured with 30-day TTL
2. User sends message at Day 0 → agent session created, expiration set to Day 30
3. User sends message at Day 15 → Expiration reset to Day 45
4. User sends message at Day 40 → Expiration reset to Day 70
5. Agent session remains active as long as there are regular interactions
## Logging
The TTL feature includes comprehensive logging to track state changes:
- **Expiration time updated**: Logged when TTL expiration time is set or updated
- **Deletion scheduled**: Logged when a deletion check signal is scheduled
- **Deletion check**: Logged when a deletion check operation runs
- **Session expired**: Logged when an agent session is deleted due to expiration
- **TTL rescheduled**: Logged when a deletion signal is rescheduled
These logs help monitor TTL behavior and troubleshoot any issues.
## Best practices
1. **Choose appropriate TTL values**: Balance between storage costs and user experience. Too short TTLs may delete active sessions, while too long TTLs may accumulate unnecessary state.
2. **Use per-agent TTLs**: Different agents may have different usage patterns. Configure TTLs per-agent based on expected session lifetimes.
3. **Monitor expiration logs**: Review logs to understand TTL behavior and adjust configuration as needed.
4. **Test with short TTLs**: During development, use short TTLs (e.g., minutes) to verify TTL behavior without waiting for long periods.
## Limitations
- TTL is based on wall-clock time, not activity time. The expiration timer starts from the last message timestamp.
- Deletion checks are durably scheduled operations and may have slight delays depending on system load.
- Once an agent session is deleted, its conversation history cannot be recovered.
- TTL deletion requires at least one worker to be available to process the deletion operation message.
+1
View File
@@ -209,6 +209,7 @@ dotnet_diagnostic.CA2000.severity = none # Call System.IDisposable.Dispose on ob
dotnet_diagnostic.CA2225.severity = none # Operator overloads have named alternates
dotnet_diagnostic.CA2227.severity = none # Change to be read-only by removing the property setter
dotnet_diagnostic.CA2249.severity = suggestion # Consider using 'Contains' method instead of 'IndexOf' method
dotnet_diagnostic.CA2252.severity = none # Requires preview
dotnet_diagnostic.CA2253.severity = none # Named placeholders in the logging message template should not be comprised of only numeric characters
dotnet_diagnostic.CA2253.severity = none # Named placeholders in the logging message template should not be comprised of only numeric characters
dotnet_diagnostic.CA2263.severity = suggestion # Use generic overload
+20 -20
View File
@@ -11,22 +11,22 @@
</PropertyGroup>
<ItemGroup>
<!-- Aspire.* -->
<PackageVersion Include="Anthropic" Version="11.0.0" />
<PackageVersion Include="Anthropic" Version="12.0.1" />
<PackageVersion Include="Anthropic.Foundry" Version="0.1.0" />
<PackageVersion Include="Aspire.Azure.AI.OpenAI" Version="13.0.0-preview.1.25560.3" />
<PackageVersion Include="Aspire.Hosting.AppHost" Version="$(AspireAppHostSdkVersion)" />
<PackageVersion Include="Aspire.Hosting.Azure.CognitiveServices" Version="$(AspireAppHostSdkVersion)" />
<PackageVersion Include="Aspire.Microsoft.Azure.Cosmos" Version="$(AspireAppHostSdkVersion)" />
<PackageVersion Include="CommunityToolkit.Aspire.OllamaSharp" Version="13.0.0-beta.440" />
<PackageVersion Include="CommunityToolkit.Aspire.OllamaSharp" Version="13.0.0" />
<!-- Azure.* -->
<PackageVersion Include="Azure.AI.Projects" Version="1.2.0-beta.3" />
<PackageVersion Include="Azure.AI.Projects.OpenAI" Version="1.0.0-beta.4" />
<PackageVersion Include="Azure.AI.Projects" Version="1.2.0-beta.5" />
<PackageVersion Include="Azure.AI.Projects.OpenAI" Version="1.0.0-beta.5" />
<PackageVersion Include="Azure.AI.Agents.Persistent" Version="1.2.0-beta.8" />
<PackageVersion Include="Azure.AI.OpenAI" Version="2.7.0-beta.2" />
<PackageVersion Include="Azure.Identity" Version="1.17.0" />
<PackageVersion Include="Azure.AI.OpenAI" Version="2.8.0-beta.1" />
<PackageVersion Include="Azure.Identity" Version="1.17.1" />
<PackageVersion Include="Azure.Monitor.OpenTelemetry.Exporter" Version="1.4.0" />
<!-- Google Gemini -->
<PackageVersion Include="Google.GenAI" Version="0.6.0" />
<PackageVersion Include="Google.GenAI" Version="0.9.0" />
<PackageVersion Include="Mscc.GenerativeAI.Microsoft" Version="2.9.3" />
<!-- Microsoft.Azure.* -->
<PackageVersion Include="Microsoft.Azure.Cosmos" Version="3.54.0" />
@@ -61,10 +61,9 @@
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.0.0" />
<!-- Microsoft.Extensions.* -->
<PackageVersion Include="Microsoft.Extensions.AI" Version="10.1.0" />
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="10.1.0" />
<PackageVersion Include="Microsoft.Extensions.AI.AzureAIInference" Version="10.0.0-preview.1.25559.3" />
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="10.1.0-preview.1.25608.1" />
<PackageVersion Include="Microsoft.Extensions.AI" Version="10.1.1" />
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="10.1.1" />
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="10.1.1-preview.1.25612.2" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.0" />
@@ -101,11 +100,10 @@
<!-- MCP -->
<PackageVersion Include="ModelContextProtocol" Version="0.4.0-preview.3" />
<!-- Inference SDKs -->
<PackageVersion Include="Anthropic.SDK" Version="5.8.0" />
<PackageVersion Include="AWSSDK.Extensions.Bedrock.MEAI" Version="4.0.4.7" />
<PackageVersion Include="AWSSDK.Extensions.Bedrock.MEAI" Version="4.0.5.1" />
<PackageVersion Include="Microsoft.ML.OnnxRuntimeGenAI" Version="0.10.0" />
<PackageVersion Include="OllamaSharp" Version="5.4.8" />
<PackageVersion Include="OpenAI" Version="2.7.0" />
<PackageVersion Include="OpenAI" Version="2.8.0" />
<!-- Identity -->
<PackageVersion Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.78.0" />
<!-- Workflows -->
@@ -114,19 +112,21 @@
<PackageVersion Include="Microsoft.Bot.ObjectModel.PowerFx" Version="1.2025.1106.1" />
<PackageVersion Include="Microsoft.PowerFx.Interpreter" Version="1.5.0-build.20251008-1002" />
<!-- Durable Task -->
<PackageVersion Include="Microsoft.DurableTask.Client" Version="1.16.2" />
<PackageVersion Include="Microsoft.DurableTask.Client.AzureManaged" Version="1.16.2-preview.1" />
<PackageVersion Include="Microsoft.DurableTask.Worker" Version="1.16.2" />
<PackageVersion Include="Microsoft.DurableTask.Worker.AzureManaged" Version="1.16.2-preview.1" />
<PackageVersion Include="Microsoft.DurableTask.Client" Version="1.18.0" />
<PackageVersion Include="Microsoft.DurableTask.Client.AzureManaged" Version="1.18.0" />
<PackageVersion Include="Microsoft.DurableTask.Worker" Version="1.18.0" />
<PackageVersion Include="Microsoft.DurableTask.Worker.AzureManaged" Version="1.18.0" />
<!-- Azure Functions -->
<PackageVersion Include="Microsoft.Azure.Functions.Worker" Version="2.50.0" />
<PackageVersion Include="Microsoft.Azure.Functions.Worker.ApplicationInsights" Version="2.50.0" />
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" Version="1.9.0" />
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged" Version="1.0.0" />
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" Version="1.11.0" />
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged" Version="1.0.1" />
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.3.0" />
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" Version="2.1.0" />
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.Mcp" Version="1.0.0" />
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Sdk" Version="2.0.7" />
<!-- Redis -->
<PackageVersion Include="StackExchange.Redis" Version="2.10.1" />
<!-- Test -->
<PackageVersion Include="FluentAssertions" Version="8.8.0" />
<PackageVersion Include="Microsoft.AspNetCore.TestHost" Condition="'$(TargetFramework)' == 'net8.0'" Version="8.0.22" />
+2
View File
@@ -33,6 +33,7 @@
<Project Path="samples/AzureFunctions/05_AgentOrchestration_HITL/05_AgentOrchestration_HITL.csproj" />
<Project Path="samples/AzureFunctions/06_LongRunningTools/06_LongRunningTools.csproj" />
<Project Path="samples/AzureFunctions/07_AgentAsMcpTool/07_AgentAsMcpTool.csproj" />
<Project Path="samples/AzureFunctions/08_ReliableStreaming/08_ReliableStreaming.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/">
<File Path="samples/GettingStarted/README.md" />
@@ -129,6 +130,7 @@
<Project Path="samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step02_Reasoning/Agent_OpenAI_Step02_Reasoning.csproj" />
<Project Path="samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step03_CreateFromChatClient/Agent_OpenAI_Step03_CreateFromChatClient.csproj" />
<Project Path="samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step04_CreateFromOpenAIResponseClient/Agent_OpenAI_Step04_CreateFromOpenAIResponseClient.csproj" />
<Project Path="samples/GettingStarted/AgentWithOpenAI/Agent_OpenAI_Step05_Conversation/Agent_OpenAI_Step05_Conversation.csproj" />
</Folder>
<Folder Name="/Samples/Purview/" />
<Folder Name="/Samples/Purview/AgentWithPurview/">
+3 -3
View File
@@ -2,9 +2,9 @@
<PropertyGroup>
<!-- Central version prefix - applies to all nuget packages. -->
<VersionPrefix>1.0.0</VersionPrefix>
<PackageVersion Condition="'$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).251204.1</PackageVersion>
<PackageVersion Condition="'$(VersionSuffix)' == ''">$(VersionPrefix)-preview.251204.1</PackageVersion>
<GitTag>1.0.0-preview.251204.1</GitTag>
<PackageVersion Condition="'$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).260108.1</PackageVersion>
<PackageVersion Condition="'$(VersionSuffix)' == ''">$(VersionPrefix)-preview.260108.1</PackageVersion>
<GitTag>1.0.0-preview.260108.1</GitTag>
<Configurations>Debug;Release;Publish</Configurations>
<IsPackable>true</IsPackable>
@@ -19,12 +19,12 @@ internal sealed class AgenticUIAgent : DelegatingAIAgent
this._jsonSerializerOptions = jsonSerializerOptions;
}
public override Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
protected override Task<AgentRunResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
return this.RunStreamingAsync(messages, thread, options, cancellationToken).ToAgentRunResponseAsync(cancellationToken);
return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken).ToAgentRunResponseAsync(cancellationToken);
}
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
@@ -20,12 +20,12 @@ internal sealed class PredictiveStateUpdatesAgent : DelegatingAIAgent
this._jsonSerializerOptions = jsonSerializerOptions;
}
public override Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
protected override Task<AgentRunResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
return this.RunStreamingAsync(messages, thread, options, cancellationToken).ToAgentRunResponseAsync(cancellationToken);
return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken).ToAgentRunResponseAsync(cancellationToken);
}
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
@@ -19,12 +19,12 @@ internal sealed class SharedStateAgent : DelegatingAIAgent
this._jsonSerializerOptions = jsonSerializerOptions;
}
public override Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
protected override Task<AgentRunResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
return this.RunStreamingAsync(messages, thread, options, cancellationToken).ToAgentRunResponseAsync(cancellationToken);
return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken).ToAgentRunResponseAsync(cancellationToken);
}
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
@@ -25,7 +25,6 @@
<PackageReference Include="CommunityToolkit.Aspire.OllamaSharp" />
<PackageReference Include="Microsoft.Extensions.AI" />
<PackageReference Include="Microsoft.Extensions.AI.Abstractions" />
<PackageReference Include="Microsoft.Extensions.AI.AzureAIInference" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
<PackageReference Include="Microsoft.AspNetCore.OpenAPI" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" />
@@ -1,8 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
using AgentWebChat.AgentHost.Utilities;
using Azure;
using Azure.AI.Inference;
using Microsoft.Extensions.AI;
using OllamaSharp;
@@ -24,7 +22,6 @@ public static class ChatClientExtensions
ClientChatProvider.Ollama => builder.AddOllamaClient(connectionName, connectionInfo),
ClientChatProvider.OpenAI => builder.AddOpenAIClient(connectionName, connectionInfo),
ClientChatProvider.AzureOpenAI => builder.AddAzureOpenAIClient(connectionName).AddChatClient(connectionInfo.SelectedModel),
ClientChatProvider.AzureAIInference => builder.AddAzureInferenceClient(connectionName, connectionInfo),
_ => throw new NotSupportedException($"Unsupported provider: {connectionInfo.Provider}")
};
@@ -44,16 +41,6 @@ public static class ChatClientExtensions
})
.AddChatClient(connectionInfo.SelectedModel);
private static ChatClientBuilder AddAzureInferenceClient(this IHostApplicationBuilder builder, string connectionName, ChatClientConnectionInfo connectionInfo) =>
builder.Services.AddChatClient(sp =>
{
var credential = new AzureKeyCredential(connectionInfo.AccessKey!);
var client = new ChatCompletionsClient(connectionInfo.Endpoint, credential, new AzureAIInferenceClientOptions());
return client.AsIChatClient(connectionInfo.SelectedModel);
});
private static ChatClientBuilder AddOllamaClient(this IHostApplicationBuilder builder, string connectionName, ChatClientConnectionInfo connectionInfo)
{
var httpKey = $"{connectionName}_http";
@@ -83,7 +70,6 @@ public static class ChatClientExtensions
ClientChatProvider.Ollama => builder.AddKeyedOllamaClient(connectionName, connectionInfo),
ClientChatProvider.OpenAI => builder.AddKeyedOpenAIClient(connectionName, connectionInfo),
ClientChatProvider.AzureOpenAI => builder.AddKeyedAzureOpenAIClient(connectionName).AddKeyedChatClient(connectionName, connectionInfo.SelectedModel),
ClientChatProvider.AzureAIInference => builder.AddKeyedAzureInferenceClient(connectionName, connectionInfo),
_ => throw new NotSupportedException($"Unsupported provider: {connectionInfo.Provider}")
};
@@ -103,16 +89,6 @@ public static class ChatClientExtensions
})
.AddKeyedChatClient(connectionName, connectionInfo.SelectedModel);
private static ChatClientBuilder AddKeyedAzureInferenceClient(this IHostApplicationBuilder builder, string connectionName, ChatClientConnectionInfo connectionInfo) =>
builder.Services.AddKeyedChatClient(connectionName, sp =>
{
var credential = new AzureKeyCredential(connectionInfo.AccessKey!);
var client = new ChatCompletionsClient(connectionInfo.Endpoint, credential, new AzureAIInferenceClientOptions());
return client.AsIChatClient(connectionInfo.SelectedModel);
});
private static ChatClientBuilder AddKeyedOllamaClient(this IHostApplicationBuilder builder, string connectionName, ChatClientConnectionInfo connectionInfo)
{
var httpKey = $"{connectionName}_http";
@@ -27,7 +27,7 @@ internal sealed class OpenAIResponsesAgentClient(HttpClient httpClient) : AgentC
Transport = new HttpClientPipelineTransport(httpClient)
};
var openAiClient = new OpenAIResponseClient(model: agentName, credential: new ApiKeyCredential("dummy-key"), options: options).AsIChatClient();
var openAiClient = new ResponsesClient(model: agentName, credential: new ApiKeyCredential("dummy-key"), options: options).AsIChatClient();
var chatOptions = new ChatOptions()
{
ConversationId = threadId
@@ -32,6 +32,6 @@ AIAgent agent = client.GetChatClient(deploymentName).CreateAIAgent(JokerInstruct
using IHost app = FunctionsApplication
.CreateBuilder(args)
.ConfigureFunctionsWebApplication()
.ConfigureDurableAgents(options => options.AddAIAgent(agent))
.ConfigureDurableAgents(options => options.AddAIAgent(agent, timeToLive: TimeSpan.FromHours(1)))
.Build();
app.Run();
@@ -0,0 +1,47 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net10.0</TargetFrameworks>
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
<OutputType>Exe</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!-- The Functions build tools don't like namespaces that start with a number -->
<AssemblyName>ReliableStreaming</AssemblyName>
<RootNamespace>ReliableStreaming</RootNamespace>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<!-- Azure Functions packages -->
<ItemGroup>
<PackageReference Include="Microsoft.Azure.Functions.Worker" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
</ItemGroup>
<!-- Redis for reliable streaming -->
<ItemGroup>
<PackageReference Include="StackExchange.Redis" />
</ItemGroup>
<!-- Local projects that should be switched to package references when using the sample outside of this MAF repo -->
<!--
<ItemGroup>
<PackageReference Include="Microsoft.Agents.AI.Hosting.AzureFunctions" />
<PackageReference Include="Microsoft.Agents.AI.OpenAI" />
</ItemGroup>
-->
<ItemGroup>
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,319 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.DurableTask;
using Microsoft.Agents.AI.Hosting.AzureFunctions;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.Functions.Worker;
using Microsoft.DurableTask.Client;
using Microsoft.Extensions.Logging;
namespace ReliableStreaming;
/// <summary>
/// HTTP trigger functions for reliable streaming of durable agent responses.
/// </summary>
/// <remarks>
/// This class exposes two endpoints:
/// <list type="bullet">
/// <item>
/// <term>Create</term>
/// <description>Starts an agent run and streams responses. The response format depends on the
/// <c>Accept</c> header: <c>text/plain</c> returns raw text (ideal for terminals), while
/// <c>text/event-stream</c> or any other value returns Server-Sent Events (SSE).</description>
/// </item>
/// <item>
/// <term>Stream</term>
/// <description>Resumes a stream from a cursor position, enabling reliable message delivery</description>
/// </item>
/// </list>
/// </remarks>
public sealed class FunctionTriggers
{
private readonly RedisStreamResponseHandler _streamHandler;
private readonly ILogger<FunctionTriggers> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="FunctionTriggers"/> class.
/// </summary>
/// <param name="streamHandler">The Redis stream handler for reading/writing agent responses.</param>
/// <param name="logger">The logger instance.</param>
public FunctionTriggers(RedisStreamResponseHandler streamHandler, ILogger<FunctionTriggers> logger)
{
this._streamHandler = streamHandler;
this._logger = logger;
}
/// <summary>
/// Creates a new agent session, starts an agent run with the provided prompt,
/// and streams the response back to the client.
/// </summary>
/// <remarks>
/// <para>
/// The response format depends on the <c>Accept</c> header:
/// <list type="bullet">
/// <item><c>text/plain</c>: Returns raw text output, ideal for terminal display with curl</item>
/// <item><c>text/event-stream</c> or other: Returns Server-Sent Events (SSE) with cursor support</item>
/// </list>
/// </para>
/// <para>
/// The response includes an <c>x-conversation-id</c> header containing the conversation ID.
/// For SSE responses, clients can use this conversation ID to resume the stream if disconnected
/// by calling the <see cref="StreamAsync"/> endpoint with the conversation ID and the last received cursor.
/// </para>
/// <para>
/// Each SSE event contains the following fields:
/// <list type="bullet">
/// <item><c>id</c>: The Redis stream entry ID (use as cursor for resumption)</item>
/// <item><c>event</c>: Either "message" for content or "done" for stream completion</item>
/// <item><c>data</c>: The text content of the response chunk</item>
/// </list>
/// </para>
/// </remarks>
/// <param name="request">The HTTP request containing the prompt in the body.</param>
/// <param name="durableClient">The Durable Task client for signaling agents.</param>
/// <param name="context">The function invocation context.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A streaming response in the format specified by the Accept header.</returns>
[Function(nameof(CreateAsync))]
public async Task<IActionResult> CreateAsync(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "agent/create")] HttpRequest request,
[DurableClient] DurableTaskClient durableClient,
FunctionContext context,
CancellationToken cancellationToken)
{
// Read the prompt from the request body
string prompt = await new StreamReader(request.Body).ReadToEndAsync(cancellationToken);
if (string.IsNullOrWhiteSpace(prompt))
{
return new BadRequestObjectResult("Request body must contain a prompt.");
}
AIAgent agentProxy = durableClient.AsDurableAgentProxy(context, "TravelPlanner");
// Create a new agent thread
AgentThread thread = agentProxy.GetNewThread();
string agentSessionId = thread.GetService<AgentSessionId>().ToString();
this._logger.LogInformation("Creating new agent session: {AgentSessionId}", agentSessionId);
// Run the agent in the background (fire-and-forget)
DurableAgentRunOptions options = new() { IsFireAndForget = true };
await agentProxy.RunAsync(prompt, thread, options, cancellationToken);
this._logger.LogInformation("Agent run started for session: {AgentSessionId}", agentSessionId);
// Check Accept header to determine response format
// text/plain = raw text output (ideal for terminals)
// text/event-stream or other = SSE format (supports resumption)
string? acceptHeader = request.Headers.Accept.FirstOrDefault();
bool useSseFormat = acceptHeader?.Contains("text/plain", StringComparison.OrdinalIgnoreCase) != true;
return await this.StreamToClientAsync(
conversationId: agentSessionId, cursor: null, useSseFormat, request.HttpContext, cancellationToken);
}
/// <summary>
/// Resumes streaming from a specific cursor position for an existing session.
/// </summary>
/// <remarks>
/// <para>
/// Use this endpoint to resume a stream after disconnection. Pass the conversation ID
/// (from the <c>x-conversation-id</c> response header) and the last received cursor
/// (Redis stream entry ID) to continue from where you left off.
/// </para>
/// <para>
/// If no cursor is provided, streaming starts from the beginning of the stream.
/// This allows clients to replay the entire response if needed.
/// </para>
/// <para>
/// The response format depends on the <c>Accept</c> header:
/// <list type="bullet">
/// <item><c>text/plain</c>: Returns raw text output, ideal for terminal display with curl</item>
/// <item><c>text/event-stream</c> or other: Returns Server-Sent Events (SSE) with cursor support</item>
/// </list>
/// </para>
/// </remarks>
/// <param name="request">The HTTP request. Use the <c>cursor</c> query parameter to specify the cursor position.</param>
/// <param name="conversationId">The conversation ID to stream from.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A streaming response in the format specified by the Accept header.</returns>
[Function(nameof(StreamAsync))]
public async Task<IActionResult> StreamAsync(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "agent/stream/{conversationId}")] HttpRequest request,
string conversationId,
CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(conversationId))
{
return new BadRequestObjectResult("Conversation ID is required.");
}
// Get the cursor from query string (optional)
string? cursor = request.Query["cursor"].FirstOrDefault();
this._logger.LogInformation(
"Resuming stream for conversation {ConversationId} from cursor: {Cursor}",
conversationId,
cursor ?? "(beginning)");
// Check Accept header to determine response format
// text/plain = raw text output (ideal for terminals)
// text/event-stream or other = SSE format (supports cursor-based resumption)
string? acceptHeader = request.Headers.Accept.FirstOrDefault();
bool useSseFormat = acceptHeader?.Contains("text/plain", StringComparison.OrdinalIgnoreCase) != true;
return await this.StreamToClientAsync(conversationId, cursor, useSseFormat, request.HttpContext, cancellationToken);
}
/// <summary>
/// Streams chunks from the Redis stream to the HTTP response.
/// </summary>
/// <param name="conversationId">The conversation ID to stream from.</param>
/// <param name="cursor">Optional cursor to resume from. If null, streams from the beginning.</param>
/// <param name="useSseFormat">True to use SSE format, false for plain text.</param>
/// <param name="httpContext">The HTTP context for writing the response.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>An empty result after streaming completes.</returns>
private async Task<IActionResult> StreamToClientAsync(
string conversationId,
string? cursor,
bool useSseFormat,
HttpContext httpContext,
CancellationToken cancellationToken)
{
// Set response headers based on format
httpContext.Response.Headers.ContentType = useSseFormat
? "text/event-stream"
: "text/plain; charset=utf-8";
httpContext.Response.Headers.CacheControl = "no-cache";
httpContext.Response.Headers.Connection = "keep-alive";
httpContext.Response.Headers["x-conversation-id"] = conversationId;
// Disable response buffering if supported
httpContext.Features.Get<IHttpResponseBodyFeature>()?.DisableBuffering();
try
{
await foreach (StreamChunk chunk in this._streamHandler.ReadStreamAsync(
conversationId,
cursor,
cancellationToken))
{
if (chunk.Error != null)
{
this._logger.LogWarning("Stream error for conversation {ConversationId}: {Error}", conversationId, chunk.Error);
await WriteErrorAsync(httpContext.Response, chunk.Error, useSseFormat, cancellationToken);
break;
}
if (chunk.IsDone)
{
await WriteEndOfStreamAsync(httpContext.Response, chunk.EntryId, useSseFormat, cancellationToken);
break;
}
if (chunk.Text != null)
{
await WriteChunkAsync(httpContext.Response, chunk, useSseFormat, cancellationToken);
}
}
}
catch (OperationCanceledException)
{
this._logger.LogInformation("Client disconnected from stream {ConversationId}", conversationId);
}
return new EmptyResult();
}
/// <summary>
/// Writes a text chunk to the response.
/// </summary>
private static async Task WriteChunkAsync(
HttpResponse response,
StreamChunk chunk,
bool useSseFormat,
CancellationToken cancellationToken)
{
if (useSseFormat)
{
await WriteSSEEventAsync(response, "message", chunk.Text!, chunk.EntryId);
}
else
{
await response.WriteAsync(chunk.Text!, cancellationToken);
}
await response.Body.FlushAsync(cancellationToken);
}
/// <summary>
/// Writes an end-of-stream marker to the response.
/// </summary>
private static async Task WriteEndOfStreamAsync(
HttpResponse response,
string entryId,
bool useSseFormat,
CancellationToken cancellationToken)
{
if (useSseFormat)
{
await WriteSSEEventAsync(response, "done", "[DONE]", entryId);
}
else
{
await response.WriteAsync("\n", cancellationToken);
}
await response.Body.FlushAsync(cancellationToken);
}
/// <summary>
/// Writes an error message to the response.
/// </summary>
private static async Task WriteErrorAsync(
HttpResponse response,
string error,
bool useSseFormat,
CancellationToken cancellationToken)
{
if (useSseFormat)
{
await WriteSSEEventAsync(response, "error", error, null);
}
else
{
await response.WriteAsync($"\n[Error: {error}]\n", cancellationToken);
}
await response.Body.FlushAsync(cancellationToken);
}
/// <summary>
/// Writes a Server-Sent Event to the response stream.
/// </summary>
private static async Task WriteSSEEventAsync(
HttpResponse response,
string eventType,
string data,
string? id)
{
StringBuilder sb = new();
// Include the ID if provided (used as cursor for resumption)
if (!string.IsNullOrEmpty(id))
{
sb.AppendLine($"id: {id}");
}
sb.AppendLine($"event: {eventType}");
sb.AppendLine($"data: {data}");
sb.AppendLine(); // Empty line marks end of event
await response.WriteAsync(sb.ToString());
}
}
@@ -0,0 +1,100 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample demonstrates how to implement reliable streaming for durable agents using Redis Streams.
// It exposes two HTTP endpoints:
// 1. Create - Starts an agent run and streams responses back via Server-Sent Events (SSE)
// 2. Stream - Resumes a stream from a specific cursor position, enabling reliable message delivery
//
// This pattern is inspired by OpenAI's background mode for the Responses API, which allows clients
// to disconnect and reconnect to ongoing agent responses without losing messages.
using Azure;
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI.DurableTask;
using Microsoft.Agents.AI.Hosting.AzureFunctions;
using Microsoft.Azure.Functions.Worker.Builder;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using OpenAI.Chat;
using ReliableStreaming;
using StackExchange.Redis;
// Get the Azure OpenAI endpoint and deployment name from environment variables.
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT")
?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT is not set.");
// Get Redis connection string from environment variable.
string redisConnectionString = Environment.GetEnvironmentVariable("REDIS_CONNECTION_STRING")
?? "localhost:6379";
// Get the Redis stream TTL from environment variable (default: 10 minutes).
int redisStreamTtlMinutes = int.TryParse(
Environment.GetEnvironmentVariable("REDIS_STREAM_TTL_MINUTES"),
out int ttlMinutes) ? ttlMinutes : 10;
// Use Azure Key Credential if provided, otherwise use Azure CLI Credential.
string? azureOpenAiKey = System.Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY");
AzureOpenAIClient client = !string.IsNullOrEmpty(azureOpenAiKey)
? new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(azureOpenAiKey))
: new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential());
// Travel Planner agent instructions - designed to produce longer responses for demonstrating streaming.
const string TravelPlannerName = "TravelPlanner";
const string TravelPlannerInstructions =
"""
You are an expert travel planner who creates detailed, personalized travel itineraries.
When asked to plan a trip, you should:
1. Create a comprehensive day-by-day itinerary
2. Include specific recommendations for activities, restaurants, and attractions
3. Provide practical tips for each destination
4. Consider weather and local events when making recommendations
5. Include estimated times and logistics between activities
Always use the available tools to get current weather forecasts and local events
for the destination to make your recommendations more relevant and timely.
Format your response with clear headings for each day and include emoji icons
to make the itinerary easy to scan and visually appealing.
""";
// Configure the function app to host the AI agent.
FunctionsApplicationBuilder builder = FunctionsApplication
.CreateBuilder(args)
.ConfigureFunctionsWebApplication()
.ConfigureDurableAgents(options =>
{
// Define the Travel Planner agent with tools for weather and events
options.AddAIAgentFactory(TravelPlannerName, sp =>
{
return client.GetChatClient(deploymentName).CreateAIAgent(
instructions: TravelPlannerInstructions,
name: TravelPlannerName,
services: sp,
tools: [
AIFunctionFactory.Create(TravelTools.GetWeatherForecast),
AIFunctionFactory.Create(TravelTools.GetLocalEvents),
]);
});
});
// Register Redis connection as a singleton
builder.Services.AddSingleton<IConnectionMultiplexer>(_ =>
ConnectionMultiplexer.Connect(redisConnectionString));
// Register the Redis stream response handler - this captures agent responses
// and publishes them to Redis Streams for reliable delivery.
// Registered as both the concrete type (for FunctionTriggers) and the interface (for the agent framework).
builder.Services.AddSingleton(sp =>
new RedisStreamResponseHandler(
sp.GetRequiredService<IConnectionMultiplexer>(),
TimeSpan.FromMinutes(redisStreamTtlMinutes)));
builder.Services.AddSingleton<IAgentResponseHandler>(sp =>
sp.GetRequiredService<RedisStreamResponseHandler>());
using IHost app = builder.Build();
app.Run();
@@ -0,0 +1,264 @@
# Reliable Streaming with Redis
This sample demonstrates how to implement reliable streaming for durable agents using Redis Streams as a message broker. It enables clients to disconnect and reconnect to ongoing agent responses without losing messages, inspired by [OpenAI's background mode](https://platform.openai.com/docs/guides/background) for the Responses API.
## Key Concepts Demonstrated
- **Reliable message delivery**: Agent responses are persisted to Redis Streams, allowing clients to resume from any point
- **Content negotiation**: Use `Accept: text/plain` for raw terminal output, or `Accept: text/event-stream` for SSE format
- **Server-Sent Events (SSE)**: Standard streaming format that works with `curl`, browsers, and most HTTP clients
- **Cursor-based resumption**: Each SSE event includes an `id` field that can be used to resume the stream
- **Fire-and-forget agent invocation**: The agent runs in the background while the client streams from Redis via an HTTP trigger function
## Environment Setup
See the [README.md](../README.md) file in the parent directory for more information on how to configure the environment, including how to install and run common sample dependencies.
### Additional Requirements: Redis
This sample requires a Redis instance. Start a local Redis instance using Docker:
```bash
docker run -d --name redis -p 6379:6379 redis:latest
```
To verify Redis is running:
```bash
docker ps | grep redis
```
## Running the Sample
Start the Azure Functions host:
```bash
func start
```
### 1. Test Streaming with curl
Open a new terminal and start a travel planning request. Use the `-i` flag to see response headers (including the conversation ID) and `Accept: text/plain` for raw text output:
**Bash (Linux/macOS/WSL):**
```bash
curl -i -N -X POST http://localhost:7071/api/agent/create \
-H "Content-Type: text/plain" \
-H "Accept: text/plain" \
-d "Plan a 7-day trip to Tokyo, Japan for next month. Include daily activities, restaurant recommendations, and tips for getting around."
```
**PowerShell:**
```powershell
curl -i -N -X POST http://localhost:7071/api/agent/create `
-H "Content-Type: text/plain" `
-H "Accept: text/plain" `
-d "Plan a 7-day trip to Tokyo, Japan for next month. Include daily activities, restaurant recommendations, and tips for getting around."
```
You'll first see the response headers, including:
```text
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
x-conversation-id: @dafx-travelplanner@a1b2c3d4e5f67890abcdef1234567890
...
```
Then the agent's response will stream to your terminal in chunks, similar to a ChatGPT-style experience (though not character-by-character).
> **Note:** The `-N` flag in curl disables output buffering, which is essential for seeing the stream in real-time. The `-i` flag includes the HTTP headers in the output.
### 2. Demonstrate Stream Interruption and Resumption
This is the key feature of reliable streaming! Follow these steps to see it in action:
#### Step 1: Start a stream and note the conversation ID
Run the curl command from step 1. Watch for the `x-conversation-id` header in the response - **copy this value**, you'll need it to resume.
```text
x-conversation-id: @dafx-travelplanner@a1b2c3d4e5f67890abcdef1234567890
```
#### Step 2: Interrupt the stream
While the agent is still generating text, press **`Ctrl+C`** to interrupt the stream. The agent continues running in the background - your messages are being saved to Redis!
#### Step 3: Resume the stream
Use the conversation ID you copied to resume streaming from where you left off. Include the `Accept: text/plain` header to get raw text output:
**Bash (Linux/macOS/WSL):**
```bash
# Replace with your actual conversation ID from the x-conversation-id header
CONVERSATION_ID="@dafx-travelplanner@a1b2c3d4e5f67890abcdef1234567890"
curl -N -H "Accept: text/plain" "http://localhost:7071/api/agent/stream/${CONVERSATION_ID}"
```
**PowerShell:**
```powershell
# Replace with your actual conversation ID from the x-conversation-id header
$conversationId = "@dafx-travelplanner@a1b2c3d4e5f67890abcdef1234567890"
curl -N -H "Accept: text/plain" "http://localhost:7071/api/agent/stream/$conversationId"
```
You'll see the **entire response replayed from the beginning**, including the parts you already received before interrupting.
#### Step 4 (Advanced): Resume from a specific cursor
If you're using SSE format, each event includes an `id` field that you can use as a cursor to resume from a specific point:
```bash
# Resume from a specific cursor position
curl -N "http://localhost:7071/api/agent/stream/${CONVERSATION_ID}?cursor=1734567890123-0"
```
### 3. Alternative: SSE Format for Programmatic Clients
If you need the full Server-Sent Events format with cursors for resumable streaming, use `Accept: text/event-stream` (or omit the Accept header):
```bash
curl -i -N -X POST http://localhost:7071/api/agent/create \
-H "Content-Type: text/plain" \
-H "Accept: text/event-stream" \
-d "Plan a 7-day trip to Tokyo, Japan."
```
This returns SSE-formatted events with `id`, `event`, and `data` fields:
```text
id: 1734567890123-0
event: message
data: # 7-Day Tokyo Adventure
id: 1734567890124-0
event: message
data: ## Day 1: Arrival and Exploration
id: 1734567890999-0
event: done
data: [DONE]
```
The `id` field is the Redis stream entry ID - use it as the `cursor` parameter to resume from that exact point.
### Understanding the Response Headers
| Header | Description |
|--------|-------------|
| `x-conversation-id` | The conversation ID (session key). Use this to resume the stream. |
| `Content-Type` | Either `text/plain` or `text/event-stream` depending on your `Accept` header. |
| `Cache-Control` | Set to `no-cache` to prevent caching of the stream. |
## Architecture Overview
```text
┌─────────────┐ POST /agent/create ┌─────────────────────┐
│ Client │ (Accept: text/plain or SSE)│ Azure Functions │
│ (curl) │ ──────────────────────────► │ (FunctionTriggers) │
└─────────────┘ └──────────┬──────────┘
▲ │
│ Text or SSE stream Signal Entity
│ │
│ ▼
│ ┌─────────────────────┐
│ │ AgentEntity │
│ │ (Durable Entity) │
│ └──────────┬──────────┘
│ │
│ IAgentResponseHandler
│ │
│ ▼
│ ┌─────────────────────┐
│ │ RedisStreamResponse │
│ │ Handler │
│ └──────────┬──────────┘
│ │
│ XADD (write)
│ │
│ ▼
│ ┌─────────────────────┐
└─────────── XREAD (poll) ────────── │ Redis Streams │
│ (Durable Log) │
└─────────────────────┘
```
### Data Flow
1. **Client sends prompt**: The `Create` endpoint receives the prompt and generates a new agent thread.
2. **Agent invoked**: The durable entity (`AgentEntity`) is signaled to run the travel planner agent. This is fire-and-forget from the HTTP request's perspective.
3. **Responses captured**: As the agent generates responses, `RedisStreamResponseHandler` (implementing `IAgentResponseHandler`) extracts the text from each `AgentRunResponseUpdate` and publishes it to a Redis Stream keyed by session ID.
4. **Client polls Redis**: The HTTP response streams events by polling the Redis Stream. For SSE format, each event includes the Redis entry ID as the `id` field.
5. **Resumption**: If the client disconnects, it can call the `Stream` endpoint with the conversation ID (from the `x-conversation-id` header) and optionally the last received cursor to resume from that point.
## Message Delivery Guarantees
This sample provides **at-least-once delivery** with the following characteristics:
- **Durability**: Messages are persisted to Redis Streams with configurable TTL (default: 10 minutes).
- **Ordering**: Messages are delivered in order within a session.
- **Resumption**: Clients can resume from any point using cursor-based pagination.
- **Replay**: Clients can replay the entire stream by omitting the cursor.
### Important Considerations
- **No exactly-once delivery**: If a client disconnects exactly when receiving a message, it may receive that message again upon resumption. Clients should handle duplicate messages idempotently.
- **TTL expiration**: Streams expire after the configured TTL. Clients cannot resume streams that have expired.
- **Redis guarantees**: Redis streams are backed by Redis persistence mechanisms (RDB/AOF). Ensure your Redis instance is configured for durability as needed.
## When to Use These Patterns
The patterns demonstrated in this sample are ideal for:
- **Long-running agent tasks**: When agent responses take minutes to complete (e.g., deep research, complex planning)
- **Unreliable network connections**: Mobile apps, unstable WiFi, or connections that may drop
- **Resumable experiences**: Users should be able to close and reopen an app without losing context
- **Background processing**: When you want to fire off a task and check on it later
These patterns may be overkill for:
- **Simple, fast responses**: If responses complete in a few seconds, standard streaming is simpler
- **Stateless interactions**: If there's no need to resume or replay conversations
- **Very high throughput**: Redis adds latency; for maximum throughput, direct streaming may be better
## Configuration
| Environment Variable | Description | Default |
|---------------------|-------------|---------|
| `REDIS_CONNECTION_STRING` | Redis connection string | `localhost:6379` |
| `REDIS_STREAM_TTL_MINUTES` | How long streams are retained after last write | `10` |
| `AZURE_OPENAI_ENDPOINT` | Azure OpenAI endpoint URL | (required) |
| `AZURE_OPENAI_DEPLOYMENT` | Azure OpenAI deployment name | (required) |
| `AZURE_OPENAI_KEY` | API key (optional, uses Azure CLI auth if not set) | (optional) |
## Cleanup
To stop and remove the Redis Docker containers:
```bash
docker stop redis
docker rm redis
```
## Disclaimer
> ⚠️ **This sample is for illustration purposes only and is not intended to be production-ready.**
>
> A production implementation should consider:
>
> - Redis cluster configuration for high availability
> - Authentication and authorization for the streaming endpoints
> - Rate limiting and abuse prevention
> - Monitoring and alerting for stream health
> - Graceful handling of Redis failures
@@ -0,0 +1,212 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Runtime.CompilerServices;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.DurableTask;
using StackExchange.Redis;
namespace ReliableStreaming;
/// <summary>
/// Represents a chunk of data read from a Redis stream.
/// </summary>
/// <param name="EntryId">The Redis stream entry ID (can be used as a cursor for resumption).</param>
/// <param name="Text">The text content of the chunk, or null if this is a completion/error marker.</param>
/// <param name="IsDone">True if this chunk marks the end of the stream.</param>
/// <param name="Error">An error message if something went wrong, or null otherwise.</param>
public readonly record struct StreamChunk(string EntryId, string? Text, bool IsDone, string? Error);
/// <summary>
/// An implementation of <see cref="IAgentResponseHandler"/> that publishes agent response updates
/// to Redis Streams for reliable delivery. This enables clients to disconnect and reconnect
/// to ongoing agent responses without losing messages.
/// </summary>
/// <remarks>
/// <para>
/// Redis Streams provide a durable, append-only log that supports consumer groups and message
/// acknowledgment. This implementation uses auto-generated IDs (which are timestamp-based)
/// as sequence numbers, allowing clients to resume from any point in the stream.
/// </para>
/// <para>
/// Each agent session gets its own Redis Stream, keyed by session ID. The stream entries
/// contain text chunks extracted from <see cref="AgentRunResponseUpdate"/> objects.
/// </para>
/// </remarks>
public sealed class RedisStreamResponseHandler : IAgentResponseHandler
{
private const int MaxEmptyReads = 300; // 5 minutes at 1 second intervals
private const int PollIntervalMs = 1000;
private readonly IConnectionMultiplexer _redis;
private readonly TimeSpan _streamTtl;
/// <summary>
/// Initializes a new instance of the <see cref="RedisStreamResponseHandler" /> class.
/// </summary>
/// <param name="redis">The Redis connection multiplexer.</param>
/// <param name="streamTtl">The time-to-live for stream entries. Streams will expire after this duration of inactivity.</param>
public RedisStreamResponseHandler(IConnectionMultiplexer redis, TimeSpan streamTtl)
{
this._redis = redis;
this._streamTtl = streamTtl;
}
/// <inheritdoc/>
public async ValueTask OnStreamingResponseUpdateAsync(
IAsyncEnumerable<AgentRunResponseUpdate> messageStream,
CancellationToken cancellationToken)
{
// Get the current session ID from the DurableAgentContext
// This is set by the AgentEntity before invoking the response handler
DurableAgentContext? context = DurableAgentContext.Current;
if (context is null)
{
throw new InvalidOperationException(
"DurableAgentContext.Current is not set. This handler must be used within a durable agent context.");
}
// Get session ID from the current thread context, which is only available in the context of
// a durable agent execution.
string agentSessionId = context.CurrentThread.GetService<AgentSessionId>().ToString();
string streamKey = GetStreamKey(agentSessionId);
IDatabase db = this._redis.GetDatabase();
int sequenceNumber = 0;
await foreach (AgentRunResponseUpdate update in messageStream.WithCancellation(cancellationToken))
{
// Extract just the text content - this avoids serialization round-trip issues
string text = update.Text;
// Only publish non-empty text chunks
if (!string.IsNullOrEmpty(text))
{
// Create the stream entry with the text and metadata
NameValueEntry[] entries =
[
new NameValueEntry("text", text),
new NameValueEntry("sequence", sequenceNumber++),
new NameValueEntry("timestamp", DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()),
];
// Add to the Redis Stream with auto-generated ID (timestamp-based)
await db.StreamAddAsync(streamKey, entries);
// Refresh the TTL on each write to keep the stream alive during active streaming
await db.KeyExpireAsync(streamKey, this._streamTtl);
}
}
// Add a sentinel entry to mark the end of the stream
NameValueEntry[] endEntries =
[
new NameValueEntry("text", ""),
new NameValueEntry("sequence", sequenceNumber),
new NameValueEntry("timestamp", DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()),
new NameValueEntry("done", "true"),
];
await db.StreamAddAsync(streamKey, endEntries);
// Set final TTL - the stream will be cleaned up after this duration
await db.KeyExpireAsync(streamKey, this._streamTtl);
}
/// <inheritdoc/>
public ValueTask OnAgentResponseAsync(AgentRunResponse message, CancellationToken cancellationToken)
{
// This handler is optimized for streaming responses.
// For non-streaming responses, we don't need to store in Redis since
// the response is returned directly to the caller.
return ValueTask.CompletedTask;
}
/// <summary>
/// Reads chunks from a Redis stream for the given session, yielding them as they become available.
/// </summary>
/// <param name="conversationId">The conversation ID to read from.</param>
/// <param name="cursor">Optional cursor to resume from. If null, reads from the beginning.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>An async enumerable of stream chunks.</returns>
public async IAsyncEnumerable<StreamChunk> ReadStreamAsync(
string conversationId,
string? cursor,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
string streamKey = GetStreamKey(conversationId);
IDatabase db = this._redis.GetDatabase();
string startId = string.IsNullOrEmpty(cursor) ? "0-0" : cursor;
int emptyReadCount = 0;
bool hasSeenData = false;
while (!cancellationToken.IsCancellationRequested)
{
StreamEntry[]? entries = null;
string? errorMessage = null;
try
{
entries = await db.StreamReadAsync(streamKey, startId, count: 100);
}
catch (Exception ex)
{
errorMessage = ex.Message;
}
if (errorMessage != null)
{
yield return new StreamChunk(startId, null, false, errorMessage);
yield break;
}
// entries is guaranteed to be non-null if errorMessage is null
if (entries!.Length == 0)
{
if (!hasSeenData)
{
emptyReadCount++;
if (emptyReadCount >= MaxEmptyReads)
{
yield return new StreamChunk(
startId,
null,
false,
$"Stream not found or timed out after {MaxEmptyReads * PollIntervalMs / 1000} seconds");
yield break;
}
}
await Task.Delay(PollIntervalMs, cancellationToken);
continue;
}
hasSeenData = true;
foreach (StreamEntry entry in entries)
{
startId = entry.Id.ToString();
string? text = entry["text"];
string? done = entry["done"];
if (done == "true")
{
yield return new StreamChunk(startId, null, true, null);
yield break;
}
if (!string.IsNullOrEmpty(text))
{
yield return new StreamChunk(startId, text, false, null);
}
}
}
}
/// <summary>
/// Gets the Redis Stream key for a given conversation ID.
/// </summary>
/// <param name="conversationId">The conversation ID.</param>
/// <returns>The Redis Stream key.</returns>
internal static string GetStreamKey(string conversationId) => $"agent-stream:{conversationId}";
}
@@ -0,0 +1,161 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ComponentModel;
namespace ReliableStreaming;
/// <summary>
/// Mock travel tools that return hardcoded data for demonstration purposes.
/// In a real application, these would call actual weather and events APIs.
/// </summary>
internal static class TravelTools
{
/// <summary>
/// Gets a weather forecast for a destination on a specific date.
/// Returns mock weather data for demonstration purposes.
/// </summary>
/// <param name="destination">The destination city or location.</param>
/// <param name="date">The date for the forecast (e.g., "2025-01-15" or "next Monday").</param>
/// <returns>A weather forecast summary.</returns>
[Description("Gets the weather forecast for a destination on a specific date. Use this to provide weather-aware recommendations in the itinerary.")]
public static string GetWeatherForecast(string destination, string date)
{
// Mock weather data based on destination for realistic responses
Dictionary<string, (string condition, int highF, int lowF)> weatherByRegion = new(StringComparer.OrdinalIgnoreCase)
{
["Tokyo"] = ("Partly cloudy with a chance of light rain", 58, 45),
["Paris"] = ("Overcast with occasional drizzle", 52, 41),
["New York"] = ("Clear and cold", 42, 28),
["London"] = ("Foggy morning, clearing in afternoon", 48, 38),
["Sydney"] = ("Sunny and warm", 82, 68),
["Rome"] = ("Sunny with light breeze", 62, 48),
["Barcelona"] = ("Partly sunny", 59, 47),
["Amsterdam"] = ("Cloudy with light rain", 46, 38),
["Dubai"] = ("Sunny and hot", 85, 72),
["Singapore"] = ("Tropical thunderstorms in afternoon", 88, 77),
["Bangkok"] = ("Hot and humid, afternoon showers", 91, 78),
["Los Angeles"] = ("Sunny and pleasant", 72, 55),
["San Francisco"] = ("Morning fog, afternoon sun", 62, 52),
["Seattle"] = ("Rainy with breaks", 48, 40),
["Miami"] = ("Warm and sunny", 78, 65),
["Honolulu"] = ("Tropical paradise weather", 82, 72),
};
// Find a matching destination or use a default
(string condition, int highF, int lowF) forecast = ("Partly cloudy", 65, 50);
foreach (KeyValuePair<string, (string, int, int)> entry in weatherByRegion)
{
if (destination.Contains(entry.Key, StringComparison.OrdinalIgnoreCase))
{
forecast = entry.Value;
break;
}
}
return $"""
Weather forecast for {destination} on {date}:
Conditions: {forecast.condition}
High: {forecast.highF}°F ({(forecast.highF - 32) * 5 / 9}°C)
Low: {forecast.lowF}°F ({(forecast.lowF - 32) * 5 / 9}°C)
Recommendation: {GetWeatherRecommendation(forecast.condition)}
""";
}
/// <summary>
/// Gets local events happening at a destination around a specific date.
/// Returns mock event data for demonstration purposes.
/// </summary>
/// <param name="destination">The destination city or location.</param>
/// <param name="date">The date to search for events (e.g., "2025-01-15" or "next week").</param>
/// <returns>A list of local events and activities.</returns>
[Description("Gets local events and activities happening at a destination around a specific date. Use this to suggest timely activities and experiences.")]
public static string GetLocalEvents(string destination, string date)
{
// Mock events data based on destination
Dictionary<string, string[]> eventsByCity = new(StringComparer.OrdinalIgnoreCase)
{
["Tokyo"] = [
"🎭 Kabuki Theater Performance at Kabukiza Theatre - Traditional Japanese drama",
"🌸 Winter Illuminations at Yoyogi Park - Spectacular light displays",
"🍜 Ramen Festival at Tokyo Station - Sample ramen from across Japan",
"🎮 Gaming Expo at Tokyo Big Sight - Latest video games and technology",
],
["Paris"] = [
"🎨 Impressionist Exhibition at Musée d'Orsay - Extended evening hours",
"🍷 Wine Tasting Tour in Le Marais - Local sommelier guided",
"🎵 Jazz Night at Le Caveau de la Huchette - Historic jazz club",
"🥐 French Pastry Workshop - Learn from master pâtissiers",
],
["New York"] = [
"🎭 Broadway Show: Hamilton - Limited engagement performances",
"🏀 Knicks vs Lakers at Madison Square Garden",
"🎨 Modern Art Exhibit at MoMA - New installations",
"🍕 Pizza Walking Tour of Brooklyn - Artisan pizzerias",
],
["London"] = [
"👑 Royal Collection Exhibition at Buckingham Palace",
"🎭 West End Musical: The Phantom of the Opera",
"🍺 Craft Beer Festival at Brick Lane",
"🎪 Winter Wonderland at Hyde Park - Rides and markets",
],
["Sydney"] = [
"🏄 Pro Surfing Competition at Bondi Beach",
"🎵 Opera at Sydney Opera House - La Bohème",
"🦘 Wildlife Night Safari at Taronga Zoo",
"🍽️ Harbor Dinner Cruise with fireworks",
],
["Rome"] = [
"🏛️ After-Hours Vatican Tour - Skip the crowds",
"🍝 Pasta Making Class in Trastevere",
"🎵 Classical Concert at Borghese Gallery",
"🍷 Wine Tasting in Roman Cellars",
],
};
// Find events for the destination or use generic events
string[] events = [
"🎭 Local theater performance",
"🍽️ Food and wine festival",
"🎨 Art gallery opening",
"🎵 Live music at local venues",
];
foreach (KeyValuePair<string, string[]> entry in eventsByCity)
{
if (destination.Contains(entry.Key, StringComparison.OrdinalIgnoreCase))
{
events = entry.Value;
break;
}
}
string eventList = string.Join("\n• ", events);
return $"""
Local events in {destination} around {date}:
• {eventList}
💡 Tip: Book popular events in advance as they may sell out quickly!
""";
}
private static string GetWeatherRecommendation(string condition)
{
// Use case-insensitive comparison instead of ToLowerInvariant() to satisfy CA1308
return condition switch
{
string c when c.Contains("rain", StringComparison.OrdinalIgnoreCase) || c.Contains("drizzle", StringComparison.OrdinalIgnoreCase) =>
"Bring an umbrella and waterproof jacket. Consider indoor activities for backup.",
string c when c.Contains("fog", StringComparison.OrdinalIgnoreCase) =>
"Morning visibility may be limited. Plan outdoor sightseeing for afternoon.",
string c when c.Contains("cold", StringComparison.OrdinalIgnoreCase) =>
"Layer up with warm clothing. Hot drinks and cozy cafés recommended.",
string c when c.Contains("hot", StringComparison.OrdinalIgnoreCase) || c.Contains("warm", StringComparison.OrdinalIgnoreCase) =>
"Stay hydrated and use sunscreen. Plan strenuous activities for cooler morning hours.",
string c when c.Contains("thunder", StringComparison.OrdinalIgnoreCase) || c.Contains("storm", StringComparison.OrdinalIgnoreCase) =>
"Keep an eye on weather updates. Have indoor alternatives ready.",
_ => "Pleasant conditions expected. Great day for outdoor exploration!"
};
}
}
@@ -0,0 +1,21 @@
{
"version": "2.0",
"logging": {
"logLevel": {
"Microsoft.Agents.AI.DurableTask": "Information",
"Microsoft.Agents.AI.Hosting.AzureFunctions": "Information",
"DurableTask": "Information",
"Microsoft.DurableTask": "Information",
"ReliableStreaming": "Information"
}
},
"extensions": {
"durableTask": {
"hubName": "default",
"storageProvider": {
"type": "AzureManaged",
"connectionStringName": "DURABLE_TASK_SCHEDULER_CONNECTION_STRING"
}
}
}
}
@@ -0,0 +1,12 @@
{
"IsEncrypted": false,
"Values": {
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None",
"AZURE_OPENAI_ENDPOINT": "<AZURE_OPENAI_ENDPOINT>",
"AZURE_OPENAI_DEPLOYMENT": "<AZURE_OPENAI_DEPLOYMENT>",
"REDIS_CONNECTION_STRING": "localhost:6379",
"REDIS_STREAM_TTL_MINUTES": "10"
}
}
+1
View File
@@ -9,6 +9,7 @@ This directory contains samples for Azure Functions.
- **[05_AgentOrchestration_HITL](05_AgentOrchestration_HITL)**: A sample that demonstrates how to implement a human-in-the-loop workflow using durable orchestration, including external event handling for human approval.
- **[06_LongRunningTools](06_LongRunningTools)**: A sample that demonstrates how agents can start and interact with durable orchestrations from tool calls to enable long-running tool scenarios.
- **[07_AgentAsMcpTool](07_AgentAsMcpTool)**: A sample that demonstrates how to configure durable AI agents to be accessible as Model Context Protocol (MCP) tools.
- **[08_ReliableStreaming](08_ReliableStreaming)**: A sample that demonstrates how to implement reliable streaming for durable agents using Redis Streams, enabling clients to disconnect and reconnect without losing messages.
## Running the Samples
@@ -22,17 +22,17 @@ internal sealed class ServerFunctionApprovalClientAgent : DelegatingAIAgent
this._jsonSerializerOptions = jsonSerializerOptions;
}
public override Task<AgentRunResponse> RunAsync(
protected override Task<AgentRunResponse> RunCoreAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default)
{
return this.RunStreamingAsync(messages, thread, options, cancellationToken)
return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken)
.ToAgentRunResponseAsync(cancellationToken);
}
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
@@ -22,17 +22,17 @@ internal sealed class ServerFunctionApprovalAgent : DelegatingAIAgent
this._jsonSerializerOptions = jsonSerializerOptions;
}
public override Task<AgentRunResponse> RunAsync(
protected override Task<AgentRunResponse> RunCoreAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default)
{
return this.RunStreamingAsync(messages, thread, options, cancellationToken)
return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken)
.ToAgentRunResponseAsync(cancellationToken);
}
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
@@ -35,18 +35,18 @@ internal sealed class StatefulAgent<TState> : DelegatingAIAgent
}
/// <inheritdoc />
public override Task<AgentRunResponse> RunAsync(
protected override Task<AgentRunResponse> RunCoreAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default)
{
return this.RunStreamingAsync(messages, thread, options, cancellationToken)
return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken)
.ToAgentRunResponseAsync(cancellationToken);
}
/// <inheritdoc />
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
@@ -17,17 +17,17 @@ internal sealed class SharedStateAgent : DelegatingAIAgent
this._jsonSerializerOptions = jsonSerializerOptions;
}
public override Task<AgentRunResponse> RunAsync(
protected override Task<AgentRunResponse> RunCoreAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default)
{
return this.RunStreamingAsync(messages, thread, options, cancellationToken)
return this.RunCoreStreamingAsync(messages, thread, options, cancellationToken)
.ToAgentRunResponseAsync(cancellationToken);
}
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
@@ -13,7 +13,7 @@ var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT
AIAgent agent = new AzureOpenAIClient(
new Uri(endpoint),
new AzureCliCredential())
.GetOpenAIResponseClient(deploymentName)
.GetResponsesClient(deploymentName)
.CreateAIAgent(instructions: "You are good at telling jokes.", name: "Joker");
// Invoke the agent and output the text result.
@@ -34,7 +34,7 @@ namespace SampleApp
public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null)
=> new CustomAgentThread(serializedThread, jsonSerializerOptions);
public override async Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
protected override async Task<AgentRunResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
// Create a thread if the user didn't supply one.
thread ??= this.GetNewThread();
@@ -44,11 +44,19 @@ namespace SampleApp
throw new ArgumentException($"The provided thread is not of type {nameof(CustomAgentThread)}.", nameof(thread));
}
// Get existing messages from the store
var invokingContext = new ChatMessageStore.InvokingContext(messages);
var storeMessages = await typedThread.MessageStore.InvokingAsync(invokingContext, cancellationToken);
// Clone the input messages and turn them into response messages with upper case text.
List<ChatMessage> responseMessages = CloneAndToUpperCase(messages, this.DisplayName).ToList();
List<ChatMessage> responseMessages = CloneAndToUpperCase(messages, this.Name).ToList();
// Notify the thread of the input and output messages.
await typedThread.MessageStore.AddMessagesAsync(messages.Concat(responseMessages), cancellationToken);
var invokedContext = new ChatMessageStore.InvokedContext(messages, storeMessages)
{
ResponseMessages = responseMessages
};
await typedThread.MessageStore.InvokedAsync(invokedContext, cancellationToken);
return new AgentRunResponse
{
@@ -58,7 +66,7 @@ namespace SampleApp
};
}
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
// Create a thread if the user didn't supply one.
thread ??= this.GetNewThread();
@@ -68,18 +76,26 @@ namespace SampleApp
throw new ArgumentException($"The provided thread is not of type {nameof(CustomAgentThread)}.", nameof(thread));
}
// Get existing messages from the store
var invokingContext = new ChatMessageStore.InvokingContext(messages);
var storeMessages = await typedThread.MessageStore.InvokingAsync(invokingContext, cancellationToken);
// Clone the input messages and turn them into response messages with upper case text.
List<ChatMessage> responseMessages = CloneAndToUpperCase(messages, this.DisplayName).ToList();
List<ChatMessage> responseMessages = CloneAndToUpperCase(messages, this.Name).ToList();
// Notify the thread of the input and output messages.
await typedThread.MessageStore.AddMessagesAsync(messages.Concat(responseMessages), cancellationToken);
var invokedContext = new ChatMessageStore.InvokedContext(messages, storeMessages)
{
ResponseMessages = responseMessages
};
await typedThread.MessageStore.InvokedAsync(invokedContext, cancellationToken);
foreach (var message in responseMessages)
{
yield return new AgentRunResponseUpdate
{
AgentId = this.Id,
AuthorName = this.DisplayName,
AuthorName = message.AuthorName,
Role = ChatRole.Assistant,
Contents = message.Contents,
ResponseId = Guid.NewGuid().ToString("N"),
@@ -88,7 +104,7 @@ namespace SampleApp
}
}
private static IEnumerable<ChatMessage> CloneAndToUpperCase(IEnumerable<ChatMessage> messages, string agentName) => messages.Select(x =>
private static IEnumerable<ChatMessage> CloneAndToUpperCase(IEnumerable<ChatMessage> messages, string? agentName) => messages.Select(x =>
{
// Clone the message and update its author to be the agent.
var messageClone = x.Clone();
@@ -11,7 +11,7 @@ var model = Environment.GetEnvironmentVariable("OPENAI_MODEL") ?? "gpt-4o-mini";
AIAgent agent = new OpenAIClient(
apiKey)
.GetOpenAIResponseClient(model)
.GetResponsesClient(model)
.CreateAIAgent(instructions: "You are good at telling jokes.", name: "Joker");
// Invoke the agent and output the text result.
@@ -11,11 +11,11 @@ var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new I
var model = Environment.GetEnvironmentVariable("OPENAI_MODEL") ?? "gpt-5";
var client = new OpenAIClient(apiKey)
.GetOpenAIResponseClient(model)
.GetResponsesClient(model)
.AsIChatClient().AsBuilder()
.ConfigureOptions(o =>
{
o.RawRepresentationFactory = _ => new ResponseCreationOptions()
o.RawRepresentationFactory = _ => new CreateResponseOptions()
{
ReasoningOptions = new()
{
@@ -87,10 +87,10 @@ public class OpenAIChatClientAgent : DelegatingAIAgent
}
/// <inheritdoc/>
public sealed override Task<AgentRunResponse> RunAsync(IEnumerable<Microsoft.Extensions.AI.ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) =>
base.RunAsync(messages, thread, options, cancellationToken);
protected sealed override Task<AgentRunResponse> RunCoreAsync(IEnumerable<Microsoft.Extensions.AI.ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) =>
base.RunCoreAsync(messages, thread, options, cancellationToken);
/// <inheritdoc/>
public override IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(IEnumerable<Microsoft.Extensions.AI.ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) =>
base.RunStreamingAsync(messages, thread, options, cancellationToken);
protected override IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(IEnumerable<Microsoft.Extensions.AI.ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) =>
base.RunCoreStreamingAsync(messages, thread, options, cancellationToken);
}
@@ -16,13 +16,13 @@ public class OpenAIResponseClientAgent : DelegatingAIAgent
/// <summary>
/// Initialize an instance of <see cref="OpenAIResponseClientAgent"/>.
/// </summary>
/// <param name="client">Instance of <see cref="OpenAIResponseClient"/></param>
/// <param name="client">Instance of <see cref="ResponsesClient"/></param>
/// <param name="instructions">Optional instructions for the agent.</param>
/// <param name="name">Optional name for the agent.</param>
/// <param name="description">Optional description for the agent.</param>
/// <param name="loggerFactory">Optional instance of <see cref="ILoggerFactory"/></param>
public OpenAIResponseClientAgent(
OpenAIResponseClient client,
ResponsesClient client,
string? instructions = null,
string? name = null,
string? description = null,
@@ -39,11 +39,11 @@ public class OpenAIResponseClientAgent : DelegatingAIAgent
/// <summary>
/// Initialize an instance of <see cref="OpenAIResponseClientAgent"/>.
/// </summary>
/// <param name="client">Instance of <see cref="OpenAIResponseClient"/></param>
/// <param name="client">Instance of <see cref="ResponsesClient"/></param>
/// <param name="options">Options to create the agent.</param>
/// <param name="loggerFactory">Optional instance of <see cref="ILoggerFactory"/></param>
public OpenAIResponseClientAgent(
OpenAIResponseClient client, ChatClientAgentOptions options, ILoggerFactory? loggerFactory = null) :
ResponsesClient client, ChatClientAgentOptions options, ILoggerFactory? loggerFactory = null) :
base(new ChatClientAgent((client ?? throw new ArgumentNullException(nameof(client))).AsIChatClient(), options, loggerFactory))
{
}
@@ -55,8 +55,8 @@ public class OpenAIResponseClientAgent : DelegatingAIAgent
/// <param name="thread">The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided messages and agent response.</param>
/// <param name="options">Optional parameters for agent invocation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="OpenAIResponse"/> containing the list of <see cref="ChatMessage"/> items.</returns>
public virtual async Task<OpenAIResponse> RunAsync(
/// <returns>A <see cref="ResponseResult"/> containing the list of <see cref="ChatMessage"/> items.</returns>
public virtual async Task<ResponseResult> RunAsync(
IEnumerable<ResponseItem> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
@@ -74,7 +74,7 @@ public class OpenAIResponseClientAgent : DelegatingAIAgent
/// <param name="thread">The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided messages and agent response.</param>
/// <param name="options">Optional parameters for agent invocation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="OpenAIResponse"/> containing the list of <see cref="ChatMessage"/> items.</returns>
/// <returns>A <see cref="ResponseResult"/> containing the list of <see cref="ChatMessage"/> items.</returns>
public virtual async IAsyncEnumerable<StreamingResponseUpdate> RunStreamingAsync(
IEnumerable<ResponseItem> messages,
AgentThread? thread = null,
@@ -105,10 +105,10 @@ public class OpenAIResponseClientAgent : DelegatingAIAgent
}
/// <inheritdoc/>
public sealed override Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) =>
base.RunAsync(messages, thread, options, cancellationToken);
protected sealed override Task<AgentRunResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) =>
base.RunCoreAsync(messages, thread, options, cancellationToken);
/// <inheritdoc/>
public sealed override IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) =>
base.RunStreamingAsync(messages, thread, options, cancellationToken);
protected sealed override IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) =>
base.RunCoreStreamingAsync(messages, thread, options, cancellationToken);
}
@@ -1,6 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample demonstrates how to create OpenAIResponseClientAgent directly from an OpenAIResponseClient instance.
// This sample demonstrates how to create OpenAIResponseClientAgent directly from an ResponsesClient instance.
using OpenAI;
using OpenAI.Responses;
@@ -9,16 +9,16 @@ using OpenAIResponseClientSample;
var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new InvalidOperationException("OPENAI_API_KEY is not set.");
var model = Environment.GetEnvironmentVariable("OPENAI_MODEL") ?? "gpt-4o-mini";
// Create an OpenAIResponseClient directly from OpenAIClient
OpenAIResponseClient responseClient = new OpenAIClient(apiKey).GetOpenAIResponseClient(model);
// Create a ResponsesClient directly from OpenAIClient
ResponsesClient responseClient = new OpenAIClient(apiKey).GetResponsesClient(model);
// Create an agent directly from the OpenAIResponseClient using OpenAIResponseClientAgent
// Create an agent directly from the ResponsesClient using OpenAIResponseClientAgent
OpenAIResponseClientAgent agent = new(responseClient, instructions: "You are good at telling jokes.", name: "Joker");
ResponseItem userMessage = ResponseItem.CreateUserMessageItem("Tell me a joke about a pirate.");
// Invoke the agent and output the text result.
OpenAIResponse response = await agent.RunAsync([userMessage]);
ResponseResult response = await agent.RunAsync([userMessage]);
Console.WriteLine(response.GetOutputText());
// Invoke the agent with streaming support.
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,98 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample demonstrates how to maintain conversation state using the OpenAIResponseClientAgent
// and AgentThread. By passing the same thread to multiple agent invocations, the agent
// automatically maintains the conversation history, allowing the AI model to understand
// context from previous exchanges.
using System.ClientModel;
using System.ClientModel.Primitives;
using System.Text.Json;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using OpenAI;
using OpenAI.Chat;
using OpenAI.Conversations;
string apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new InvalidOperationException("OPENAI_API_KEY is not set.");
string model = Environment.GetEnvironmentVariable("OPENAI_MODEL") ?? "gpt-4o-mini";
// Create a ConversationClient directly from OpenAIClient
OpenAIClient openAIClient = new(apiKey);
ConversationClient conversationClient = openAIClient.GetConversationClient();
// Create an agent directly from the ResponsesClient using OpenAIResponseClientAgent
ChatClientAgent agent = new(openAIClient.GetResponsesClient(model).AsIChatClient(), instructions: "You are a helpful assistant.", name: "ConversationAgent");
ClientResult createConversationResult = await conversationClient.CreateConversationAsync(BinaryContent.Create(BinaryData.FromString("{}")));
using JsonDocument createConversationResultAsJson = JsonDocument.Parse(createConversationResult.GetRawResponse().Content.ToString());
string conversationId = createConversationResultAsJson.RootElement.GetProperty("id"u8)!.GetString()!;
// Create a thread for the conversation - this enables conversation state management for subsequent turns
AgentThread thread = agent.GetNewThread(conversationId);
Console.WriteLine("=== Multi-turn Conversation Demo ===\n");
// First turn: Ask about a topic
Console.WriteLine("User: What is the capital of France?");
UserChatMessage firstMessage = new("What is the capital of France?");
// After this call, the conversation state associated in the options is stored in 'thread' and used in subsequent calls
ChatCompletion firstResponse = await agent.RunAsync([firstMessage], thread);
Console.WriteLine($"Assistant: {firstResponse.Content.Last().Text}\n");
// Second turn: Follow-up question that relies on conversation context
Console.WriteLine("User: What famous landmarks are located there?");
UserChatMessage secondMessage = new("What famous landmarks are located there?");
ChatCompletion secondResponse = await agent.RunAsync([secondMessage], thread);
Console.WriteLine($"Assistant: {secondResponse.Content.Last().Text}\n");
// Third turn: Another follow-up that demonstrates context continuity
Console.WriteLine("User: How tall is the most famous one?");
UserChatMessage thirdMessage = new("How tall is the most famous one?");
ChatCompletion thirdResponse = await agent.RunAsync([thirdMessage], thread);
Console.WriteLine($"Assistant: {thirdResponse.Content.Last().Text}\n");
Console.WriteLine("=== End of Conversation ===");
// Show full conversation history
Console.WriteLine("Full Conversation History:");
ClientResult getConversationResult = await conversationClient.GetConversationAsync(conversationId);
Console.WriteLine("Conversation created.");
Console.WriteLine($" Conversation ID: {conversationId}");
Console.WriteLine();
CollectionResult getConversationItemsResults = conversationClient.GetConversationItems(conversationId);
foreach (ClientResult result in getConversationItemsResults.GetRawPages())
{
Console.WriteLine("Message contents retrieved. Order is most recent first by default.");
using JsonDocument getConversationItemsResultAsJson = JsonDocument.Parse(result.GetRawResponse().Content.ToString());
foreach (JsonElement element in getConversationItemsResultAsJson.RootElement.GetProperty("data").EnumerateArray())
{
string messageId = element.GetProperty("id"u8).ToString();
string messageRole = element.GetProperty("role"u8).ToString();
Console.WriteLine($" Message ID: {messageId}");
Console.WriteLine($" Message Role: {messageRole}");
foreach (var content in element.GetProperty("content").EnumerateArray())
{
string messageContentText = content.GetProperty("text"u8).ToString();
Console.WriteLine($" Message Text: {messageContentText}");
}
Console.WriteLine();
}
}
ClientResult deleteConversationResult = conversationClient.DeleteConversation(conversationId);
using JsonDocument deleteConversationResultAsJson = JsonDocument.Parse(deleteConversationResult.GetRawResponse().Content.ToString());
bool deleted = deleteConversationResultAsJson.RootElement
.GetProperty("deleted"u8)
.GetBoolean();
Console.WriteLine("Conversation deleted.");
Console.WriteLine($" Deleted: {deleted}");
Console.WriteLine();
@@ -0,0 +1,90 @@
# Managing Conversation State with OpenAI
This sample demonstrates how to maintain conversation state across multiple turns using the Agent Framework with OpenAI's Conversation API.
## What This Sample Shows
- **Conversation State Management**: Shows how to use `ConversationClient` and `AgentThread` to maintain conversation context across multiple agent invocations
- **Multi-turn Conversations**: Demonstrates follow-up questions that rely on context from previous messages in the conversation
- **Server-Side Storage**: Uses OpenAI's Conversation API to manage conversation history server-side, allowing the model to access previous messages without resending them
- **Conversation Lifecycle**: Demonstrates creating, retrieving, and deleting conversations
## Key Concepts
### ConversationClient for Server-Side Storage
The `ConversationClient` manages conversations on OpenAI's servers:
```csharp
// Create a ConversationClient from OpenAIClient
OpenAIClient openAIClient = new(apiKey);
ConversationClient conversationClient = openAIClient.GetConversationClient();
// Create a new conversation
ClientResult createConversationResult = await conversationClient.CreateConversationAsync(BinaryContent.Create(BinaryData.FromString("{}")));
```
### AgentThread for Conversation State
The `AgentThread` works with `ChatClientAgentRunOptions` to link the agent to a server-side conversation:
```csharp
// Set up agent run options with the conversation ID
ChatClientAgentRunOptions agentRunOptions = new() { ChatOptions = new ChatOptions() { ConversationId = conversationId } };
// Create a thread for the conversation
AgentThread thread = agent.GetNewThread();
// First call links the thread to the conversation
ChatCompletion firstResponse = await agent.RunAsync([firstMessage], thread, agentRunOptions);
// Subsequent calls use the thread without needing to pass options again
ChatCompletion secondResponse = await agent.RunAsync([secondMessage], thread);
```
### Retrieving Conversation History
You can retrieve the full conversation history from the server:
```csharp
CollectionResult getConversationItemsResults = conversationClient.GetConversationItems(conversationId);
foreach (ClientResult result in getConversationItemsResults.GetRawPages())
{
// Process conversation items
}
```
### How It Works
1. **Create an OpenAI Client**: Initialize an `OpenAIClient` with your API key
2. **Create a Conversation**: Use `ConversationClient` to create a server-side conversation
3. **Create an Agent**: Initialize an `OpenAIResponseClientAgent` with the desired model and instructions
4. **Create a Thread**: Call `agent.GetNewThread()` to create a new conversation thread
5. **Link Thread to Conversation**: Pass `ChatClientAgentRunOptions` with the `ConversationId` on the first call
6. **Send Messages**: Subsequent calls to `agent.RunAsync()` only need the thread - context is maintained
7. **Cleanup**: Delete the conversation when done using `conversationClient.DeleteConversation()`
## Running the Sample
1. Set the required environment variables:
```powershell
$env:OPENAI_API_KEY = "your_api_key_here"
$env:OPENAI_MODEL = "gpt-4o-mini"
```
2. Run the sample:
```powershell
dotnet run
```
## Expected Output
The sample demonstrates a three-turn conversation where each follow-up question relies on context from previous messages:
1. First question asks about the capital of France
2. Second question asks about landmarks "there" - requiring understanding of the previous answer
3. Third question asks about "the most famous one" - requiring context from both previous turns
After the conversation, the sample retrieves and displays the full conversation history from the server, then cleans up by deleting the conversation.
This demonstrates that the conversation state is properly maintained across multiple agent invocations using OpenAI's server-side conversation storage.
@@ -13,4 +13,5 @@ Agent Framework provides additional support to allow OpenAI developers to use th
|[Creating an AIAgent](./Agent_OpenAI_Step01_Running/)|This sample demonstrates how to create and run a basic agent with native OpenAI SDK types. Shows both regular and streaming invocation of the agent.|
|[Using Reasoning Capabilities](./Agent_OpenAI_Step02_Reasoning/)|This sample demonstrates how to create an AI agent with reasoning capabilities using OpenAI's reasoning models and response types.|
|[Creating an Agent from a ChatClient](./Agent_OpenAI_Step03_CreateFromChatClient/)|This sample demonstrates how to create an AI agent directly from an OpenAI.Chat.ChatClient instance using OpenAIChatClientAgent.|
|[Creating an Agent from an OpenAIResponseClient](./Agent_OpenAI_Step04_CreateFromOpenAIResponseClient/)|This sample demonstrates how to create an AI agent directly from an OpenAI.Responses.OpenAIResponseClient instance using OpenAIResponseClientAgent.|
|[Creating an Agent from an OpenAIResponseClient](./Agent_OpenAI_Step04_CreateFromOpenAIResponseClient/)|This sample demonstrates how to create an AI agent directly from an OpenAI.Responses.OpenAIResponseClient instance using OpenAIResponseClientAgent.|
|[Managing Conversation State](./Agent_OpenAI_Step05_Conversation/)|This sample demonstrates how to maintain conversation state across multiple turns using the AgentThread for context continuity.|
@@ -62,7 +62,12 @@ AIAgent agent = azureOpenAIClient
.CreateAIAgent(new ChatClientAgentOptions
{
ChatOptions = new() { Instructions = "You are a helpful support specialist for Contoso Outdoors. Answer questions using the provided context and cite the source document when available." },
AIContextProviderFactory = ctx => new TextSearchProvider(SearchAdapter, ctx.SerializedState, ctx.JsonSerializerOptions, textSearchOptions)
AIContextProviderFactory = ctx => new TextSearchProvider(SearchAdapter, ctx.SerializedState, ctx.JsonSerializerOptions, textSearchOptions),
// Since we are using ChatCompletion which stores chat history locally, we can also add a message removal policy
// that removes messages produced by the TextSearchProvider before they are added to the chat history, so that
// we don't bloat chat history with all the search result messages.
ChatMessageStoreFactory = ctx => new InMemoryChatMessageStore(ctx.SerializedState, ctx.JsonSerializerOptions)
.WithAIContextProviderMessageRemoval(),
});
AgentThread thread = agent.GetNewThread();
@@ -89,24 +89,7 @@ namespace SampleApp
public string? ThreadDbKey { get; private set; }
public override async Task AddMessagesAsync(IEnumerable<ChatMessage> messages, CancellationToken cancellationToken = default)
{
this.ThreadDbKey ??= Guid.NewGuid().ToString("N");
var collection = this._vectorStore.GetCollection<string, ChatHistoryItem>("ChatHistory");
await collection.EnsureCollectionExistsAsync(cancellationToken);
await collection.UpsertAsync(messages.Select(x => new ChatHistoryItem()
{
Key = this.ThreadDbKey + x.MessageId,
Timestamp = DateTimeOffset.UtcNow,
ThreadId = this.ThreadDbKey,
SerializedMessage = JsonSerializer.Serialize(x),
MessageText = x.Text
}), cancellationToken);
}
public override async Task<IEnumerable<ChatMessage>> GetMessagesAsync(CancellationToken cancellationToken = default)
public override async ValueTask<IEnumerable<ChatMessage>> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
var collection = this._vectorStore.GetCollection<string, ChatHistoryItem>("ChatHistory");
await collection.EnsureCollectionExistsAsync(cancellationToken);
@@ -124,6 +107,33 @@ namespace SampleApp
return messages;
}
public override async ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default)
{
// Don't store messages if the request failed.
if (context.InvokeException is not null)
{
return;
}
this.ThreadDbKey ??= Guid.NewGuid().ToString("N");
var collection = this._vectorStore.GetCollection<string, ChatHistoryItem>("ChatHistory");
await collection.EnsureCollectionExistsAsync(cancellationToken);
// Add both request and response messages to the store
// Optionally messages produced by the AIContextProvider can also be persisted (not shown).
var allNewMessages = context.RequestMessages.Concat(context.AIContextProviderMessages ?? []).Concat(context.ResponseMessages ?? []);
await collection.UpsertAsync(allNewMessages.Select(x => new ChatHistoryItem()
{
Key = this.ThreadDbKey + x.MessageId,
Timestamp = DateTimeOffset.UtcNow,
ThreadId = this.ThreadDbKey,
SerializedMessage = JsonSerializer.Serialize(x),
MessageText = x.Text
}), cancellationToken);
}
public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null) =>
// We have to serialize the thread id, so that on deserialization we can retrieve the messages using the same thread id.
JsonSerializer.SerializeToElement(this.ThreadDbKey);
@@ -22,7 +22,7 @@ var stateStore = new Dictionary<string, JsonElement?>();
AIAgent agent = new AzureOpenAIClient(
new Uri(endpoint),
new AzureCliCredential())
.GetOpenAIResponseClient(deploymentName)
.GetResponsesClient(deploymentName)
.CreateAIAgent(
name: "SpaceNovelWriter",
instructions: "You are a space novel writer. Always research relevant facts and generate character profiles for the main characters before writing novels." +
@@ -13,7 +13,7 @@ var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT
AIAgent agent = new AzureOpenAIClient(
new Uri(endpoint),
new AzureCliCredential())
.GetOpenAIResponseClient(deploymentName)
.GetResponsesClient(deploymentName)
.CreateAIAgent();
// Enable background responses (only supported by OpenAI Responses at this time).
@@ -73,7 +73,7 @@ internal sealed class Program
Dictionary<string, byte[]> screenshots = ComputerUseUtil.LoadScreenshotAssets();
ChatOptions chatOptions = new();
ResponseCreationOptions responseCreationOptions = new()
CreateResponseOptions responseCreationOptions = new()
{
TruncationMode = ResponseTruncationMode.Auto
};
@@ -30,7 +30,7 @@ var mcpTool = new HostedMcpServerTool(
AIAgent agent = new AzureOpenAIClient(
new Uri(endpoint),
new AzureCliCredential())
.GetOpenAIResponseClient(deploymentName)
.GetResponsesClient(deploymentName)
.CreateAIAgent(
instructions: "You answer questions by searching the Microsoft Learn content only.",
name: "MicrosoftLearnAgent",
@@ -57,7 +57,7 @@ var mcpToolWithApproval = new HostedMcpServerTool(
AIAgent agentWithRequiredApproval = new AzureOpenAIClient(
new Uri(endpoint),
new AzureCliCredential())
.GetOpenAIResponseClient(deploymentName)
.GetResponsesClient(deploymentName)
.CreateAIAgent(
instructions: "You answer questions by searching the Microsoft Learn content only.",
name: "MicrosoftLearnAgentWithApproval",
@@ -36,11 +36,11 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.AgentServer.AgentFramework" Version="1.0.0-beta.4" />
<PackageReference Include="Azure.AI.AgentServer.AgentFramework" Version="1.0.0-beta.5" />
<PackageReference Include="Azure.AI.OpenAI" Version="2.7.0-beta.2" />
<PackageReference Include="Azure.Identity" Version="1.17.0" />
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.0.0-preview.251125.1" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="10.1.0-preview.1.25608.1" />
<PackageReference Include="Azure.Identity" Version="1.17.1" />
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.0.0-preview.251219.1" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="10.1.1-preview.1.25612.2" />
</ItemGroup>
<!-- Add analyzers with compatible versions -->
@@ -9,7 +9,7 @@ using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using OpenAI;
using OpenAI.Responses;
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
@@ -25,7 +25,7 @@ AITool mcpTool = new HostedMcpServerTool(serverName: "microsoft_learn", serverAd
AIAgent agent = new AzureOpenAIClient(
new Uri(endpoint),
new DefaultAzureCredential())
.GetOpenAIResponseClient(deploymentName)
.GetResponsesClient(deploymentName)
.CreateAIAgent(
instructions: "You answer questions by searching the Microsoft Learn content only.",
name: "MicrosoftLearnAgent",
@@ -35,11 +35,11 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.AgentServer.AgentFramework" Version="1.0.0-beta.4" />
<PackageReference Include="Azure.AI.AgentServer.AgentFramework" Version="1.0.0-beta.5" />
<PackageReference Include="Azure.AI.OpenAI" Version="2.7.0-beta.2" />
<PackageReference Include="Azure.Identity" Version="1.17.0" />
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.0.0-preview.251125.1" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="10.1.0-preview.1.25608.1" />
<PackageReference Include="Azure.Identity" Version="1.17.1" />
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.0.0-preview.251219.1" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="10.1.1-preview.1.25612.2" />
</ItemGroup>
<!-- Add analyzers with compatible versions -->
@@ -8,9 +8,8 @@ using Azure.AI.AgentServer.AgentFramework.Extensions;
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Data;
using Microsoft.Extensions.AI;
using OpenAI;
using OpenAI.Chat;
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
@@ -28,7 +27,10 @@ AIAgent agent = new AzureOpenAIClient(
.GetChatClient(deploymentName)
.CreateAIAgent(new ChatClientAgentOptions
{
Instructions = "You are a helpful support specialist for Contoso Outdoors. Answer questions using the provided context and cite the source document when available.",
ChatOptions = new ChatOptions
{
Instructions = "You are a helpful support specialist for Contoso Outdoors. Answer questions using the provided context and cite the source document when available.",
},
AIContextProviderFactory = ctx => new TextSearchProvider(MockSearchAsync, ctx.SerializedState, ctx.JsonSerializerOptions, textSearchOptions)
});
@@ -35,10 +35,10 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.AgentServer.AgentFramework" Version="1.0.0-beta.4" />
<PackageReference Include="Azure.AI.AgentServer.AgentFramework" Version="1.0.0-beta.5" />
<PackageReference Include="Azure.AI.OpenAI" Version="2.7.0-beta.2" />
<PackageReference Include="Azure.Identity" Version="1.17.0" />
<PackageReference Include="Microsoft.Agents.AI.Workflows" Version="1.0.0-preview.251125.1" />
<PackageReference Include="Azure.Identity" Version="1.17.1" />
<PackageReference Include="Microsoft.Agents.AI.Workflows" Version="1.0.0-preview.251219.1" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="10.1.0-preview.1.25608.1" />
</ItemGroup>
@@ -48,9 +48,9 @@ public class WeatherForecastAgent : DelegatingAIAgent
{
}
public override async Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
protected override async Task<AgentRunResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
var response = await base.RunAsync(messages, thread, options, cancellationToken);
var response = await base.RunCoreAsync(messages, thread, options, cancellationToken);
// If the agent returned a valid structured output response
// we might be able to enhance the response with an adaptive card.
@@ -27,7 +27,7 @@ TokenCredential browserCredential = new InteractiveBrowserCredential(
using IChatClient client = new AzureOpenAIClient(
new Uri(endpoint),
new AzureCliCredential())
.GetOpenAIResponseClient(deploymentName)
.GetResponsesClient(deploymentName)
.AsIChatClient()
.AsBuilder()
.WithPurview(browserCredential, new PurviewSettings("Agent Framework Test App"))
+19 -17
View File
@@ -24,13 +24,12 @@ namespace Microsoft.Agents.AI.A2A;
/// Support for tasks will be added later as part of the long-running
/// executions work.
/// </remarks>
internal sealed class A2AAgent : AIAgent
public sealed class A2AAgent : AIAgent
{
private readonly A2AClient _a2aClient;
private readonly string? _id;
private readonly string? _name;
private readonly string? _description;
private readonly string? _displayName;
private readonly ILogger _logger;
/// <summary>
@@ -40,9 +39,8 @@ internal sealed class A2AAgent : AIAgent
/// <param name="id">The unique identifier for the agent.</param>
/// <param name="name">The the name of the agent.</param>
/// <param name="description">The description of the agent.</param>
/// <param name="displayName">The display name of the agent.</param>
/// <param name="loggerFactory">Optional logger factory to use for logging.</param>
public A2AAgent(A2AClient a2aClient, string? id = null, string? name = null, string? description = null, string? displayName = null, ILoggerFactory? loggerFactory = null)
public A2AAgent(A2AClient a2aClient, string? id = null, string? name = null, string? description = null, ILoggerFactory? loggerFactory = null)
{
_ = Throw.IfNull(a2aClient);
@@ -50,7 +48,6 @@ internal sealed class A2AAgent : AIAgent
this._id = id;
this._name = name;
this._description = description;
this._displayName = displayName;
this._logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger<A2AAgent>();
}
@@ -71,7 +68,7 @@ internal sealed class A2AAgent : AIAgent
=> new A2AAgentThread(serializedThread, jsonSerializerOptions);
/// <inheritdoc/>
public override async Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
protected override async Task<AgentRunResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
_ = Throw.IfNull(messages);
@@ -87,9 +84,13 @@ internal sealed class A2AAgent : AIAgent
}
else
{
var a2aMessage = CreateA2AMessage(typedThread, messages);
MessageSendParams sendParams = new()
{
Message = CreateA2AMessage(typedThread, messages),
Metadata = options?.AdditionalProperties?.ToA2AMetadata()
};
a2aResponse = await this._a2aClient.SendMessageAsync(new MessageSendParams { Message = a2aMessage }, cancellationToken).ConfigureAwait(false);
a2aResponse = await this._a2aClient.SendMessageAsync(sendParams, cancellationToken).ConfigureAwait(false);
}
this._logger.LogAgentChatClientInvokedAgent(nameof(RunAsync), this.Id, this.Name);
@@ -134,7 +135,7 @@ internal sealed class A2AAgent : AIAgent
}
/// <inheritdoc/>
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
_ = Throw.IfNull(messages);
@@ -157,9 +158,13 @@ internal sealed class A2AAgent : AIAgent
// a2aSseEvents = this._a2aClient.SubscribeToTaskAsync(token.TaskId, cancellationToken).ConfigureAwait(false);
}
var a2aMessage = CreateA2AMessage(typedThread, messages);
MessageSendParams sendParams = new()
{
Message = CreateA2AMessage(typedThread, messages),
Metadata = options?.AdditionalProperties?.ToA2AMetadata()
};
a2aSseEvents = this._a2aClient.SendMessageStreamingAsync(new MessageSendParams { Message = a2aMessage }, cancellationToken).ConfigureAwait(false);
a2aSseEvents = this._a2aClient.SendMessageStreamingAsync(sendParams, cancellationToken).ConfigureAwait(false);
this._logger.LogAgentChatClientInvokedAgent(nameof(RunStreamingAsync), this.Id, this.Name);
@@ -198,16 +203,13 @@ internal sealed class A2AAgent : AIAgent
}
/// <inheritdoc/>
public override string Id => this._id ?? base.Id;
protected override string? IdCore => this._id;
/// <inheritdoc/>
public override string? Name => this._name ?? base.Name;
public override string? Name => this._name;
/// <inheritdoc/>
public override string DisplayName => this._displayName ?? base.DisplayName;
/// <inheritdoc/>
public override string? Description => this._description ?? base.Description;
public override string? Description => this._description;
private A2AAgentThread GetA2AThread(AgentThread? thread, AgentRunOptions? options)
{
@@ -33,9 +33,8 @@ public static class A2AClientExtensions
/// <param name="id">The unique identifier for the agent.</param>
/// <param name="name">The the name of the agent.</param>
/// <param name="description">The description of the agent.</param>
/// <param name="displayName">The display name of the agent.</param>
/// <param name="loggerFactory">Optional logger factory for enabling logging within the agent.</param>
/// <returns>An <see cref="AIAgent"/> instance backed by the A2A agent.</returns>
public static AIAgent GetAIAgent(this A2AClient client, string? id = null, string? name = null, string? description = null, string? displayName = null, ILoggerFactory? loggerFactory = null) =>
new A2AAgent(client, id, name, description, displayName, loggerFactory);
public static AIAgent GetAIAgent(this A2AClient client, string? id = null, string? name = null, string? description = null, ILoggerFactory? loggerFactory = null) =>
new A2AAgent(client, id, name, description, loggerFactory);
}
@@ -14,6 +14,9 @@ internal static class A2AMetadataExtensions
/// <summary>
/// Converts a dictionary of metadata to an <see cref="AdditionalPropertiesDictionary"/>.
/// </summary>
/// <remarks>
/// This method can be replaced by the one from A2A SDK once it is public.
/// </remarks>
/// <param name="metadata">The metadata dictionary to convert.</param>
/// <returns>The converted <see cref="AdditionalPropertiesDictionary"/>, or null if the input is null or empty.</returns>
internal static AdditionalPropertiesDictionary? ToAdditionalProperties(this Dictionary<string, JsonElement>? metadata)
@@ -0,0 +1,44 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Text.Json;
using Microsoft.Agents.AI;
namespace Microsoft.Extensions.AI;
/// <summary>
/// Extension methods for AdditionalPropertiesDictionary.
/// </summary>
internal static class AdditionalPropertiesDictionaryExtensions
{
/// <summary>
/// Converts an <see cref="AdditionalPropertiesDictionary"/> to a dictionary of <see cref="JsonElement"/> values suitable for A2A metadata.
/// </summary>
/// <remarks>
/// This method can be replaced by the one from A2A SDK once it is available.
/// </remarks>
/// <param name="additionalProperties">The additional properties dictionary to convert, or <c>null</c>.</param>
/// <returns>A dictionary of JSON elements representing the metadata, or <c>null</c> if the input is null or empty.</returns>
internal static Dictionary<string, JsonElement>? ToA2AMetadata(this AdditionalPropertiesDictionary? additionalProperties)
{
if (additionalProperties is not { Count: > 0 })
{
return null;
}
var metadata = new Dictionary<string, JsonElement>();
foreach (var kvp in additionalProperties)
{
if (kvp.Value is JsonElement)
{
metadata[kvp.Key] = (JsonElement)kvp.Value!;
continue;
}
metadata[kvp.Key] = JsonSerializer.SerializeToElement(kvp.Value, A2AJsonUtilities.DefaultOptions.GetTypeInfo(typeof(object)));
}
return metadata;
}
}
@@ -22,9 +22,6 @@ namespace Microsoft.Agents.AI;
[DebuggerDisplay("{DisplayName,nq}")]
public abstract class AIAgent
{
/// <summary>Default ID of this agent instance.</summary>
private readonly string _id = Guid.NewGuid().ToString("N");
/// <summary>
/// Gets the unique identifier for this agent instance.
/// </summary>
@@ -37,7 +34,19 @@ public abstract class AIAgent
/// agent instances in multi-agent scenarios. They should remain stable for the lifetime
/// of the agent instance.
/// </remarks>
public virtual string Id => this._id;
public string Id { get => this.IdCore ?? field; } = Guid.NewGuid().ToString("N");
/// <summary>
/// Gets a custom identifier for the agent, which can be overridden by derived classes.
/// </summary>
/// <value>
/// A string representing the agent's identifier, or <see langword="null"/> if the default ID should be used.
/// </value>
/// <remarks>
/// Derived classes can override this property to provide a custom identifier.
/// When <see langword="null"/> is returned, the <see cref="Id"/> property will use the default randomly-generated identifier.
/// </remarks>
protected virtual string? IdCore => null;
/// <summary>
/// Gets the human-readable name of the agent.
@@ -51,18 +60,6 @@ public abstract class AIAgent
/// </remarks>
public virtual string? Name { get; }
/// <summary>
/// Gets a display-friendly name for the agent.
/// </summary>
/// <value>
/// The agent's <see cref="Name"/> if available, otherwise the <see cref="Id"/>.
/// </value>
/// <remarks>
/// This property provides a guaranteed non-null string suitable for display in user interfaces,
/// logs, or other contexts where a readable identifier is needed.
/// </remarks>
public virtual string DisplayName => this.Name ?? this.Id ?? this._id; // final fallback to _id in case Id override returns null
/// <summary>
/// Gets a description of the agent's purpose, capabilities, or behavior.
/// </summary>
@@ -221,6 +218,35 @@ public abstract class AIAgent
/// <returns>A task that represents the asynchronous operation. The task result contains an <see cref="AgentRunResponse"/> with the agent's output.</returns>
/// <remarks>
/// <para>
/// This method delegates to <see cref="RunCoreAsync"/> to perform the actual agent invocation. It handles collections of messages,
/// allowing for complex conversational scenarios including multi-turn interactions, function calls, and
/// context-rich conversations.
/// </para>
/// <para>
/// The messages are processed in the order provided and become part of the conversation history.
/// The agent's response will also be added to <paramref name="thread"/> if one is provided.
/// </para>
/// </remarks>
public Task<AgentRunResponse> RunAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default) =>
this.RunCoreAsync(messages, thread, options, cancellationToken);
/// <summary>
/// Core implementation of the agent invocation logic with a collection of chat messages.
/// </summary>
/// <param name="messages">The collection of messages to send to the agent for processing.</param>
/// <param name="thread">
/// The conversation thread to use for this invocation. If <see langword="null"/>, a new thread will be created.
/// The thread will be updated with the input messages and any response messages generated during invocation.
/// </param>
/// <param name="options">Optional configuration parameters for controlling the agent's invocation behavior.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains an <see cref="AgentRunResponse"/> with the agent's output.</returns>
/// <remarks>
/// <para>
/// This is the primary invocation method that implementations must override. It handles collections of messages,
/// allowing for complex conversational scenarios including multi-turn interactions, function calls, and
/// context-rich conversations.
@@ -230,7 +256,7 @@ public abstract class AIAgent
/// The agent's response will also be added to <paramref name="thread"/> if one is provided.
/// </para>
/// </remarks>
public abstract Task<AgentRunResponse> RunAsync(
protected abstract Task<AgentRunResponse> RunCoreAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
@@ -315,6 +341,34 @@ public abstract class AIAgent
/// <returns>An asynchronous enumerable of <see cref="AgentRunResponseUpdate"/> instances representing the streaming response.</returns>
/// <remarks>
/// <para>
/// This method delegates to <see cref="RunCoreStreamingAsync"/> to perform the actual streaming invocation. It provides real-time
/// updates as the agent processes the input and generates its response, enabling more responsive user experiences.
/// </para>
/// <para>
/// Each <see cref="AgentRunResponseUpdate"/> represents a portion of the complete response, allowing consumers
/// to display partial results, implement progressive loading, or provide immediate feedback to users.
/// </para>
/// </remarks>
public IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default) =>
this.RunCoreStreamingAsync(messages, thread, options, cancellationToken);
/// <summary>
/// Core implementation of the agent streaming invocation logic with a collection of chat messages.
/// </summary>
/// <param name="messages">The collection of messages to send to the agent for processing.</param>
/// <param name="thread">
/// The conversation thread to use for this invocation. If <see langword="null"/>, a new thread will be created.
/// The thread will be updated with the input messages and any response updates generated during invocation.
/// </param>
/// <param name="options">Optional configuration parameters for controlling the agent's invocation behavior.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>An asynchronous enumerable of <see cref="AgentRunResponseUpdate"/> instances representing the streaming response.</returns>
/// <remarks>
/// <para>
/// This is the primary streaming invocation method that implementations must override. It provides real-time
/// updates as the agent processes the input and generates its response, enabling more responsive user experiences.
/// </para>
@@ -323,7 +377,7 @@ public abstract class AIAgent
/// to display partial results, implement progressive loading, or provide immediate feedback to users.
/// </para>
/// </remarks>
public abstract IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
protected abstract IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
@@ -291,6 +291,15 @@ public class AgentRunResponse
return updates;
}
/// <summary>
/// Deserializes the response text into the given type.
/// </summary>
/// <typeparam name="T">The output type to deserialize into.</typeparam>
/// <returns>The result as the requested type.</returns>
/// <exception cref="InvalidOperationException">The result is not parsable into the requested type.</exception>
public T Deserialize<T>() =>
this.Deserialize<T>(AgentAbstractionsJsonUtilities.DefaultOptions);
/// <summary>
/// Deserializes the response text into the given type using the specified serializer options.
/// </summary>
@@ -311,6 +320,15 @@ public class AgentRunResponse
};
}
/// <summary>
/// Tries to deserialize response text into the given type.
/// </summary>
/// <typeparam name="T">The output type to deserialize into.</typeparam>
/// <param name="structuredOutput">The parsed structured output.</param>
/// <returns><see langword="true" /> if parsing was successful; otherwise, <see langword="false" />.</returns>
public bool TryDeserialize<T>([NotNullWhen(true)] out T? structuredOutput) =>
this.TryDeserialize(AgentAbstractionsJsonUtilities.DefaultOptions, out structuredOutput);
/// <summary>
/// Tries to deserialize response text into the given type using the specified serializer options.
/// </summary>
@@ -68,7 +68,7 @@ public abstract class AgentThread
/// <exception cref="ArgumentNullException"><paramref name="serviceType"/> is <see langword="null"/>.</exception>
/// <remarks>
/// The purpose of this method is to allow for the retrieval of strongly-typed services that might be provided by the <see cref="AgentThread"/>,
/// including itself or any services it might be wrapping. For example, to access the <see cref="AgentThreadMetadata"/> for the instance,
/// including itself or any services it might be wrapping. For example, to access a <see cref="ChatMessageStore"/> if available for the instance,
/// <see cref="GetService"/> may be used to request it.
/// </remarks>
public virtual object? GetService(Type serviceType, object? serviceKey = null)
@@ -1,29 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Diagnostics;
namespace Microsoft.Agents.AI;
/// <summary>
/// Provides metadata information about an <see cref="AgentThread"/> instance.
/// </summary>
[DebuggerDisplay("ConversationId = {ConversationId}")]
public class AgentThreadMetadata
{
/// <summary>
/// Initializes a new instance of the <see cref="AgentThreadMetadata"/> class.
/// </summary>
/// <param name="conversationId">The unique identifier for the conversation, if available.</param>
public AgentThreadMetadata(string? conversationId)
{
this.ConversationId = conversationId;
}
/// <summary>
/// Gets the unique identifier for the conversation, if available.
/// </summary>
/// <remarks>
/// The meaning of this ID may vary depending on the agent implementation.
/// </remarks>
public string? ConversationId { get; }
}
@@ -32,8 +32,9 @@ namespace Microsoft.Agents.AI;
public abstract class ChatMessageStore
{
/// <summary>
/// Asynchronously retrieves all messages from the store that should be provided as context for the next agent invocation.
/// Called at the start of agent invocation to retrieve all messages from the store that should be provided as context for the next agent invocation.
/// </summary>
/// <param name="context">Contains the request context including the caller provided messages that will be used by the agent for this invocation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>
/// A task that represents the asynchronous operation. The task result contains a collection of <see cref="ChatMessage"/>
@@ -59,20 +60,19 @@ public abstract class ChatMessageStore
/// and context management.
/// </para>
/// </remarks>
public abstract Task<IEnumerable<ChatMessage>> GetMessagesAsync(CancellationToken cancellationToken = default);
public abstract ValueTask<IEnumerable<ChatMessage>> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default);
/// <summary>
/// Asynchronously adds new messages to the store.
/// Called at the end of the agent invocation to add new messages to the store.
/// </summary>
/// <param name="messages">The collection of chat messages to add to the store.</param>
/// <param name="context">Contains the invocation context including request messages, response messages, and any exception that occurred.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A task that represents the asynchronous add operation.</returns>
/// <exception cref="ArgumentNullException"><paramref name="messages"/> is <see langword="null"/>.</exception>
/// <remarks>
/// <para>
/// Messages should be added in the order they were generated to maintain proper chronological sequence.
/// The store is responsible for preserving message ordering and ensuring that subsequent calls to
/// <see cref="GetMessagesAsync"/> return messages in the correct chronological order.
/// <see cref="InvokingAsync"/> return messages in the correct chronological order.
/// </para>
/// <para>
/// Implementations may perform additional processing during message addition, such as:
@@ -83,8 +83,12 @@ public abstract class ChatMessageStore
/// <item><description>Updating indices or search capabilities</description></item>
/// </list>
/// </para>
/// <para>
/// This method is called regardless of whether the invocation succeeded or failed.
/// To check if the invocation was successful, inspect the <see cref="InvokedContext.InvokeException"/> property.
/// </para>
/// </remarks>
public abstract Task AddMessagesAsync(IEnumerable<ChatMessage> messages, CancellationToken cancellationToken = default);
public abstract ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default);
/// <summary>
/// Serializes the current object's state to a <see cref="JsonElement"/> using the specified serialization options.
@@ -121,4 +125,100 @@ public abstract class ChatMessageStore
/// </remarks>
public TService? GetService<TService>(object? serviceKey = null)
=> this.GetService(typeof(TService), serviceKey) is TService service ? service : default;
/// <summary>
/// Contains the context information provided to <see cref="InvokingAsync(InvokingContext, CancellationToken)"/>.
/// </summary>
/// <remarks>
/// This class provides context about the invocation before the messages are retrieved from the store,
/// including the new messages that will be used. Stores can use this information to determine what
/// messages should be retrieved for the invocation.
/// </remarks>
public sealed class InvokingContext
{
/// <summary>
/// Initializes a new instance of the <see cref="InvokingContext"/> class with the specified request messages.
/// </summary>
/// <param name="requestMessages">The new messages to be used by the agent for this invocation.</param>
/// <exception cref="ArgumentNullException"><paramref name="requestMessages"/> is <see langword="null"/>.</exception>
public InvokingContext(IEnumerable<ChatMessage> requestMessages)
{
this.RequestMessages = requestMessages ?? throw new ArgumentNullException(nameof(requestMessages));
}
/// <summary>
/// Gets the caller provided messages that will be used by the agent for this invocation.
/// </summary>
/// <value>
/// A collection of <see cref="ChatMessage"/> instances representing new messages that were provided by the caller.
/// </value>
public IEnumerable<ChatMessage> RequestMessages { get; }
}
/// <summary>
/// Contains the context information provided to <see cref="InvokedAsync(InvokedContext, CancellationToken)"/>.
/// </summary>
/// <remarks>
/// This class provides context about a completed agent invocation, including both the
/// request messages that were used and the response messages that were generated. It also indicates
/// whether the invocation succeeded or failed.
/// </remarks>
public sealed class InvokedContext
{
/// <summary>
/// Initializes a new instance of the <see cref="InvokedContext"/> class with the specified request messages.
/// </summary>
/// <param name="requestMessages">The caller provided messages that were used by the agent for this invocation.</param>
/// <param name="chatMessageStoreMessages">The messages retrieved from the <see cref="ChatMessageStore"/> for this invocation.</param>
/// <exception cref="ArgumentNullException"><paramref name="requestMessages"/> is <see langword="null"/>.</exception>
public InvokedContext(IEnumerable<ChatMessage> requestMessages, IEnumerable<ChatMessage> chatMessageStoreMessages)
{
this.RequestMessages = Throw.IfNull(requestMessages);
this.ChatMessageStoreMessages = chatMessageStoreMessages;
}
/// <summary>
/// Gets the caller provided messages that were used by the agent for this invocation.
/// </summary>
/// <value>
/// A collection of <see cref="ChatMessage"/> instances representing new messages that were provided by the caller.
/// This does not include any <see cref="ChatMessageStore"/> supplied messages.
/// </value>
public IEnumerable<ChatMessage> RequestMessages { get; }
/// <summary>
/// Gets the messages retrieved from the <see cref="ChatMessageStore"/> for this invocation, if any.
/// </summary>
/// <value>
/// A collection of <see cref="ChatMessage"/> instances that were retrieved from the <see cref="ChatMessageStore"/>,
/// and were used by the agent as part of the invocation.
/// </value>
public IEnumerable<ChatMessage> ChatMessageStoreMessages { get; }
/// <summary>
/// Gets or sets the messages provided by the <see cref="AIContextProvider"/> for this invocation, if any.
/// </summary>
/// <value>
/// A collection of <see cref="ChatMessage"/> instances that were provided by the <see cref="AIContextProvider"/>,
/// and were used by the agent as part of the invocation.
/// </value>
public IEnumerable<ChatMessage>? AIContextProviderMessages { get; set; }
/// <summary>
/// Gets the collection of response messages generated during this invocation if the invocation succeeded.
/// </summary>
/// <value>
/// A collection of <see cref="ChatMessage"/> instances representing the response,
/// or <see langword="null"/> if the invocation failed or did not produce response messages.
/// </value>
public IEnumerable<ChatMessage>? ResponseMessages { get; set; }
/// <summary>
/// Gets the <see cref="Exception"/> that was thrown during the invocation, if the invocation failed.
/// </summary>
/// <value>
/// The exception that caused the invocation to fail, or <see langword="null"/> if the invocation succeeded.
/// </value>
public Exception? InvokeException { get; set; }
}
}
@@ -0,0 +1,50 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI;
/// <summary>
/// Contains extension methods for the <see cref="ChatMessageStore"/> class.
/// </summary>
public static class ChatMessageStoreExtensions
{
/// <summary>
/// Adds message filtering to an existing store, so that messages passed to the store and messages produced by the store
/// can be filtered, updated or replaced.
/// </summary>
/// <param name="store">The store to add the message filter to.</param>
/// <param name="invokingMessagesFilter">An optional filter function to apply to messages produced by the store. If null, no filter is applied at this
/// stage.</param>
/// <param name="invokedMessagesFilter">An optional filter function to apply to the invoked context messages before they are passed to the store. If null, no
/// filter is applied at this stage.</param>
/// <returns>The <see cref="ChatMessageStore"/> with filtering applied.</returns>
public static ChatMessageStore WithMessageFilters(
this ChatMessageStore store,
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? invokingMessagesFilter = null,
Func<ChatMessageStore.InvokedContext, ChatMessageStore.InvokedContext>? invokedMessagesFilter = null)
{
return new ChatMessageStoreMessageFilter(
innerChatMessageStore: store,
invokingMessagesFilter: invokingMessagesFilter,
invokedMessagesFilter: invokedMessagesFilter);
}
/// <summary>
/// Decorates the provided chat message store so that it does not store messages produced by any <see cref="AIContextProvider"/>.
/// </summary>
/// <param name="store">The store to add the message filter to.</param>
/// <returns>A new <see cref="ChatMessageStore"/> instance that filters out <see cref="AIContextProvider"/> messages so they do not get stored.</returns>
public static ChatMessageStore WithAIContextProviderMessageRemoval(this ChatMessageStore store)
{
return new ChatMessageStoreMessageFilter(
innerChatMessageStore: store,
invokedMessagesFilter: (ctx) =>
{
ctx.AIContextProviderMessages = null;
return ctx;
});
}
}
@@ -0,0 +1,74 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI;
/// <summary>
/// A <see cref="ChatMessageStore"/> decorator that allows filtering the messages
/// passed into and out of an inner <see cref="ChatMessageStore"/>.
/// </summary>
public sealed class ChatMessageStoreMessageFilter : ChatMessageStore
{
private readonly ChatMessageStore _innerChatMessageStore;
private readonly Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? _invokingMessagesFilter;
private readonly Func<InvokedContext, InvokedContext>? _invokedMessagesFilter;
/// <summary>
/// Initializes a new instance of the <see cref="ChatMessageStoreMessageFilter"/> class.
/// </summary>
/// <remarks>Use this constructor to customize how messages are filtered before and after invocation by
/// providing appropriate filter functions. If no filters are provided, the message store operates without
/// additional filtering.</remarks>
/// <param name="innerChatMessageStore">The underlying chat message store to be wrapped. Cannot be null.</param>
/// <param name="invokingMessagesFilter">An optional filter function to apply to messages before they are invoked. If null, no filter is applied at this
/// stage.</param>
/// <param name="invokedMessagesFilter">An optional filter function to apply to the invocation context after messages have been invoked. If null, no
/// filter is applied at this stage.</param>
/// <exception cref="ArgumentNullException">Thrown if innerChatMessageStore is null.</exception>
public ChatMessageStoreMessageFilter(
ChatMessageStore innerChatMessageStore,
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? invokingMessagesFilter = null,
Func<InvokedContext, InvokedContext>? invokedMessagesFilter = null)
{
this._innerChatMessageStore = Throw.IfNull(innerChatMessageStore);
if (invokingMessagesFilter == null && invokedMessagesFilter == null)
{
throw new ArgumentException("At least one filter function, invokingMessagesFilter or invokedMessagesFilter, must be provided.");
}
this._invokingMessagesFilter = invokingMessagesFilter;
this._invokedMessagesFilter = invokedMessagesFilter;
}
/// <inheritdoc />
public override async ValueTask<IEnumerable<ChatMessage>> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
var messages = await this._innerChatMessageStore.InvokingAsync(context, cancellationToken).ConfigureAwait(false);
return this._invokingMessagesFilter != null ? this._invokingMessagesFilter(messages) : messages;
}
/// <inheritdoc />
public override ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default)
{
if (this._invokedMessagesFilter != null)
{
context = this._invokedMessagesFilter(context);
}
return this._innerChatMessageStore.InvokedAsync(context, cancellationToken);
}
/// <inheritdoc />
public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
{
return this._innerChatMessageStore.Serialize(jsonSerializerOptions);
}
}
@@ -25,7 +25,7 @@ namespace Microsoft.Agents.AI;
/// Derived classes can override specific methods to add custom behavior while maintaining compatibility with the agent interface.
/// </para>
/// </remarks>
public class DelegatingAIAgent : AIAgent
public abstract class DelegatingAIAgent : AIAgent
{
/// <summary>
/// Initializes a new instance of the <see cref="DelegatingAIAgent"/> class with the specified inner agent.
@@ -54,7 +54,7 @@ public class DelegatingAIAgent : AIAgent
protected AIAgent InnerAgent { get; }
/// <inheritdoc />
public override string Id => this.InnerAgent.Id;
protected override string? IdCore => this.InnerAgent.Id;
/// <inheritdoc />
public override string? Name => this.InnerAgent.Name;
@@ -81,7 +81,7 @@ public class DelegatingAIAgent : AIAgent
=> this.InnerAgent.DeserializeThread(serializedThread, jsonSerializerOptions);
/// <inheritdoc />
public override Task<AgentRunResponse> RunAsync(
protected override Task<AgentRunResponse> RunCoreAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
@@ -89,7 +89,7 @@ public class DelegatingAIAgent : AIAgent
=> this.InnerAgent.RunAsync(messages, thread, options, cancellationToken);
/// <inheritdoc />
public override IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
protected override IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
@@ -134,21 +134,10 @@ public sealed class InMemoryChatMessageStore : ChatMessageStore, IList<ChatMessa
}
/// <inheritdoc />
public override async Task AddMessagesAsync(IEnumerable<ChatMessage> messages, CancellationToken cancellationToken = default)
public override async ValueTask<IEnumerable<ChatMessage>> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
_ = Throw.IfNull(messages);
_ = Throw.IfNull(context);
this._messages.AddRange(messages);
if (this.ReducerTriggerEvent is ChatReducerTriggerEvent.AfterMessageAdded && this.ChatReducer is not null)
{
this._messages = (await this.ChatReducer.ReduceAsync(this._messages, cancellationToken).ConfigureAwait(false)).ToList();
}
}
/// <inheritdoc />
public override async Task<IEnumerable<ChatMessage>> GetMessagesAsync(CancellationToken cancellationToken = default)
{
if (this.ReducerTriggerEvent is ChatReducerTriggerEvent.BeforeMessagesRetrieval && this.ChatReducer is not null)
{
this._messages = (await this.ChatReducer.ReduceAsync(this._messages, cancellationToken).ConfigureAwait(false)).ToList();
@@ -157,6 +146,26 @@ public sealed class InMemoryChatMessageStore : ChatMessageStore, IList<ChatMessa
return this._messages;
}
/// <inheritdoc />
public override async ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default)
{
_ = Throw.IfNull(context);
if (context.InvokeException is not null)
{
return;
}
// Add request, AI context provider, and response messages to the store
var allNewMessages = context.RequestMessages.Concat(context.AIContextProviderMessages ?? []).Concat(context.ResponseMessages ?? []);
this._messages.AddRange(allNewMessages);
if (this.ReducerTriggerEvent is ChatReducerTriggerEvent.AfterMessageAdded && this.ChatReducer is not null)
{
this._messages = (await this.ChatReducer.ReduceAsync(this._messages, cancellationToken).ConfigureAwait(false)).ToList();
}
}
/// <inheritdoc />
public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
{
@@ -221,7 +230,7 @@ public sealed class InMemoryChatMessageStore : ChatMessageStore, IList<ChatMessa
{
/// <summary>
/// Trigger the reducer when a new message is added.
/// <see cref="AddMessagesAsync(IEnumerable{ChatMessage}, CancellationToken)"/> will only complete when reducer processing is done.
/// <see cref="InvokedAsync(InvokedContext, CancellationToken)"/> will only complete when reducer processing is done.
/// </summary>
AfterMessageAdded,
@@ -23,11 +23,6 @@ internal sealed class AzureAIProjectChatClient : DelegatingChatClient
private readonly AgentRecord? _agentRecord;
private readonly ChatOptions? _chatOptions;
private readonly AgentReference _agentReference;
/// <summary>
/// The usage of a no-op model is a necessary change to avoid OpenAIClients to throw exceptions when
/// used with Azure AI Agents as the model used is now defined at the agent creation time.
/// </summary>
private const string NoOpModel = "no-op";
/// <summary>
/// Initializes a new instance of the <see cref="AzureAIProjectChatClient"/> class.
@@ -42,7 +37,7 @@ internal sealed class AzureAIProjectChatClient : DelegatingChatClient
internal AzureAIProjectChatClient(AIProjectClient aiProjectClient, AgentReference agentReference, string? defaultModelId, ChatOptions? chatOptions)
: base(Throw.IfNull(aiProjectClient)
.GetProjectOpenAIClient()
.GetOpenAIResponseClient(defaultModelId ?? NoOpModel)
.GetProjectResponsesClientForAgent(agentReference)
.AsIChatClient())
{
this._agentClient = aiProjectClient;
@@ -132,13 +127,15 @@ internal sealed class AzureAIProjectChatClient : DelegatingChatClient
agentEnabledChatOptions.RawRepresentationFactory = (client) =>
{
if (originalFactory?.Invoke(this) is not ResponseCreationOptions responseCreationOptions)
if (originalFactory?.Invoke(this) is not CreateResponseOptions responseCreationOptions)
{
responseCreationOptions = new ResponseCreationOptions();
responseCreationOptions = new CreateResponseOptions();
}
ResponseCreationOptionsExtensions.set_Agent(responseCreationOptions, this._agentReference);
ResponseCreationOptionsExtensions.set_Model(responseCreationOptions, null);
responseCreationOptions.Agent = this._agentReference;
#pragma warning disable SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
responseCreationOptions.Patch.Remove("$.model"u8);
#pragma warning restore SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
return responseCreationOptions;
};
@@ -400,7 +400,7 @@ public static partial class AzureAIProjectChatClientExtensions
};
// Attempt to capture breaking glass options from the raw representation factory that match the agent definition.
if (options.ChatOptions?.RawRepresentationFactory?.Invoke(new NoOpChatClient()) is ResponseCreationOptions respCreationOptions)
if (options.ChatOptions?.RawRepresentationFactory?.Invoke(new NoOpChatClient()) is CreateResponseOptions respCreationOptions)
{
agentDefinition.ReasoningOptions = respCreationOptions.ReasoningOptions;
}
@@ -466,7 +466,7 @@ public static partial class AzureAIProjectChatClientExtensions
};
// Attempt to capture breaking glass options from the raw representation factory that match the agent definition.
if (options.ChatOptions?.RawRepresentationFactory?.Invoke(new NoOpChatClient()) is ResponseCreationOptions respCreationOptions)
if (options.ChatOptions?.RawRepresentationFactory?.Invoke(new NoOpChatClient()) is CreateResponseOptions respCreationOptions)
{
agentDefinition.ReasoningOptions = respCreationOptions.ReasoningOptions;
}
@@ -58,7 +58,7 @@ public class CopilotStudioAgent : AIAgent
=> new CopilotStudioAgentThread(serializedThread, jsonSerializerOptions);
/// <inheritdoc/>
public override async Task<AgentRunResponse> RunAsync(
protected override async Task<AgentRunResponse> RunCoreAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
@@ -96,7 +96,7 @@ public class CopilotStudioAgent : AIAgent
}
/// <inheritdoc/>
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
@@ -287,7 +287,7 @@ public sealed class CosmosChatMessageStore : ChatMessageStore, IDisposable
}
/// <inheritdoc />
public override async Task<IEnumerable<ChatMessage>> GetMessagesAsync(CancellationToken cancellationToken = default)
public override async ValueTask<IEnumerable<ChatMessage>> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
#pragma warning disable CA1513 // Use ObjectDisposedException.ThrowIf - not available on all target frameworks
if (this._disposed)
@@ -347,11 +347,14 @@ public sealed class CosmosChatMessageStore : ChatMessageStore, IDisposable
}
/// <inheritdoc />
public override async Task AddMessagesAsync(IEnumerable<ChatMessage> messages, CancellationToken cancellationToken = default)
public override async ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default)
{
if (messages is null)
Throw.IfNull(context);
if (context.InvokeException is not null)
{
throw new ArgumentNullException(nameof(messages));
// Do not store messages if there was an exception during invocation
return;
}
#pragma warning disable CA1513 // Use ObjectDisposedException.ThrowIf - not available on all target frameworks
@@ -361,7 +364,7 @@ public sealed class CosmosChatMessageStore : ChatMessageStore, IDisposable
}
#pragma warning restore CA1513
var messageList = messages as IReadOnlyCollection<ChatMessage> ?? messages.ToList();
var messageList = context.RequestMessages.Concat(context.AIContextProviderMessages ?? []).Concat(context.ResponseMessages ?? []).ToList();
if (messageList.Count == 0)
{
return;
@@ -381,7 +384,7 @@ public sealed class CosmosChatMessageStore : ChatMessageStore, IDisposable
/// <summary>
/// Adds multiple messages using transactional batch operations for atomicity.
/// </summary>
private async Task AddMessagesInBatchAsync(IReadOnlyCollection<ChatMessage> messages, CancellationToken cancellationToken)
private async Task AddMessagesInBatchAsync(List<ChatMessage> messages, CancellationToken cancellationToken)
{
var currentTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
@@ -217,9 +217,7 @@ public class CosmosCheckpointStore<T> : JsonCheckpointStore, IDisposable
}
}
/// <summary>
/// Represents a checkpoint document stored in Cosmos DB.
/// </summary>
/// <summary>Represents a checkpoint document stored in Cosmos DB.</summary>
internal sealed class CosmosCheckpointDocument
{
[JsonProperty("id")]
@@ -81,7 +81,7 @@ internal sealed partial class DevUIMiddleware
}
context.Response.StatusCode = StatusCodes.Status301MovedPermanently;
context.Response.Headers.Location = redirectUrl;
context.Response.Headers.Location = redirectUrl; // CodeQL [SM04598] justification: The redirect URL is constructed from a server-configured base path (_basePath), not user input. The query string is only appended as parameters and cannot change the redirect destination since this is a relative URL.
if (this._logger.IsEnabled(LogLevel.Debug))
{
@@ -231,7 +231,7 @@ internal static class EntitiesApiExtensions
return new EntityInfo(
Id: entityId,
Type: "agent",
Name: agent.DisplayName,
Name: agent.Name ?? agent.Id,
Description: agent.Description,
Framework: "agent_framework",
Tools: tools,
@@ -16,29 +16,34 @@ internal class AgentEntity(IServiceProvider services, CancellationToken cancella
private readonly DurableTaskClient _client = services.GetRequiredService<DurableTaskClient>();
private readonly ILoggerFactory _loggerFactory = services.GetRequiredService<ILoggerFactory>();
private readonly IAgentResponseHandler? _messageHandler = services.GetService<IAgentResponseHandler>();
private readonly DurableAgentsOptions _options = services.GetRequiredService<DurableAgentsOptions>();
private readonly CancellationToken _cancellationToken = cancellationToken != default
? cancellationToken
: services.GetService<IHostApplicationLifetime>()?.ApplicationStopping ?? CancellationToken.None;
public async Task<AgentRunResponse> RunAgentAsync(RunRequest request)
public Task<AgentRunResponse> RunAgentAsync(RunRequest request)
{
return this.Run(request);
}
// IDE1006 and VSTHRD200 disabled to allow method name to match the common cross-platform entity operation name.
#pragma warning disable IDE1006
#pragma warning disable VSTHRD200
public async Task<AgentRunResponse> Run(RunRequest request)
#pragma warning restore VSTHRD200
#pragma warning restore IDE1006
{
AgentSessionId sessionId = this.Context.Id;
IReadOnlyDictionary<string, Func<IServiceProvider, AIAgent>> agents =
this._services.GetRequiredService<IReadOnlyDictionary<string, Func<IServiceProvider, AIAgent>>>();
if (!agents.TryGetValue(sessionId.Name, out Func<IServiceProvider, AIAgent>? agentFactory))
{
throw new InvalidOperationException($"Agent '{sessionId.Name}' not found");
}
AIAgent agent = agentFactory(this._services);
AIAgent agent = this.GetAgent(sessionId);
EntityAgentWrapper agentWrapper = new(agent, this.Context, request, this._services);
// Logger category is Microsoft.DurableTask.Agents.{agentName}.{sessionId}
ILogger logger = this._loggerFactory.CreateLogger($"Microsoft.DurableTask.Agents.{agent.Name}.{sessionId.Key}");
ILogger logger = this.GetLogger(agent.Name!, sessionId.Key);
if (request.Messages.Count == 0)
{
logger.LogInformation("Ignoring empty request");
return new AgentRunResponse();
}
this.State.Data.ConversationHistory.Add(DurableAgentStateRequest.FromRunRequest(request));
@@ -113,6 +118,36 @@ internal class AgentEntity(IServiceProvider services, CancellationToken cancella
response.Usage?.TotalTokenCount);
}
// Update TTL expiration time. Only schedule deletion check on first interaction.
// Subsequent interactions just update the expiration time; CheckAndDeleteIfExpiredAsync
// will reschedule the deletion check when it runs.
TimeSpan? timeToLive = this._options.GetTimeToLive(sessionId.Name);
if (timeToLive.HasValue)
{
DateTime newExpirationTime = DateTime.UtcNow.Add(timeToLive.Value);
bool isFirstInteraction = this.State.Data.ExpirationTimeUtc is null;
this.State.Data.ExpirationTimeUtc = newExpirationTime;
logger.LogTTLExpirationTimeUpdated(sessionId, newExpirationTime);
// Only schedule deletion check on the first interaction when entity is created.
// On subsequent interactions, we just update the expiration time. The scheduled
// CheckAndDeleteIfExpiredAsync will reschedule itself if the entity hasn't expired.
if (isFirstInteraction)
{
this.ScheduleDeletionCheck(sessionId, logger, timeToLive.Value);
}
}
else
{
// TTL is disabled. Clear the expiration time if it was previously set.
if (this.State.Data.ExpirationTimeUtc.HasValue)
{
logger.LogTTLExpirationTimeCleared(sessionId);
this.State.Data.ExpirationTimeUtc = null;
}
}
return response;
}
finally
@@ -121,4 +156,78 @@ internal class AgentEntity(IServiceProvider services, CancellationToken cancella
DurableAgentContext.ClearCurrent();
}
}
/// <summary>
/// Checks if the entity has expired and deletes it if so, otherwise reschedules the deletion check.
/// </summary>
/// <remarks>
/// This method is called by the durable task runtime when a <c>CheckAndDeleteIfExpired</c> signal is received.
/// </remarks>
public void CheckAndDeleteIfExpired()
{
AgentSessionId sessionId = this.Context.Id;
AIAgent agent = this.GetAgent(sessionId);
ILogger logger = this.GetLogger(agent.Name!, sessionId.Key);
DateTime currentTime = DateTime.UtcNow;
DateTime? expirationTime = this.State.Data.ExpirationTimeUtc;
logger.LogTTLDeletionCheck(sessionId, expirationTime, currentTime);
if (expirationTime.HasValue)
{
if (currentTime >= expirationTime.Value)
{
// Entity has expired, delete it
logger.LogTTLEntityExpired(sessionId, expirationTime.Value);
this.State = null!;
}
else
{
// Entity hasn't expired yet, reschedule the deletion check
TimeSpan? timeToLive = this._options.GetTimeToLive(sessionId.Name);
if (timeToLive.HasValue)
{
this.ScheduleDeletionCheck(sessionId, logger, timeToLive.Value);
}
}
}
}
private void ScheduleDeletionCheck(AgentSessionId sessionId, ILogger logger, TimeSpan timeToLive)
{
DateTime currentTime = DateTime.UtcNow;
DateTime expirationTime = this.State.Data.ExpirationTimeUtc ?? currentTime.Add(timeToLive);
TimeSpan minimumDelay = this._options.MinimumTimeToLiveSignalDelay;
// To avoid excessive scheduling, we schedule the deletion check for no less than the minimum delay.
DateTime scheduledTime = expirationTime > currentTime.Add(minimumDelay)
? expirationTime
: currentTime.Add(minimumDelay);
logger.LogTTLDeletionScheduled(sessionId, scheduledTime);
// Schedule a signal to self to check for expiration
this.Context.SignalEntity(
this.Context.Id,
nameof(CheckAndDeleteIfExpired), // self-signal
options: new SignalEntityOptions { SignalTime = scheduledTime });
}
private AIAgent GetAgent(AgentSessionId sessionId)
{
IReadOnlyDictionary<string, Func<IServiceProvider, AIAgent>> agents =
this._services.GetRequiredService<IReadOnlyDictionary<string, Func<IServiceProvider, AIAgent>>>();
if (!agents.TryGetValue(sessionId.Name, out Func<IServiceProvider, AIAgent>? agentFactory))
{
throw new InvalidOperationException($"Agent '{sessionId.Name}' not found");
}
return agentFactory(this._services);
}
private ILogger GetLogger(string agentName, string sessionKey)
{
return this._loggerFactory.CreateLogger($"Microsoft.DurableTask.Agents.{agentName}.{sessionKey}");
}
}
@@ -1,5 +1,13 @@
# Release History
## [Unreleased]
### Changed
- Added TTL configuration for durable agent entities ([#2679](https://github.com/microsoft/agent-framework/pull/2679))
- Switch to new "Run" method name ([#2843](https://github.com/microsoft/agent-framework/pull/2843))
- Removed AgentThreadMetadata and used AgentSessionId directly instead ([#3067](https://github.com/microsoft/agent-framework/pull/3067));
## v1.0.0-preview.251204.1
- Added orchestration ID to durable agent entity state ([#2137](https://github.com/microsoft/agent-framework/pull/2137))
@@ -22,7 +22,7 @@ internal class DefaultDurableAgentClient(DurableTaskClient client, ILoggerFactor
await this._client.Entities.SignalEntityAsync(
sessionId,
nameof(AgentEntity.RunAgentAsync),
nameof(AgentEntity.Run),
request,
cancellation: cancellationToken);
@@ -63,7 +63,7 @@ public sealed class DurableAIAgent : AIAgent
/// <exception cref="AgentNotRegisteredException">Thrown when the agent has not been registered.</exception>
/// <exception cref="ArgumentException">Thrown when the provided thread is not valid for a durable agent.</exception>
/// <exception cref="NotSupportedException">Thrown when cancellation is requested (cancellation is not supported for durable agents).</exception>
public override async Task<AgentRunResponse> RunAsync(
protected override async Task<AgentRunResponse> RunCoreAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
@@ -107,7 +107,7 @@ public sealed class DurableAIAgent : AIAgent
{
return await this._context.Entities.CallEntityAsync<AgentRunResponse>(
durableThread.SessionId,
nameof(AgentEntity.RunAgentAsync),
nameof(AgentEntity.Run),
request);
}
catch (EntityOperationFailedException e) when (e.FailureDetails.ErrorType == "EntityTaskNotFound")
@@ -128,7 +128,7 @@ public sealed class DurableAIAgent : AIAgent
/// <param name="options">Optional run options.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A streaming response enumerable.</returns>
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
@@ -23,7 +23,7 @@ internal class DurableAIAgentProxy(string name, IDurableAgentClient agentClient)
return new DurableAgentThread(AgentSessionId.WithRandomKey(this.Name!));
}
public override async Task<AgentRunResponse> RunAsync(
protected override async Task<AgentRunResponse> RunCoreAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
@@ -70,7 +70,7 @@ internal class DurableAIAgentProxy(string name, IDurableAgentClient agentClient)
return await agentRunHandle.ReadAgentResponseAsync(cancellationToken);
}
public override IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
protected override IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
@@ -55,12 +55,6 @@ public sealed class DurableAgentThread : AgentThread
/// <inheritdoc/>
public override object? GetService(Type serviceType, object? serviceKey = null)
{
// This is a common convention for MAF agents.
if (serviceType == typeof(AgentThreadMetadata))
{
return new AgentThreadMetadata(conversationId: this.SessionId.ToString());
}
if (serviceType == typeof(AgentSessionId))
{
return this.SessionId;
@@ -9,23 +9,67 @@ public sealed class DurableAgentsOptions
{
// Agent names are case-insensitive
private readonly Dictionary<string, Func<IServiceProvider, AIAgent>> _agentFactories = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, TimeSpan?> _agentTimeToLive = new(StringComparer.OrdinalIgnoreCase);
internal DurableAgentsOptions()
{
}
/// <summary>
/// Gets or sets the default time-to-live (TTL) for agent entities.
/// </summary>
/// <remarks>
/// If an agent entity is idle for this duration, it will be automatically deleted.
/// Defaults to 14 days. Set to <see langword="null"/> to disable TTL for agents without explicit TTL configuration.
/// </remarks>
public TimeSpan? DefaultTimeToLive { get; set; } = TimeSpan.FromDays(14);
/// <summary>
/// Gets or sets the minimum delay for scheduling TTL deletion signals. Defaults to 5 minutes.
/// </summary>
/// <remarks>
/// This property is primarily useful for testing (where shorter delays are needed) or for
/// shorter-lived agents in workflows that need more rapid cleanup. The maximum allowed value is 5 minutes.
/// Reducing the minimum deletion delay below 5 minutes can be useful for testing or for ensuring rapid cleanup of short-lived agent sessions.
/// However, this can also increase the load on the system and should be used with caution.
/// </remarks>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the value exceeds 5 minutes.</exception>
public TimeSpan MinimumTimeToLiveSignalDelay
{
get;
set
{
const int MaximumDelayMinutes = 5;
if (value > TimeSpan.FromMinutes(MaximumDelayMinutes))
{
throw new ArgumentOutOfRangeException(
nameof(value),
value,
$"The minimum time-to-live signal delay cannot exceed {MaximumDelayMinutes} minutes.");
}
field = value;
}
} = TimeSpan.FromMinutes(5);
/// <summary>
/// Adds an AI agent factory to the options.
/// </summary>
/// <param name="name">The name of the agent.</param>
/// <param name="factory">The factory function to create the agent.</param>
/// <param name="timeToLive">Optional time-to-live for this agent's entities. If not specified, uses <see cref="DefaultTimeToLive"/>.</param>
/// <returns>The options instance.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> or <paramref name="factory"/> is null.</exception>
public DurableAgentsOptions AddAIAgentFactory(string name, Func<IServiceProvider, AIAgent> factory)
public DurableAgentsOptions AddAIAgentFactory(string name, Func<IServiceProvider, AIAgent> factory, TimeSpan? timeToLive = null)
{
ArgumentNullException.ThrowIfNull(name);
ArgumentNullException.ThrowIfNull(factory);
this._agentFactories.Add(name, factory);
if (timeToLive.HasValue)
{
this._agentTimeToLive[name] = timeToLive;
}
return this;
}
@@ -50,12 +94,13 @@ public sealed class DurableAgentsOptions
/// Adds an AI agent to the options.
/// </summary>
/// <param name="agent">The agent to add.</param>
/// <param name="timeToLive">Optional time-to-live for this agent's entities. If not specified, uses <see cref="DefaultTimeToLive"/>.</param>
/// <returns>The options instance.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="agent"/> is null.</exception>
/// <exception cref="ArgumentException">
/// Thrown when <paramref name="agent.Name"/> is null or whitespace or when an agent with the same name has already been registered.
/// </exception>
public DurableAgentsOptions AddAIAgent(AIAgent agent)
public DurableAgentsOptions AddAIAgent(AIAgent agent, TimeSpan? timeToLive = null)
{
ArgumentNullException.ThrowIfNull(agent);
@@ -70,6 +115,11 @@ public sealed class DurableAgentsOptions
}
this._agentFactories.Add(agent.Name, sp => agent);
if (timeToLive.HasValue)
{
this._agentTimeToLive[agent.Name] = timeToLive;
}
return this;
}
@@ -81,4 +131,14 @@ public sealed class DurableAgentsOptions
{
return this._agentFactories.AsReadOnly();
}
/// <summary>
/// Gets the time-to-live for a specific agent, or the default TTL if not specified.
/// </summary>
/// <param name="agentName">The name of the agent.</param>
/// <returns>The time-to-live for the agent, or the default TTL if not specified.</returns>
internal TimeSpan? GetTimeToLive(string agentName)
{
return this._agentTimeToLive.TryGetValue(agentName, out TimeSpan? ttl) ? ttl : this.DefaultTimeToLive;
}
}
@@ -19,15 +19,15 @@ internal sealed class EntityAgentWrapper(
private readonly IServiceProvider? _entityScopedServices = entityScopedServices;
// The ID of the agent is always the entity ID.
public override string Id => this._entityContext.Id.ToString();
protected override string? IdCore => this._entityContext.Id.ToString();
public override async Task<AgentRunResponse> RunAsync(
protected override async Task<AgentRunResponse> RunCoreAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default)
{
AgentRunResponse response = await base.RunAsync(
AgentRunResponse response = await base.RunCoreAsync(
messages,
thread,
this.GetAgentEntityRunOptions(options),
@@ -37,13 +37,13 @@ internal sealed class EntityAgentWrapper(
return response;
}
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
await foreach (AgentRunResponseUpdate update in base.RunStreamingAsync(
await foreach (AgentRunResponseUpdate update in base.RunCoreStreamingAsync(
messages,
thread,
this.GetAgentEntityRunOptions(options),

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