Compare commits

...
Author SHA1 Message Date
ChrisandGitHub 5fa153642e .NET Workflows - Update State Serialization for JSON Checkpointing (#1388)
* Checkpoint

* Checkpoint

* Namespace

* All ready

* Namespace

* Clean

* Fix
2025-10-10 20:10:17 +00:00
29cb87b805 Added example with MCP and authentication (#1389)
Co-authored-by: Eric Zhu <ekzhu@users.noreply.github.com>
2025-10-10 19:10:24 +00:00
350cdfc1cd Python: Fix AI Search Tool Sample and improve AI Search Exceptions (#1206)
* Python: Fix AI Search Tool Sample and improve AI Search Exceptions

* Python: Fix AI Search Tool Test

---------

Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com>
2025-10-10 18:46:57 +00:00
Victor DibiaandGitHub e032fe3993 Python: Fix Failure with Function Approval Messages in Chat Clients (#1322)
* Python: set role=tool when processing approval responses

* Python: set role=tool when processing approval responses

* Fix approval mode with OpenAIChatClient and threads: add approval requests to assistant message, fix deduplication/rejection call_id, filter approval content, add tests and example

* update test tools after change

* Rename _collect_approval_todos to _collect_approval_responses and filter empty call_ids
2025-10-10 18:38:23 +00:00
westeyandGitHub d6fdb91480 .NET: Inline const values in samples where used only once (#1387)
* Inline const values in samples where used only once

* Fix some spacing issues and use consistent ordering of settings/params
2025-10-10 16:50:53 +00:00
3730db3e94 .NET: Sanitize agent name (#1368)
* sanitize agent name

* simplify

* Update dotnet/src/Microsoft.Agents.AI/AgentExtensions.cs

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

* Update dotnet/src/Microsoft.Agents.AI/AgentExtensions.cs

Co-authored-by: Stephen Toub <stoub@microsoft.com>

* Update dotnet/src/Microsoft.Agents.AI/AgentExtensions.cs

Co-authored-by: Stephen Toub <stoub@microsoft.com>

* Update dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentExtensionsTests.cs

Co-authored-by: Stephen Toub <stoub@microsoft.com>

* change regex to flag underscores as well so their sequence can be replaced with a single one.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Stephen Toub <stoub@microsoft.com>
2025-10-10 16:49:56 +00:00
Dmytro StrukandGitHub 00a124dae6 Added git tag property (#1386) 2025-10-10 15:37:14 +00:00
ChrisandGitHub 7e1fd67e76 .NET Workflows - Capture the identifier of the LastMessage (the workflow input) (#1372)
* Fixed

* Test fix
2025-10-10 06:45:09 +00:00
Stephen ToubandGitHub 2c9cf6f59d Remove unused PackageVersions from Directory.Packages.props (#1334) 2025-10-09 21:28:00 +00:00
Giles OdigweandGitHub 7238cde5af Python: AzureAI Bing Connection Name Support (#1364)
* bing connection name support

* error handling + unit tests

* connection id and name handling
2025-10-09 21:13:07 +00:00
Jacob AlberandGitHub 4cd81fe8e7 .NET: fix: Make State Persistence APIs work better with PortableValue (#1367)
* fix: Make State Persistence APIs work better with PortableValue

* test: Temporarily disable checking for T=object in ReadStateAsync
2025-10-09 20:24:55 +00:00
2f53ce4abd Python: A2A AuthInterceptor Support (#1317)
* a2a authinterceptor support

* Update python/packages/a2a/agent_framework_a2a/_agent.py

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

* small fix

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-09 18:56:54 +00:00
9fe4a61dd0 Python: Added function approval example with streaming (#1365)
* Added function approval example with streaming

* Update python/samples/getting_started/tools/ai_tool_with_approval.py

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-09 17:31:36 +00:00
Dmytro StrukandGitHub a02b82f022 Python: Fixed instructions duplication in model clients (#1332)
* Small fix

* Updated instruction handling in chat clients
2025-10-09 14:49:03 +00:00
Christian GlessnerandGitHub 8967269d3e Fix: Add missing ChatMessage[] handler to ChatProtocolExecutor (#1337)
- Added ChatMessage[] handler to ConfigureRoutes to support array dispatch
- Workflow runtime can dispatch messages as either List<ChatMessage> or ChatMessage[]
- Added 10 comprehensive unit tests validating message routing behavior
- Tests ensure functionality and protect against future refactoring (issue #782)
- Updated code comments to reflect exact-type-matching requirement

Fixes issue where raw WorkflowBuilder with AIAgent nodes failed to receive
initial messages due to missing array type handler.
2025-10-09 10:00:09 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
d16d56b555 Bump AWSSDK.Extensions.Bedrock.MEAI from 4.0.3.5 to 4.0.3.6 (#1340)
---
updated-dependencies:
- dependency-name: AWSSDK.Extensions.Bedrock.MEAI
  dependency-version: 4.0.3.6
  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>
2025-10-09 09:39:48 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
f17bf0a502 Bump FluentAssertions from 8.7.0 to 8.7.1 (#1341)
---
updated-dependencies:
- dependency-name: FluentAssertions
  dependency-version: 8.7.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>
2025-10-09 09:39:20 +00:00
Mark WallaceandGitHub 1b5e20b5b0 Version preview.251009.1 (#1339) 2025-10-09 08:42:49 +00:00
Tao ChenandGitHub 2397795c1d Python: Fix deadlock in Magentic workflow (#1325)
* Fix deadlock in Magentic workflow

* Fix unit test
2025-10-09 00:23:46 +00:00
SergeyMenshykhandGitHub 15afc966ce .NET: Add missing workflow properties to the workflow.definition tag (#1261)
* add workflow edge data properties to the workflow.definition tag.

* use workflow info classes for workflow.definition tag value

* add unit test

* fix test

* fix formatting issue

* remove flaky unit test

* remove unused package dependency
2025-10-08 21:51:59 +00:00
ChrisandGitHub 988623e7b8 .NET Workflow - Update declarative code patterns (#1313)
* It hurts

* Fixed

* Namespace

* Namespace
2025-10-08 21:32:47 +00:00
Stephen ToubandGitHub 74864f353d Update to latest OpenAI (#1286) 2025-10-08 21:09:52 +00:00
3d31a4a204 Python: samples: add api_key for Ollama sample usage (#1240)
* samples: add api_key for Ollama sample usage

* Update python/samples/getting_started/agents/ollama/ollama_with_openai_chat_client.py

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

* fix: update Ollama API key to an unused constant

---------

Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com>
2025-10-08 20:45:24 +00:00
Dmytro StrukandGitHub 7e891fab39 Python: Included existing agent definition in requests to Azure AI (#1285)
* Fixed instructions handling for existing Azure AI agents

* Updated tool handling for existing agents

* Small update

* Added more comments
2025-10-08 20:33:57 +00:00
Victor DibiaandGitHub c341ee7ed2 Python: DevUI - Internal Refactor, Conversations API support, and per… (#1235)
* Python: DevUI - Internal Refactor, Conversations API support, and performance improvements

Comprehensive refactor of DevUI package including samples relocation,
frontend reorganization, OpenAI Conversations API support, and critical
performance and code quality improvements.

Key Changes:

Architecture & Organization
- Moved DevUI samples to python/samples/getting_started/devui/
- Consolidated with other framework samples for better discoverability
- Added .env.example files and comprehensive README
- Restructured frontend components into feature-based folders (agent, workflow, gallery, layout)
- Created new OpenAI-compliant message renderers (devui should render oai responses types primarily)

New Features
- Added _conversations.py (467 lines) - Full conversation storage abstraction, replaces the /threads endpoint to better match oai conversations api
- Implements OpenAI Conversations API for thread management, Supports in-memory and extensible storage backends

API Simplification
- Use 'model' field as entity_id (agent/workflow name) instead of extra_body
- Use standard OpenAI 'conversation' field for conversation context.

Performance & Quality Improvements
- Improved context management in MessageMapper with bounded memory (~500KB max)
- Implemented hybrid LRU + cleanup approach to prevent unbounded memory growth
- General QOL improvement - Eliminated ~150 lines of dead/duplicate code, Consolidated helper functions into _utils.py, Extracted magic numbers to module-level constants, Optimized conversation item lookups with index-based approach

Testing
- Added test_conversations.py (13 tests)
- Added test_performance_fixes.py (9 tests)
- Updated existing tests for code consolidation
- 53 tests passing

Impact: 76 files changed: +4,106 insertions, -2,373 deletions
All linting and formatting checks passing. No breaking changes - backward compatible.

Migration: Samples moved to python/samples/getting_started/devui/

* readme lint fixes

* initial support for function approval and minor ui fixes
2025-10-08 19:34:30 +00:00
Eduard van ValkenburgandGitHub f5abbc67ae Python: sample with Foundry Redteams (#1306)
* sample with evals

* remove demo output
2025-10-08 19:16:44 +00:00
Eduard van ValkenburgandGitHub a36e183600 Python: improved thread serialization and deser with better tests (#1316)
* improved thread serialization and deser with better tests

* update redis with the same
2025-10-08 19:04:11 +00:00
Tao ChenandGitHub c2c8ec3d4e Python: Reorganize workflows modules (#1282)
* Reorganize modules

* Fix unit tests

* Remove submodules
2025-10-08 17:04:06 +00:00
Eric ZhuandGitHub 1c5e607a1f Python: fix tool call content not showing up in workflow events (#1290)
* fix tool call content not showing up in workflow events

* fix lint

* touch up

* add another sample to show function bridge

* missing sample
2025-10-08 16:39:08 +00:00
Giles OdigweandGitHub 334d52f300 Python: AzureAI Agent AI Search Sample (#1281)
* azure ai search sample

* small fix
2025-10-08 15:52:02 +00:00
westeyandGitHub 523127fbf4 Update readme.md sample to show sample parameter values and switch to openai sdk (#1265)
* Update readme.md sample to show sample parameter values and make azure sdk pre-release

* Add clarifying comment about params.

* Switch sample to use the OpenAI SDK
2025-10-08 10:56:34 +00:00
Jacob AlberandGitHub 5902bcb10a .NET: [BREAKING] Propagate CancellationToken into Workflow Executors and message handlers (#1280)
* feat: Propagate CancellationToken to Executors

* Also adds cancellation propagation to `Executor`-accessible APIs
* Adds registrators for cancellable handlers to `RouteBuilder`
* [BREAKING]: Adds `CancellationToken` to `IMessageHandler.HandleAsync`

* test: Re-enable Concurrent Orchestration test

* refactor: Delete unused IInputCoordinator

* refactor: Remove superfluous argument qualifications
2025-10-08 00:28:44 +00:00
ChrisandGitHub eb049c43a6 .NET Workflows - Enhance support and validation for image input (#1274)
* Updated

* Comments

* Namespace

* Comment

* Cleanup

* Update agent definition

* Disable

* Config

* Enable
2025-10-08 00:00:34 +00:00
wuwengandGitHub 362652b966 .NET: Add Azure Monitor OpenTelemetry Exporter samples (#1231)
* Add Azure Monitor OpenTelemetry Exporter to enhance observability in agent samples

* add another sample code for application isnights
2025-10-07 23:16:32 +00:00
westeyandGitHub 127bf68748 Reference the correct JsonUtilities class (#1270) 2025-10-07 22:02:19 +00:00
275 changed files with 10187 additions and 4808 deletions
@@ -160,6 +160,7 @@ jobs:
AzureAI__DeploymentName: ${{ vars.AZUREAI__DEPLOYMENTNAME }}
AzureAI__BingConnectionId: ${{ vars.AZUREAI__BINGCONECTIONID }}
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
FOUNDRY_MEDIA_DEPLOYMENT_NAME: ${{ vars.FOUNDRY_MEDIA_DEPLOYMENT_NAME }}
FOUNDRY_MODEL_DEPLOYMENT_NAME: ${{ vars.FOUNDRY_MODEL_DEPLOYMENT_NAME }}
FOUNDRY_CONNECTION_GROUNDING_TOOL: ${{ vars.FOUNDRY_CONNECTION_GROUNDING_TOOL }}
+22 -9
View File
@@ -119,22 +119,35 @@ if __name__ == "__main__":
### Basic Agent - .NET
Create a simple Agent, using OpenAI Responses, that writes a haiku about the Microsoft Agent Framework
```c#
// dotnet add package Microsoft.Agents.AI.OpenAI --prerelease
using System;
using OpenAI;
// Replace the <apikey> with your OpenAI API key.
var agent = new OpenAIClient("<apikey>")
.GetOpenAIResponseClient("gpt-4o-mini")
.CreateAIAgent(name: "HaikuBot", instructions: "You are an upbeat assistant that writes beautifully.");
Console.WriteLine(await agent.RunAsync("Write a haiku about Microsoft Agent Framework."));
```
Create a simple Agent, using Azure OpenAI Responses with token based auth, that writes a haiku about the Microsoft Agent Framework
```c#
// dotnet add package Microsoft.Agents.AI.OpenAI --prerelease
// dotnet add package Azure.AI.OpenAI
// dotnet add package Azure.Identity
// Use `az login` to authenticate with Azure CLI
using System;
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using OpenAI;
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")!;
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME")!;
var agent = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential())
.GetOpenAIResponseClient(deploymentName)
// Replace <resource> and gpt-4o-mini with your Azure OpenAI resource name and deployment name.
var agent = new OpenAIClient(
new BearerTokenPolicy(new AzureCliCredential(), "https://ai.azure.com/.default"),
new OpenAIClientOptions() { Endpoint = new Uri("https://<resource>.openai.azure.com/openai/v1") })
.GetOpenAIResponseClient("gpt-4o-mini")
.CreateAIAgent(name: "HaikuBot", instructions: "You are an upbeat assistant that writes beautifully.");
Console.WriteLine(await agent.RunAsync("Write a haiku about Microsoft Agent Framework."));
+37 -45
View File
@@ -10,58 +10,47 @@
<AspireAppHostSdkVersion>9.5.1</AspireAppHostSdkVersion>
</PropertyGroup>
<ItemGroup>
<!-- Azure.* -->
<PackageVersion Include="Aspire.Azure.AI.OpenAI" Version="9.5.0-preview.1.25474.7" />
<!-- Aspire.* -->
<PackageVersion Include="Aspire.Azure.AI.OpenAI" Version="9.5.1-preview.1.25502.11" />
<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="Aspire.Hosting.Testing" Version="$(AspireAppHostSdkVersion)" />
<PackageVersion Include="Azure.AI.Agents.Persistent" Version="1.2.0-beta.5" />
<PackageVersion Include="Azure.AI.OpenAI" Version="2.3.0-beta.2" />
<PackageVersion Include="Azure.Identity" Version="1.16.0" />
<PackageVersion Include="Azure.Monitor.OpenTelemetry.Exporter" Version="1.4.0" />
<PackageVersion Include="CommunityToolkit.Aspire.OllamaSharp" Version="9.8.0" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="9.0.9" />
<PackageVersion Include="Microsoft.Extensions.AI.AzureAIInference" Version="9.9.1-preview.1.25474.6" />
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="9.9.0" />
<PackageVersion Include="Microsoft.Extensions.ServiceDiscovery" Version="$(AspireAppHostSdkVersion)" />
<PackageVersion Include="OpenTelemetry.Api" Version="1.12.0" />
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.12.0" />
<PackageVersion Include="Microsoft.Azure.Cosmos" Version="3.53.1" />
<!-- Newtonsoft (Required by CosmosClient) -->
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.4" />
<!-- Azure.* -->
<PackageVersion Include="Azure.AI.Agents.Persistent" Version="1.2.0-beta.5" />
<PackageVersion Include="Azure.AI.OpenAI" Version="2.5.0-beta.1" />
<PackageVersion Include="Azure.Identity" Version="1.17.0" />
<PackageVersion Include="Azure.Monitor.OpenTelemetry.Exporter" Version="1.4.0" />
<!-- System.* -->
<PackageVersion Include="System.Linq.Async" Version="6.0.3" />
<PackageVersion Include="System.Net.ServerSentEvents" Version="9.0.9" />
<PackageVersion Include="System.Text.Json" Version="9.0.9" />
<PackageVersion Include="System.CodeDom" Version="9.0.9" />
<PackageVersion Include="System.Collections.Immutable" Version="9.0.9" />
<PackageVersion Include="System.CommandLine" Version="2.0.0-rc.1.25451.107" />
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="9.0.9" />
<PackageVersion Include="System.Net.ServerSentEvents" Version="9.0.9" />
<PackageVersion Include="System.Text.Json" Version="9.0.9" />
<PackageVersion Include="System.Threading.Channels" Version="9.0.9" />
<PackageVersion Include="System.Threading.Tasks.Extensions" Version="4.6.3" />
<PackageVersion Include="Microsoft.Bcl.HashCode" Version="6.0.0" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.9" />
<!-- OpenTelemetry -->
<PackageVersion Include="OpenTelemetry" Version="1.12.0" />
<PackageVersion Include="OpenTelemetry.Api" Version="1.12.0" />
<PackageVersion Include="OpenTelemetry.Exporter.Console" Version="1.12.0" />
<PackageVersion Include="OpenTelemetry.Exporter.InMemory" Version="1.12.0" />
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0" />
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.12.0" />
<!-- Microsoft.AspNetCore.* -->
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="9.0.9" />
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.4" />
<!-- Microsoft.Extensions.* -->
<PackageVersion Include="Microsoft.Bcl.HashCode" Version="6.0.0" />
<PackageVersion Include="Microsoft.Agents.AI" Version="1.0.0-preview.251001.3" />
<PackageVersion Include="Microsoft.Agents.AI.Abstractions" Version="1.0.0-preview.251001.3" />
<PackageVersion Include="Microsoft.Agents.AI.AzureAI" Version="1.0.0-preview.251001.3" />
<PackageVersion Include="Microsoft.Agents.AI.OpenAI" Version="1.0.0-preview.251001.3" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.9" />
<PackageVersion Include="OpenAI" Version="2.4.0" />
<PackageVersion Include="Microsoft.Extensions.AI" Version="9.9.1" />
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="9.9.1" />
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="9.9.0-preview.1.25458.4" />
<PackageVersion Include="Microsoft.Extensions.AI.AzureAIInference" Version="9.9.1-preview.1.25474.6" />
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="9.9.1-preview.1.25474.6" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="9.0.9" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.9" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.9" />
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.9" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.9" />
@@ -69,29 +58,30 @@
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.9" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.9" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="9.0.9" />
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="9.9.0" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.9" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.9" />
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="9.0.9" />
<PackageVersion Include="Microsoft.Extensions.Logging.Testing" Version="9.0.9" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.9" />
<PackageVersion Include="Microsoft.Extensions.ServiceDiscovery" Version="$(AspireAppHostSdkVersion)" />
<!-- Vector Stores -->
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.InMemory" Version="1.65.0-preview" />
<PackageVersion Include="Microsoft.SemanticKernel" Version="1.65.0" />
<PackageVersion Include="Microsoft.SemanticKernel.Agents.Core" Version="1.65.0" />
<PackageVersion Include="Microsoft.SemanticKernel.Agents.OpenAI" Version="1.65.0-preview" />
<PackageVersion Include="Microsoft.SemanticKernel.Agents.AzureAI" Version="1.65.0-preview" />
<PackageVersion Include="Microsoft.SemanticKernel" Version="1.66.0" />
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.InMemory" Version="1.66.0-preview" />
<PackageVersion Include="Microsoft.SemanticKernel.Agents.Core" Version="1.66.0" />
<PackageVersion Include="Microsoft.SemanticKernel.Agents.OpenAI" Version="1.66.0-preview" />
<PackageVersion Include="Microsoft.SemanticKernel.Agents.AzureAI" Version="1.66.0-preview" />
<!-- Agent SDKs -->
<PackageVersion Include="Microsoft.Agents.CopilotStudio.Client" Version="1.2.41" />
<!-- A2A -->
<PackageVersion Include="A2A" Version="0.3.1-preview" />
<PackageVersion Include="A2A.AspNetCore" Version="0.3.1-preview" />
<!-- MCP -->
<PackageVersion Include="ModelContextProtocol" Version="0.4.0-preview.1" />
<PackageVersion Include="ModelContextProtocol" Version="0.4.0-preview.2" />
<!-- Inference SDKs -->
<PackageVersion Include="Anthropic.SDK" Version="5.5.3" />
<PackageVersion Include="AWSSDK.Extensions.Bedrock.MEAI" Version="4.0.3.5" />
<PackageVersion Include="Anthropic.SDK" Version="5.6.0" />
<PackageVersion Include="AWSSDK.Extensions.Bedrock.MEAI" Version="4.0.3.6" />
<PackageVersion Include="Microsoft.ML.OnnxRuntimeGenAI" Version="0.9.2" />
<PackageVersion Include="OllamaSharp" Version="5.4.7" />
<PackageVersion Include="OpenAI" Version="2.5.0" />
<!-- Identity -->
<PackageVersion Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.77.1" />
<!-- Workflows -->
@@ -99,12 +89,14 @@
<PackageVersion Include="Microsoft.Bot.ObjectModel.Json" Version="1.2025.1003.2" />
<PackageVersion Include="Microsoft.Bot.ObjectModel.PowerFx" Version="1.2025.1003.2" />
<PackageVersion Include="Microsoft.PowerFx.Interpreter" Version="1.4.0" />
<!-- Community -->
<PackageVersion Include="System.Linq.Async" Version="6.0.3" />
<!-- Test -->
<PackageVersion Include="FluentAssertions" Version="8.7.0" />
<PackageVersion Include="FluentAssertions" Version="8.7.1" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.0" />
<PackageVersion Include="Moq" Version="[4.18.4]" />
<PackageVersion Include="Microsoft.SemanticKernel.Agents.Abstractions" Version="1.65.0" />
<PackageVersion Include="Microsoft.SemanticKernel.Agents.Yaml" Version="1.65.0-beta" />
<PackageVersion Include="Microsoft.SemanticKernel.Agents.Abstractions" Version="1.66.0" />
<PackageVersion Include="Microsoft.SemanticKernel.Agents.Yaml" Version="1.66.0-beta" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.abstractions" Version="2.0.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.3" />
+1
View File
@@ -113,6 +113,7 @@
<Project Path="samples/GettingStarted/Workflows/HumanInTheLoop/HumanInTheLoopBasic/HumanInTheLoopBasic.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/Workflows/Observability/">
<Project Path="samples/GettingStarted/Workflows/Observability/ApplicationInsights/ApplicationInsights.csproj" />
<Project Path="samples/GettingStarted/Workflows/Observability/AspireDashboard/AspireDashboard.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/Workflows/Visualization/">
+3 -2
View File
@@ -2,8 +2,9 @@
<PropertyGroup>
<!-- Central version prefix - applies to all nuget packages. -->
<VersionPrefix>1.0.0</VersionPrefix>
<PackageVersion Condition="'$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).251007.1</PackageVersion>
<PackageVersion Condition="'$(VersionSuffix)' == ''">$(VersionPrefix)-preview.251007.1</PackageVersion>
<PackageVersion Condition="'$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).251009.1</PackageVersion>
<PackageVersion Condition="'$(VersionSuffix)' == ''">$(VersionPrefix)-preview.251009.1</PackageVersion>
<GitTag>1.0.0-preview.251009.1</GitTag>
<Configurations>Debug;Release;Publish</Configurations>
<IsPackable>true</IsPackable>
@@ -14,9 +14,6 @@ var endpoint = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_OPENAI_ENDPOINT
var apiKey = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_OPENAI_APIKEY");
var model = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_MODEL_DEPLOYMENT") ?? "Phi-4-mini-instruct";
const string JokerName = "Joker";
const string JokerInstructions = "You are good at telling jokes.";
// Since we are using the OpenAI Client SDK, we need to override the default endpoint to point to Azure Foundry.
var clientOptions = new OpenAIClientOptions() { Endpoint = new Uri(endpoint) };
@@ -26,8 +23,8 @@ OpenAIClient client = string.IsNullOrWhiteSpace(apiKey)
: new OpenAIClient(new ApiKeyCredential(apiKey), clientOptions);
AIAgent agent = client
.GetChatClient(model)
.CreateAIAgent(JokerInstructions, JokerName);
.GetChatClient(model)
.CreateAIAgent(instructions: "You are good at telling jokes.", name: "Joker");
// Invoke the agent and output the text result.
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate."));
@@ -10,14 +10,11 @@ using OpenAI;
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";
const string JokerName = "Joker";
const string JokerInstructions = "You are good at telling jokes.";
AIAgent agent = new AzureOpenAIClient(
new Uri(endpoint),
new AzureCliCredential())
.GetChatClient(deploymentName)
.CreateAIAgent(JokerInstructions, JokerName);
.CreateAIAgent(instructions: "You are good at telling jokes.", name: "Joker");
// Invoke the agent and output the text result.
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate."));
@@ -10,14 +10,11 @@ using OpenAI;
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";
const string JokerName = "Joker";
const string JokerInstructions = "You are good at telling jokes.";
AIAgent agent = new AzureOpenAIClient(
new Uri(endpoint),
new AzureCliCredential())
.GetOpenAIResponseClient(deploymentName)
.CreateAIAgent(JokerInstructions, JokerName);
.CreateAIAgent(instructions: "You are good at telling jokes.", name: "Joker");
// Invoke the agent and output the text result.
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate."));
@@ -10,12 +10,9 @@ using Microsoft.ML.OnnxRuntimeGenAI;
// E.g. C:\repos\Phi-4-mini-instruct-onnx\cpu_and_mobile\cpu-int4-rtn-block-32-acc-level-4
var modelPath = Environment.GetEnvironmentVariable("ONNX_MODEL_PATH") ?? throw new InvalidOperationException("ONNX_MODEL_PATH is not set.");
const string JokerName = "Joker";
const string JokerInstructions = "You are good at telling jokes.";
// Get a chat client for ONNX and use it to construct an AIAgent.
using OnnxRuntimeGenAIChatClient chatClient = new(modelPath);
AIAgent agent = chatClient.CreateAIAgent(JokerInstructions, JokerName);
AIAgent agent = chatClient.CreateAIAgent(instructions: "You are good at telling jokes.", name: "Joker");
// Invoke the agent and output the text result.
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate."));
@@ -9,12 +9,9 @@ using OllamaSharp;
var endpoint = Environment.GetEnvironmentVariable("OLLAMA_ENDPOINT") ?? throw new InvalidOperationException("OLLAMA_ENDPOINT is not set.");
var modelName = Environment.GetEnvironmentVariable("OLLAMA_MODEL_NAME") ?? throw new InvalidOperationException("OLLAMA_MODEL_NAME is not set.");
const string JokerName = "Joker";
const string JokerInstructions = "You are good at telling jokes.";
// Get a chat client for Ollama and use it to construct an AIAgent.
AIAgent agent = new OllamaApiClient(new Uri(endpoint), modelName)
.CreateAIAgent(JokerInstructions, JokerName);
.CreateAIAgent(instructions: "You are good at telling jokes.", name: "Joker");
// Invoke the agent and output the text result.
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate."));
@@ -8,13 +8,10 @@ using OpenAI;
var apiKey = Environment.GetEnvironmentVariable("OPENAI_APIKEY") ?? throw new InvalidOperationException("OPENAI_APIKEY is not set.");
var model = Environment.GetEnvironmentVariable("OPENAI_MODEL") ?? "gpt-4o-mini";
const string JokerName = "Joker";
const string JokerInstructions = "You are good at telling jokes.";
AIAgent agent = new OpenAIClient(
apiKey)
.GetChatClient(model)
.CreateAIAgent(JokerInstructions, JokerName);
.CreateAIAgent(instructions: "You are good at telling jokes.", name: "Joker");
// Invoke the agent and output the text result.
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate."));
@@ -8,13 +8,10 @@ using OpenAI;
var apiKey = Environment.GetEnvironmentVariable("OPENAI_APIKEY") ?? throw new InvalidOperationException("OPENAI_APIKEY is not set.");
var model = Environment.GetEnvironmentVariable("OPENAI_MODEL") ?? "gpt-4o-mini";
const string JokerName = "Joker";
const string JokerInstructions = "You are good at telling jokes.";
AIAgent agent = new OpenAIClient(
apiKey)
.GetOpenAIResponseClient(model)
.CreateAIAgent(JokerInstructions, JokerName);
.CreateAIAgent(instructions: "You are good at telling jokes.", name: "Joker");
// Invoke the agent and output the text result.
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate."));
@@ -10,12 +10,9 @@ using OpenAI.Chat;
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";
const string JokerName = "Joker";
const string JokerInstructions = "You are good at telling jokes.";
AIAgent agent = new OpenAIClient(apiKey)
.GetChatClient(model)
.CreateAIAgent(JokerInstructions, JokerName);
.CreateAIAgent(instructions: "You are good at telling jokes.", name: "Joker");
UserChatMessage chatMessage = new("Tell me a joke about a pirate.");
@@ -10,14 +10,11 @@ using OpenAI;
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";
const string JokerName = "Joker";
const string JokerInstructions = "You are good at telling jokes.";
AIAgent agent = new AzureOpenAIClient(
new Uri(endpoint),
new AzureCliCredential())
.GetChatClient(deploymentName)
.CreateAIAgent(JokerInstructions, JokerName);
.GetChatClient(deploymentName)
.CreateAIAgent(instructions: "You are good at telling jokes.", name: "Joker");
// Invoke the agent and output the text result.
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate."));
@@ -10,14 +10,11 @@ using OpenAI;
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";
const string JokerName = "Joker";
const string JokerInstructions = "You are good at telling jokes.";
AIAgent agent = new AzureOpenAIClient(
new Uri(endpoint),
new AzureCliCredential())
.GetChatClient(deploymentName)
.CreateAIAgent(JokerInstructions, JokerName);
.GetChatClient(deploymentName)
.CreateAIAgent(instructions: "You are good at telling jokes.", name: "Joker");
// Invoke the agent with a multi-turn conversation, where the context is preserved in the thread object.
AgentThread thread = agent.GetNewThread();
@@ -21,8 +21,8 @@ static string GetWeather([Description("The location to get the weather for.")] s
AIAgent agent = new AzureOpenAIClient(
new Uri(endpoint),
new AzureCliCredential())
.GetChatClient(deploymentName)
.CreateAIAgent(instructions: "You are a helpful assistant", tools: [AIFunctionFactory.Create(GetWeather)]);
.GetChatClient(deploymentName)
.CreateAIAgent(instructions: "You are a helpful assistant", tools: [AIFunctionFactory.Create(GetWeather)]);
// Non-streaming agent interaction with function tools.
Console.WriteLine(await agent.RunAsync("What is the weather like in Amsterdam?"));
@@ -25,8 +25,8 @@ static string GetWeather([Description("The location to get the weather for.")] s
AIAgent agent = new AzureOpenAIClient(
new Uri(endpoint),
new AzureCliCredential())
.GetChatClient(deploymentName)
.CreateAIAgent(instructions: "You are a helpful assistant", tools: [new ApprovalRequiredAIFunction(AIFunctionFactory.Create(GetWeather))]);
.GetChatClient(deploymentName)
.CreateAIAgent(instructions: "You are a helpful assistant", tools: [new ApprovalRequiredAIFunction(AIFunctionFactory.Create(GetWeather))]);
// Call the agent and check if there are any user input requests to handle.
AgentThread thread = agent.GetNewThread();
@@ -11,15 +11,12 @@ using OpenAI;
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";
const string JokerName = "Joker";
const string JokerInstructions = "You are good at telling jokes.";
// Create the agent
AIAgent agent = new AzureOpenAIClient(
new Uri(endpoint),
new AzureCliCredential())
.GetChatClient(deploymentName)
.CreateAIAgent(JokerInstructions, JokerName);
.GetChatClient(deploymentName)
.CreateAIAgent(instructions: "You are good at telling jokes.", name: "Joker");
// Start a new thread for the agent conversation.
AgentThread thread = agent.GetNewThread();
@@ -17,9 +17,6 @@ using SampleApp;
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";
const string JokerName = "Joker";
const string JokerInstructions = "You are good at telling jokes.";
// Create a vector store to store the chat messages in.
// Replace this with a vector store implementation of your choice if you want to persist the chat history to disk.
VectorStore vectorStore = new InMemoryVectorStore();
@@ -28,19 +25,19 @@ VectorStore vectorStore = new InMemoryVectorStore();
AIAgent agent = new AzureOpenAIClient(
new Uri(endpoint),
new AzureCliCredential())
.GetChatClient(deploymentName)
.CreateAIAgent(new ChatClientAgentOptions
{
Name = JokerName,
Instructions = JokerInstructions,
ChatMessageStoreFactory = ctx =>
{
// Create a new chat message store for this agent that stores the messages in a vector store.
// Each thread must get its own copy of the VectorChatMessageStore, since the store
// also contains the id that the thread is stored under.
return new VectorChatMessageStore(vectorStore, ctx.SerializedState, ctx.JsonSerializerOptions);
}
});
.GetChatClient(deploymentName)
.CreateAIAgent(new ChatClientAgentOptions
{
Instructions = "You are good at telling jokes.",
Name = "Joker",
ChatMessageStoreFactory = ctx =>
{
// Create a new chat message store for this agent that stores the messages in a vector store.
// Each thread must get its own copy of the VectorChatMessageStore, since the store
// also contains the id that the thread is stored under.
return new VectorChatMessageStore(vectorStore, ctx.SerializedState, ctx.JsonSerializerOptions);
}
});
// Start a new thread for the agent conversation.
AgentThread thread = agent.GetNewThread();
@@ -11,6 +11,7 @@
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Azure.Monitor.OpenTelemetry.Exporter" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
<PackageReference Include="OpenTelemetry" />
<PackageReference Include="OpenTelemetry.Exporter.Console" />
@@ -4,6 +4,7 @@
using Azure.AI.OpenAI;
using Azure.Identity;
using Azure.Monitor.OpenTelemetry.Exporter;
using Microsoft.Agents.AI;
using OpenAI;
using OpenTelemetry;
@@ -11,22 +12,24 @@ using OpenTelemetry.Trace;
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";
const string JokerName = "Joker";
const string JokerInstructions = "You are good at telling jokes.";
var applicationInsightsConnectionString = Environment.GetEnvironmentVariable("APPLICATIONINSIGHTS_CONNECTION_STRING");
// Create TracerProvider with console exporter
// This will output the telemetry data to the console.
string sourceName = Guid.NewGuid().ToString("N");
using var tracerProvider = Sdk.CreateTracerProviderBuilder()
var tracerProviderBuilder = Sdk.CreateTracerProviderBuilder()
.AddSource(sourceName)
.AddConsoleExporter()
.Build();
.AddConsoleExporter();
if (!string.IsNullOrWhiteSpace(applicationInsightsConnectionString))
{
tracerProviderBuilder.AddAzureMonitorTraceExporter(options => options.ConnectionString = applicationInsightsConnectionString);
}
using var tracerProvider = tracerProviderBuilder.Build();
// Create the agent, and enable OpenTelemetry instrumentation.
AIAgent agent = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential())
.GetChatClient(deploymentName)
.CreateAIAgent(JokerInstructions, JokerName)
.CreateAIAgent(instructions: "You are good at telling jokes.", name: "Joker")
.AsBuilder()
.UseOpenTelemetry(sourceName: sourceName)
.Build();
@@ -18,9 +18,8 @@ var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
// Add agent options to the service collection.
const string JokerName = "Joker";
const string JokerInstructions = "You are good at telling jokes.";
builder.Services.AddSingleton(new ChatClientAgentOptions(JokerInstructions, JokerName));
builder.Services.AddSingleton(
new ChatClientAgentOptions(instructions: "You are good at telling jokes.", name: "Joker"));
// Add a chat client to the service collection.
builder.Services.AddKeyedChatClient("AzureOpenAI", (sp) => new AzureOpenAIClient(
@@ -12,18 +12,14 @@ using ModelContextProtocol.Server;
var endpoint = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_FOUNDRY_PROJECT_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
const string JokerName = "Joker";
const string JokerDescription = "An agent that tells jokes.";
const string JokerInstructions = "You are good at telling jokes, and you always start each joke with 'Aye aye, captain!'.";
var persistentAgentsClient = new PersistentAgentsClient(endpoint, new AzureCliCredential());
// Create a server side persistent agent
var agentMetadata = await persistentAgentsClient.Administration.CreateAgentAsync(
model: deploymentName,
name: JokerName,
description: JokerDescription,
instructions: JokerInstructions);
instructions: "You are good at telling jokes, and you always start each joke with 'Aye aye, captain!'.",
name: "Joker",
description: "An agent that tells jokes.");
// Retrieve the server side persistent agent as an AIAgent.
AIAgent agent = await persistentAgentsClient.GetAIAgentAsync(agentMetadata.Value.Id);
@@ -31,8 +31,8 @@ AIAgent weatherAgent = new AzureOpenAIClient(
AIAgent agent = new AzureOpenAIClient(
new Uri(endpoint),
new AzureCliCredential())
.GetChatClient(deploymentName)
.CreateAIAgent(instructions: "You are a helpful assistant who responds in French.", tools: [weatherAgent.AsAIFunction()]);
.GetChatClient(deploymentName)
.CreateAIAgent(instructions: "You are a helpful assistant who responds in French.", tools: [weatherAgent.AsAIFunction()]);
// Invoke the agent and output the text result.
Console.WriteLine(await agent.RunAsync("What is the weather like in Amsterdam?"));
@@ -27,16 +27,13 @@ services.AddSingleton<AgentPlugin>(); // The plugin depends on WeatherProvider a
IServiceProvider serviceProvider = services.BuildServiceProvider();
const string AgentName = "Assistant";
const string AgentInstructions = "You are a helpful assistant that helps people find information.";
AIAgent agent = new AzureOpenAIClient(
new Uri(endpoint),
new AzureCliCredential())
.GetChatClient(deploymentName)
.CreateAIAgent(
instructions: AgentInstructions,
name: AgentName,
.GetChatClient(deploymentName)
.CreateAIAgent(
instructions: "You are a helpful assistant that helps people find information.",
name: "Assistant",
tools: [.. serviceProvider.GetRequiredService<AgentPlugin>().AsAITools()],
services: serviceProvider); // Pass the service provider to the agent so it will be available to plugin functions to resolve dependencies.
@@ -14,20 +14,17 @@ using OpenAI;
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";
const string JokerName = "Joker";
const string JokerInstructions = "You are good at telling jokes.";
// Construct the agent, and provide a factory to create an in-memory chat message store with a reducer that keeps only the last 2 non-system messages.
AIAgent agent = new AzureOpenAIClient(
new Uri(endpoint),
new AzureCliCredential())
.GetChatClient(deploymentName)
.CreateAIAgent(new ChatClientAgentOptions
{
Name = JokerName,
Instructions = JokerInstructions,
ChatMessageStoreFactory = ctx => new InMemoryChatMessageStore(new MessageCountingChatReducer(2), ctx.SerializedState, ctx.JsonSerializerOptions)
});
.GetChatClient(deploymentName)
.CreateAIAgent(new ChatClientAgentOptions
{
Instructions = "You are good at telling jokes.",
Name = "Joker",
ChatMessageStoreFactory = ctx => new InMemoryChatMessageStore(new MessageCountingChatReducer(2), ctx.SerializedState, ctx.JsonSerializerOptions)
});
AgentThread thread = agent.GetNewThread();
@@ -9,9 +9,6 @@ using Microsoft.Agents.AI;
var endpoint = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_FOUNDRY_PROJECT_ENDPOINT is not set.");
var model = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_MODEL_ID") ?? "gpt-4.1-mini";
const string AgentName = "MicrosoftLearnAgent";
const string AgentInstructions = "You answer questions by searching the Microsoft Learn content only.";
// Get a client to create/retrieve server side agents with.
var persistentAgentsClient = new PersistentAgentsClient(endpoint, new AzureCliCredential());
@@ -24,8 +21,8 @@ mcpTool.AllowedTools.Add("microsoft_docs_search");
// Create a server side persistent agent with the Azure.AI.Agents.Persistent SDK.
var agentMetadata = await persistentAgentsClient.Administration.CreateAgentAsync(
model: model,
name: AgentName,
instructions: AgentInstructions,
name: "MicrosoftLearnAgent",
instructions: "You answer questions by searching the Microsoft Learn content only.",
tools: [mcpTool]);
// Retrieve an already created server side persistent agent as an AIAgent.
@@ -134,17 +134,17 @@ internal sealed class SloganWriterExecutor
this._thread = this._agent.GetNewThread();
}
public async ValueTask<SloganResult> HandleAsync(string message, IWorkflowContext context)
public async ValueTask<SloganResult> HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
var result = await this._agent.RunAsync(message, this._thread);
var result = await this._agent.RunAsync(message, this._thread, cancellationToken: cancellationToken);
var sloganResult = JsonSerializer.Deserialize<SloganResult>(result.Text) ?? throw new InvalidOperationException("Failed to deserialize slogan result.");
await context.AddEventAsync(new SloganGeneratedEvent(sloganResult));
await context.AddEventAsync(new SloganGeneratedEvent(sloganResult), cancellationToken);
return sloganResult;
}
public async ValueTask<SloganResult> HandleAsync(FeedbackResult message, IWorkflowContext context)
public async ValueTask<SloganResult> HandleAsync(FeedbackResult message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
var feedbackMessage = $"""
Here is the feedback on your previous slogan:
@@ -155,10 +155,10 @@ internal sealed class SloganWriterExecutor
Please use this feedback to improve your slogan.
""";
var result = await this._agent.RunAsync(feedbackMessage, this._thread);
var result = await this._agent.RunAsync(feedbackMessage, this._thread, cancellationToken: cancellationToken);
var sloganResult = JsonSerializer.Deserialize<SloganResult>(result.Text) ?? throw new InvalidOperationException("Failed to deserialize slogan result.");
await context.AddEventAsync(new SloganGeneratedEvent(sloganResult));
await context.AddEventAsync(new SloganGeneratedEvent(sloganResult), cancellationToken);
return sloganResult;
}
}
@@ -205,7 +205,7 @@ internal sealed class FeedbackExecutor : ReflectingExecutor<FeedbackExecutor>, I
this._thread = this._agent.GetNewThread();
}
public async ValueTask HandleAsync(SloganResult message, IWorkflowContext context)
public async ValueTask HandleAsync(SloganResult message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
var sloganMessage = $"""
Here is a slogan for the task '{message.Task}':
@@ -213,24 +213,24 @@ internal sealed class FeedbackExecutor : ReflectingExecutor<FeedbackExecutor>, I
Please provide feedback on this slogan, including comments, a rating from 1 to 10, and suggested actions for improvement.
""";
var response = await this._agent.RunAsync(sloganMessage, this._thread);
var response = await this._agent.RunAsync(sloganMessage, this._thread, cancellationToken: cancellationToken);
var feedback = JsonSerializer.Deserialize<FeedbackResult>(response.Text) ?? throw new InvalidOperationException("Failed to deserialize feedback.");
await context.AddEventAsync(new FeedbackEvent(feedback));
await context.AddEventAsync(new FeedbackEvent(feedback), cancellationToken);
if (feedback.Rating >= this.MinimumRating)
{
await context.YieldOutputAsync($"The following slogan was accepted:\n\n{message.Slogan}");
await context.YieldOutputAsync($"The following slogan was accepted:\n\n{message.Slogan}", cancellationToken);
return;
}
if (this._attempts >= this.MaxAttempts)
{
await context.YieldOutputAsync($"The slogan was rejected after {this.MaxAttempts} attempts. Final slogan:\n\n{message.Slogan}");
await context.YieldOutputAsync($"The slogan was rejected after {this.MaxAttempts} attempts. Final slogan:\n\n{message.Slogan}", cancellationToken);
return;
}
await context.SendMessageAsync(feedback);
await context.SendMessageAsync(feedback, cancellationToken: cancellationToken);
this._attempts++;
}
}
@@ -51,13 +51,15 @@ internal static class WorkflowHelper
/// </summary>
/// <param name="message">The user message to process</param>
/// <param name="context">Workflow context for accessing workflow services and adding events</param>
public async ValueTask HandleAsync(List<ChatMessage> message, IWorkflowContext context)
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
/// The default is <see cref="CancellationToken.None"/>.</param>
public async ValueTask HandleAsync(List<ChatMessage> message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
// Broadcast the message to all connected agents. Receiving agents will queue
// the message but will not start processing until they receive a turn token.
await context.SendMessageAsync(message);
await context.SendMessageAsync(message, cancellationToken: cancellationToken);
// Broadcast the turn token to kick off the agents.
await context.SendMessageAsync(new TurnToken(emitEvents: true));
await context.SendMessageAsync(new TurnToken(emitEvents: true), cancellationToken: cancellationToken);
}
}
@@ -75,14 +77,16 @@ internal static class WorkflowHelper
/// </summary>
/// <param name="message">The message from the agent</param>
/// <param name="context">Workflow context for accessing workflow services and adding events</param>
public async ValueTask HandleAsync(ChatMessage message, IWorkflowContext context)
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
/// The default is <see cref="CancellationToken.None"/>.</param>
public async ValueTask HandleAsync(ChatMessage message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
this._messages.Add(message);
if (this._messages.Count == 2)
{
var formattedMessages = string.Join(Environment.NewLine, this._messages.Select(m => $"{m.Text}"));
await context.YieldOutputAsync(formattedMessages);
await context.YieldOutputAsync(formattedMessages, cancellationToken);
}
}
}
@@ -69,20 +69,20 @@ internal sealed class GuessNumberExecutor() : ReflectingExecutor<GuessNumberExec
private int NextGuess => (this.LowerBound + this.UpperBound) / 2;
public async ValueTask HandleAsync(NumberSignal message, IWorkflowContext context)
public async ValueTask HandleAsync(NumberSignal message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
switch (message)
{
case NumberSignal.Init:
await context.SendMessageAsync(this.NextGuess).ConfigureAwait(false);
await context.SendMessageAsync(this.NextGuess, cancellationToken: cancellationToken).ConfigureAwait(false);
break;
case NumberSignal.Above:
this.UpperBound = this.NextGuess - 1;
await context.SendMessageAsync(this.NextGuess).ConfigureAwait(false);
await context.SendMessageAsync(this.NextGuess, cancellationToken: cancellationToken).ConfigureAwait(false);
break;
case NumberSignal.Below:
this.LowerBound = this.NextGuess + 1;
await context.SendMessageAsync(this.NextGuess).ConfigureAwait(false);
await context.SendMessageAsync(this.NextGuess, cancellationToken: cancellationToken).ConfigureAwait(false);
break;
}
}
@@ -92,14 +92,14 @@ internal sealed class GuessNumberExecutor() : ReflectingExecutor<GuessNumberExec
/// This must be overridden to save any state that is needed to resume the executor.
/// </summary>
protected override ValueTask OnCheckpointingAsync(IWorkflowContext context, CancellationToken cancellationToken = default) =>
context.QueueStateUpdateAsync(StateKey, (this.LowerBound, this.UpperBound));
context.QueueStateUpdateAsync(StateKey, (this.LowerBound, this.UpperBound), cancellationToken: cancellationToken);
/// <summary>
/// Restore the state of the executor from a checkpoint.
/// This must be overridden to restore any state that was saved during checkpointing.
/// </summary>
protected override async ValueTask OnCheckpointRestoredAsync(IWorkflowContext context, CancellationToken cancellationToken = default) =>
(this.LowerBound, this.UpperBound) = await context.ReadStateAsync<(int, int)>(StateKey).ConfigureAwait(false);
(this.LowerBound, this.UpperBound) = await context.ReadStateAsync<(int, int)>(StateKey, cancellationToken: cancellationToken).ConfigureAwait(false);
}
/// <summary>
@@ -120,20 +120,20 @@ internal sealed class JudgeExecutor() : ReflectingExecutor<JudgeExecutor>("Judge
this._targetNumber = targetNumber;
}
public async ValueTask HandleAsync(int message, IWorkflowContext context)
public async ValueTask HandleAsync(int message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
this._tries++;
if (message == this._targetNumber)
{
await context.YieldOutputAsync($"{this._targetNumber} found in {this._tries} tries!").ConfigureAwait(false);
await context.YieldOutputAsync($"{this._targetNumber} found in {this._tries} tries!", cancellationToken: cancellationToken).ConfigureAwait(false);
}
else if (message < this._targetNumber)
{
await context.SendMessageAsync(NumberSignal.Below).ConfigureAwait(false);
await context.SendMessageAsync(NumberSignal.Below, cancellationToken: cancellationToken).ConfigureAwait(false);
}
else
{
await context.SendMessageAsync(NumberSignal.Above).ConfigureAwait(false);
await context.SendMessageAsync(NumberSignal.Above, cancellationToken: cancellationToken).ConfigureAwait(false);
}
}
@@ -142,12 +142,12 @@ internal sealed class JudgeExecutor() : ReflectingExecutor<JudgeExecutor>("Judge
/// This must be overridden to save any state that is needed to resume the executor.
/// </summary>
protected override ValueTask OnCheckpointingAsync(IWorkflowContext context, CancellationToken cancellationToken = default) =>
context.QueueStateUpdateAsync(StateKey, this._tries);
context.QueueStateUpdateAsync(StateKey, this._tries, cancellationToken: cancellationToken);
/// <summary>
/// Restore the state of the executor from a checkpoint.
/// This must be overridden to restore any state that was saved during checkpointing.
/// </summary>
protected override async ValueTask OnCheckpointRestoredAsync(IWorkflowContext context, CancellationToken cancellationToken = default) =>
this._tries = await context.ReadStateAsync<int>(StateKey).ConfigureAwait(false);
this._tries = await context.ReadStateAsync<int>(StateKey, cancellationToken: cancellationToken).ConfigureAwait(false);
}
@@ -69,20 +69,20 @@ internal sealed class GuessNumberExecutor() : ReflectingExecutor<GuessNumberExec
private int NextGuess => (this.LowerBound + this.UpperBound) / 2;
public async ValueTask HandleAsync(NumberSignal message, IWorkflowContext context)
public async ValueTask HandleAsync(NumberSignal message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
switch (message)
{
case NumberSignal.Init:
await context.SendMessageAsync(this.NextGuess).ConfigureAwait(false);
await context.SendMessageAsync(this.NextGuess, cancellationToken: cancellationToken).ConfigureAwait(false);
break;
case NumberSignal.Above:
this.UpperBound = this.NextGuess - 1;
await context.SendMessageAsync(this.NextGuess).ConfigureAwait(false);
await context.SendMessageAsync(this.NextGuess, cancellationToken: cancellationToken).ConfigureAwait(false);
break;
case NumberSignal.Below:
this.LowerBound = this.NextGuess + 1;
await context.SendMessageAsync(this.NextGuess).ConfigureAwait(false);
await context.SendMessageAsync(this.NextGuess, cancellationToken: cancellationToken).ConfigureAwait(false);
break;
}
}
@@ -92,14 +92,14 @@ internal sealed class GuessNumberExecutor() : ReflectingExecutor<GuessNumberExec
/// This must be overridden to save any state that is needed to resume the executor.
/// </summary>
protected override ValueTask OnCheckpointingAsync(IWorkflowContext context, CancellationToken cancellationToken = default) =>
context.QueueStateUpdateAsync(StateKey, (this.LowerBound, this.UpperBound));
context.QueueStateUpdateAsync(StateKey, (this.LowerBound, this.UpperBound), cancellationToken: cancellationToken);
/// <summary>
/// Restore the state of the executor from a checkpoint.
/// This must be overridden to restore any state that was saved during checkpointing.
/// </summary>
protected override async ValueTask OnCheckpointRestoredAsync(IWorkflowContext context, CancellationToken cancellationToken = default) =>
(this.LowerBound, this.UpperBound) = await context.ReadStateAsync<(int, int)>(StateKey).ConfigureAwait(false);
(this.LowerBound, this.UpperBound) = await context.ReadStateAsync<(int, int)>(StateKey, cancellationToken: cancellationToken).ConfigureAwait(false);
}
/// <summary>
@@ -120,20 +120,20 @@ internal sealed class JudgeExecutor() : ReflectingExecutor<JudgeExecutor>("Judge
this._targetNumber = targetNumber;
}
public async ValueTask HandleAsync(int message, IWorkflowContext context)
public async ValueTask HandleAsync(int message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
this._tries++;
if (message == this._targetNumber)
{
await context.YieldOutputAsync($"{this._targetNumber} found in {this._tries} tries!").ConfigureAwait(false);
await context.YieldOutputAsync($"{this._targetNumber} found in {this._tries} tries!", cancellationToken).ConfigureAwait(false);
}
else if (message < this._targetNumber)
{
await context.SendMessageAsync(NumberSignal.Below).ConfigureAwait(false);
await context.SendMessageAsync(NumberSignal.Below, cancellationToken: cancellationToken).ConfigureAwait(false);
}
else
{
await context.SendMessageAsync(NumberSignal.Above).ConfigureAwait(false);
await context.SendMessageAsync(NumberSignal.Above, cancellationToken: cancellationToken).ConfigureAwait(false);
}
}
@@ -142,12 +142,12 @@ internal sealed class JudgeExecutor() : ReflectingExecutor<JudgeExecutor>("Judge
/// This must be overridden to save any state that is needed to resume the executor.
/// </summary>
protected override ValueTask OnCheckpointingAsync(IWorkflowContext context, CancellationToken cancellationToken = default) =>
context.QueueStateUpdateAsync(StateKey, this._tries);
context.QueueStateUpdateAsync(StateKey, this._tries, cancellationToken: cancellationToken);
/// <summary>
/// Restore the state of the executor from a checkpoint.
/// This must be overridden to restore any state that was saved during checkpointing.
/// </summary>
protected override async ValueTask OnCheckpointRestoredAsync(IWorkflowContext context, CancellationToken cancellationToken = default) =>
this._tries = await context.ReadStateAsync<int>(StateKey).ConfigureAwait(false);
this._tries = await context.ReadStateAsync<int>(StateKey, cancellationToken: cancellationToken).ConfigureAwait(false);
}
@@ -69,21 +69,21 @@ internal sealed class JudgeExecutor() : ReflectingExecutor<JudgeExecutor>("Judge
this._targetNumber = targetNumber;
}
public async ValueTask HandleAsync(int message, IWorkflowContext context)
public async ValueTask HandleAsync(int message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
this._tries++;
if (message == this._targetNumber)
{
await context.YieldOutputAsync($"{this._targetNumber} found in {this._tries} tries!")
await context.YieldOutputAsync($"{this._targetNumber} found in {this._tries} tries!", cancellationToken)
.ConfigureAwait(false);
}
else if (message < this._targetNumber)
{
await context.SendMessageAsync(new SignalWithNumber(NumberSignal.Below, message)).ConfigureAwait(false);
await context.SendMessageAsync(new SignalWithNumber(NumberSignal.Below, message), cancellationToken: cancellationToken).ConfigureAwait(false);
}
else
{
await context.SendMessageAsync(new SignalWithNumber(NumberSignal.Above, message)).ConfigureAwait(false);
await context.SendMessageAsync(new SignalWithNumber(NumberSignal.Above, message), cancellationToken: cancellationToken).ConfigureAwait(false);
}
}
@@ -92,12 +92,12 @@ internal sealed class JudgeExecutor() : ReflectingExecutor<JudgeExecutor>("Judge
/// This must be overridden to save any state that is needed to resume the executor.
/// </summary>
protected override ValueTask OnCheckpointingAsync(IWorkflowContext context, CancellationToken cancellationToken = default) =>
context.QueueStateUpdateAsync(StateKey, this._tries);
context.QueueStateUpdateAsync(StateKey, this._tries, cancellationToken: cancellationToken);
/// <summary>
/// Restore the state of the executor from a checkpoint.
/// This must be overridden to restore any state that was saved during checkpointing.
/// </summary>
protected override async ValueTask OnCheckpointRestoredAsync(IWorkflowContext context, CancellationToken cancellationToken = default) =>
this._tries = await context.ReadStateAsync<int>(StateKey).ConfigureAwait(false);
this._tries = await context.ReadStateAsync<int>(StateKey, cancellationToken: cancellationToken).ConfigureAwait(false);
}
@@ -82,14 +82,16 @@ internal sealed class ConcurrentStartExecutor() :
/// </summary>
/// <param name="message">The user message to process</param>
/// <param name="context">Workflow context for accessing workflow services and adding events</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
/// The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A task representing the asynchronous operation</returns>
public async ValueTask HandleAsync(string message, IWorkflowContext context)
public async ValueTask HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
// Broadcast the message to all connected agents. Receiving agents will queue
// the message but will not start processing until they receive a turn token.
await context.SendMessageAsync(new ChatMessage(ChatRole.User, message));
await context.SendMessageAsync(new ChatMessage(ChatRole.User, message), cancellationToken: cancellationToken);
// Broadcast the turn token to kick off the agents.
await context.SendMessageAsync(new TurnToken(emitEvents: true));
await context.SendMessageAsync(new TurnToken(emitEvents: true), cancellationToken: cancellationToken);
}
}
@@ -107,15 +109,17 @@ internal sealed class ConcurrentAggregationExecutor() :
/// </summary>
/// <param name="message">The message from the agent</param>
/// <param name="context">Workflow context for accessing workflow services and adding events</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
/// The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A task representing the asynchronous operation</returns>
public async ValueTask HandleAsync(ChatMessage message, IWorkflowContext context)
public async ValueTask HandleAsync(ChatMessage message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
this._messages.Add(message);
if (this._messages.Count == 2)
{
var formattedMessages = string.Join(Environment.NewLine, this._messages.Select(m => $"{m.AuthorName}: {m.Text}"));
await context.YieldOutputAsync(formattedMessages);
await context.YieldOutputAsync(formattedMessages, cancellationToken);
}
}
}
@@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Agents.AI.Workflows.Reflection;
@@ -138,7 +139,7 @@ internal sealed class Split(string[] mapperIds, string id) :
/// <summary>
/// Tokenize input and assign contiguous index ranges to each mapper via shared state.
/// </summary>
public async ValueTask HandleAsync(string message, IWorkflowContext context)
public async ValueTask HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
// Ensure temp directory exists
Directory.CreateDirectory(MapReduceConstants.TempDir);
@@ -147,7 +148,7 @@ internal sealed class Split(string[] mapperIds, string id) :
var wordList = Preprocess(message);
// Store the tokenized words once so that all mappers can read by index
await context.QueueStateUpdateAsync(MapReduceConstants.DataToProcessKey, wordList, scopeName: MapReduceConstants.StateScope);
await context.QueueStateUpdateAsync(MapReduceConstants.DataToProcessKey, wordList, scopeName: MapReduceConstants.StateScope, cancellationToken);
// Divide indices into contiguous slices for each mapper
var mapperCount = this._mapperIds.Length;
@@ -160,10 +161,10 @@ internal sealed class Split(string[] mapperIds, string id) :
var endIndex = i < mapperCount - 1 ? startIndex + chunkSize : wordList.Length;
// Save the indices under the mapper's Id
await context.QueueStateUpdateAsync(this._mapperIds[i], (startIndex, endIndex), scopeName: MapReduceConstants.StateScope);
await context.QueueStateUpdateAsync(this._mapperIds[i], (startIndex, endIndex), scopeName: MapReduceConstants.StateScope, cancellationToken);
// Notify the mapper that data is ready
await context.SendMessageAsync(new SplitComplete(), targetId: this._mapperIds[i]);
await context.SendMessageAsync(new SplitComplete(), targetId: this._mapperIds[i], cancellationToken);
}
// Process all the chunks
@@ -192,10 +193,10 @@ internal sealed class Mapper(string id) : ReflectingExecutor<Mapper>(id), IMessa
/// <summary>
/// Read the assigned slice, emit (word, 1) pairs, and persist to disk.
/// </summary>
public async ValueTask HandleAsync(SplitComplete message, IWorkflowContext context)
public async ValueTask HandleAsync(SplitComplete message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
var dataToProcess = await context.ReadStateAsync<string[]>(MapReduceConstants.DataToProcessKey, scopeName: MapReduceConstants.StateScope);
var chunk = await context.ReadStateAsync<(int start, int end)>(this.Id, scopeName: MapReduceConstants.StateScope);
var dataToProcess = await context.ReadStateAsync<string[]>(MapReduceConstants.DataToProcessKey, scopeName: MapReduceConstants.StateScope, cancellationToken);
var chunk = await context.ReadStateAsync<(int start, int end)>(this.Id, scopeName: MapReduceConstants.StateScope, cancellationToken);
var results = dataToProcess![chunk.start..chunk.end]
.Select(word => (word, 1))
@@ -204,9 +205,9 @@ internal sealed class Mapper(string id) : ReflectingExecutor<Mapper>(id), IMessa
// Write this mapper's results as simple text lines for easy debugging
var filePath = Path.Combine(MapReduceConstants.TempDir, $"map_results_{this.Id}.txt");
var lines = results.Select(r => $"{r.word}: {r.Item2}");
await File.WriteAllLinesAsync(filePath, lines);
await File.WriteAllLinesAsync(filePath, lines, cancellationToken);
await context.SendMessageAsync(new MapComplete(filePath));
await context.SendMessageAsync(new MapComplete(filePath), cancellationToken: cancellationToken);
}
}
@@ -224,7 +225,7 @@ internal sealed class Shuffler(string[] reducerIds, string[] mapperIds, string i
/// <summary>
/// Aggregate mapper outputs and write one partition file per reducer.
/// </summary>
public async ValueTask HandleAsync(MapComplete message, IWorkflowContext context)
public async ValueTask HandleAsync(MapComplete message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
this._mapResults.Add(message);
@@ -241,9 +242,9 @@ internal sealed class Shuffler(string[] reducerIds, string[] mapperIds, string i
// Write one grouped partition for reducer index and notify that reducer
var filePath = Path.Combine(MapReduceConstants.TempDir, $"shuffle_results_{index}.txt");
var lines = chunk.Select(kvp => $"{kvp.key}: {JsonSerializer.Serialize(kvp.values)}");
await File.WriteAllLinesAsync(filePath, lines);
await File.WriteAllLinesAsync(filePath, lines, cancellationToken);
await context.SendMessageAsync(new ShuffleComplete(filePath, this._reducerIds[index]));
await context.SendMessageAsync(new ShuffleComplete(filePath, this._reducerIds[index]), cancellationToken: cancellationToken);
}
var tasks = chunks.Select((chunk, i) => ProcessChunkAsync(chunk, i));
@@ -318,7 +319,7 @@ internal sealed class Reducer(string id) : ReflectingExecutor<Reducer>(id), IMes
/// <summary>
/// Read one shuffle partition and reduce it to totals.
/// </summary>
public async ValueTask HandleAsync(ShuffleComplete message, IWorkflowContext context)
public async ValueTask HandleAsync(ShuffleComplete message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
if (message.ReducerId != this.Id)
{
@@ -327,7 +328,7 @@ internal sealed class Reducer(string id) : ReflectingExecutor<Reducer>(id), IMes
}
// Read grouped values from the shuffle output
var lines = await File.ReadAllLinesAsync(message.FilePath);
var lines = await File.ReadAllLinesAsync(message.FilePath, cancellationToken);
// Sum values per key. Values are serialized JSON arrays like [1, 1, ...]
var reducedResults = new Dictionary<string, int>();
@@ -345,9 +346,9 @@ internal sealed class Reducer(string id) : ReflectingExecutor<Reducer>(id), IMes
// Persist our partition totals
var filePath = Path.Combine(MapReduceConstants.TempDir, $"reduced_results_{this.Id}.txt");
var outputLines = reducedResults.Select(kvp => $"{kvp.Key}: {kvp.Value}");
await File.WriteAllLinesAsync(filePath, outputLines);
await File.WriteAllLinesAsync(filePath, outputLines, cancellationToken);
await context.SendMessageAsync(new ReduceComplete(filePath));
await context.SendMessageAsync(new ReduceComplete(filePath), cancellationToken: cancellationToken);
}
}
@@ -361,10 +362,10 @@ internal sealed class CompletionExecutor(string id) :
/// <summary>
/// Collect reducer output file paths and yield final output.
/// </summary>
public async ValueTask HandleAsync(List<ReduceComplete> message, IWorkflowContext context)
public async ValueTask HandleAsync(List<ReduceComplete> message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
var filePaths = message.ConvertAll(r => r.FilePath);
await context.YieldOutputAsync(filePaths);
await context.YieldOutputAsync(filePaths, cancellationToken);
}
}
@@ -160,7 +160,7 @@ internal sealed class SpamDetectionExecutor : ReflectingExecutor<SpamDetectionEx
this._spamDetectionAgent = spamDetectionAgent;
}
public async ValueTask<DetectionResult> HandleAsync(ChatMessage message, IWorkflowContext context)
public async ValueTask<DetectionResult> HandleAsync(ChatMessage message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
// Generate a random email ID and store the email content to the shared state
var newEmail = new Email
@@ -168,10 +168,10 @@ internal sealed class SpamDetectionExecutor : ReflectingExecutor<SpamDetectionEx
EmailId = Guid.NewGuid().ToString("N"),
EmailContent = message.Text
};
await context.QueueStateUpdateAsync(newEmail.EmailId, newEmail, scopeName: EmailStateConstants.EmailStateScope);
await context.QueueStateUpdateAsync(newEmail.EmailId, newEmail, scopeName: EmailStateConstants.EmailStateScope, cancellationToken);
// Invoke the agent
var response = await this._spamDetectionAgent.RunAsync(message);
var response = await this._spamDetectionAgent.RunAsync(message, cancellationToken: cancellationToken);
var detectionResult = JsonSerializer.Deserialize<DetectionResult>(response.Text);
detectionResult!.EmailId = newEmail.EmailId;
@@ -205,7 +205,7 @@ internal sealed class EmailAssistantExecutor : ReflectingExecutor<EmailAssistant
this._emailAssistantAgent = emailAssistantAgent;
}
public async ValueTask<EmailResponse> HandleAsync(DetectionResult message, IWorkflowContext context)
public async ValueTask<EmailResponse> HandleAsync(DetectionResult message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
if (message.IsSpam)
{
@@ -213,11 +213,11 @@ internal sealed class EmailAssistantExecutor : ReflectingExecutor<EmailAssistant
}
// Retrieve the email content from the shared state
var email = await context.ReadStateAsync<Email>(message.EmailId, scopeName: EmailStateConstants.EmailStateScope)
var email = await context.ReadStateAsync<Email>(message.EmailId, scopeName: EmailStateConstants.EmailStateScope, cancellationToken)
?? throw new InvalidOperationException("Email not found.");
// Invoke the agent
var response = await this._emailAssistantAgent.RunAsync(email.EmailContent);
var response = await this._emailAssistantAgent.RunAsync(email.EmailContent, cancellationToken: cancellationToken);
var emailResponse = JsonSerializer.Deserialize<EmailResponse>(response.Text);
return emailResponse!;
@@ -232,8 +232,8 @@ internal sealed class SendEmailExecutor() : ReflectingExecutor<SendEmailExecutor
/// <summary>
/// Simulate the sending of an email.
/// </summary>
public async ValueTask HandleAsync(EmailResponse message, IWorkflowContext context) =>
await context.YieldOutputAsync($"Email sent: {message.Response}");
public async ValueTask HandleAsync(EmailResponse message, IWorkflowContext context, CancellationToken cancellationToken = default) =>
await context.YieldOutputAsync($"Email sent: {message.Response}", cancellationToken);
}
/// <summary>
@@ -244,11 +244,11 @@ internal sealed class HandleSpamExecutor() : ReflectingExecutor<HandleSpamExecut
/// <summary>
/// Simulate the handling of a spam message.
/// </summary>
public async ValueTask HandleAsync(DetectionResult message, IWorkflowContext context)
public async ValueTask HandleAsync(DetectionResult message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
if (message.IsSpam)
{
await context.YieldOutputAsync($"Email marked as spam: {message.Reason}");
await context.YieldOutputAsync($"Email marked as spam: {message.Reason}", cancellationToken);
}
else
{
@@ -185,7 +185,7 @@ internal sealed class SpamDetectionExecutor : ReflectingExecutor<SpamDetectionEx
this._spamDetectionAgent = spamDetectionAgent;
}
public async ValueTask<DetectionResult> HandleAsync(ChatMessage message, IWorkflowContext context)
public async ValueTask<DetectionResult> HandleAsync(ChatMessage message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
// Generate a random email ID and store the email content
var newEmail = new Email
@@ -193,10 +193,10 @@ internal sealed class SpamDetectionExecutor : ReflectingExecutor<SpamDetectionEx
EmailId = Guid.NewGuid().ToString("N"),
EmailContent = message.Text
};
await context.QueueStateUpdateAsync(newEmail.EmailId, newEmail, scopeName: EmailStateConstants.EmailStateScope);
await context.QueueStateUpdateAsync(newEmail.EmailId, newEmail, scopeName: EmailStateConstants.EmailStateScope, cancellationToken);
// Invoke the agent
var response = await this._spamDetectionAgent.RunAsync(message);
var response = await this._spamDetectionAgent.RunAsync(message, cancellationToken: cancellationToken);
var detectionResult = JsonSerializer.Deserialize<DetectionResult>(response.Text);
detectionResult!.EmailId = newEmail.EmailId;
@@ -230,7 +230,7 @@ internal sealed class EmailAssistantExecutor : ReflectingExecutor<EmailAssistant
this._emailAssistantAgent = emailAssistantAgent;
}
public async ValueTask<EmailResponse> HandleAsync(DetectionResult message, IWorkflowContext context)
public async ValueTask<EmailResponse> HandleAsync(DetectionResult message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
if (message.spamDecision == SpamDecision.Spam)
{
@@ -238,10 +238,10 @@ internal sealed class EmailAssistantExecutor : ReflectingExecutor<EmailAssistant
}
// Retrieve the email content from the context
var email = await context.ReadStateAsync<Email>(message.EmailId, scopeName: EmailStateConstants.EmailStateScope);
var email = await context.ReadStateAsync<Email>(message.EmailId, scopeName: EmailStateConstants.EmailStateScope, cancellationToken);
// Invoke the agent
var response = await this._emailAssistantAgent.RunAsync(email!.EmailContent);
var response = await this._emailAssistantAgent.RunAsync(email!.EmailContent, cancellationToken: cancellationToken);
var emailResponse = JsonSerializer.Deserialize<EmailResponse>(response.Text);
return emailResponse!;
@@ -256,8 +256,8 @@ internal sealed class SendEmailExecutor() : ReflectingExecutor<SendEmailExecutor
/// <summary>
/// Simulate the sending of an email.
/// </summary>
public async ValueTask HandleAsync(EmailResponse message, IWorkflowContext context) =>
await context.YieldOutputAsync($"Email sent: {message.Response}").ConfigureAwait(false);
public async ValueTask HandleAsync(EmailResponse message, IWorkflowContext context, CancellationToken cancellationToken = default) =>
await context.YieldOutputAsync($"Email sent: {message.Response}", cancellationToken).ConfigureAwait(false);
}
/// <summary>
@@ -268,11 +268,11 @@ internal sealed class HandleSpamExecutor() : ReflectingExecutor<HandleSpamExecut
/// <summary>
/// Simulate the handling of a spam message.
/// </summary>
public async ValueTask HandleAsync(DetectionResult message, IWorkflowContext context)
public async ValueTask HandleAsync(DetectionResult message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
if (message.spamDecision == SpamDecision.Spam)
{
await context.YieldOutputAsync($"Email marked as spam: {message.Reason}").ConfigureAwait(false);
await context.YieldOutputAsync($"Email marked as spam: {message.Reason}", cancellationToken).ConfigureAwait(false);
}
else
{
@@ -289,12 +289,12 @@ internal sealed class HandleUncertainExecutor() : ReflectingExecutor<HandleUncer
/// <summary>
/// Simulate the handling of an uncertain spam decision.
/// </summary>
public async ValueTask HandleAsync(DetectionResult message, IWorkflowContext context)
public async ValueTask HandleAsync(DetectionResult message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
if (message.spamDecision == SpamDecision.Uncertain)
{
var email = await context.ReadStateAsync<Email>(message.EmailId, scopeName: EmailStateConstants.EmailStateScope);
await context.YieldOutputAsync($"Email marked as uncertain: {message.Reason}. Email content: {email?.EmailContent}");
var email = await context.ReadStateAsync<Email>(message.EmailId, scopeName: EmailStateConstants.EmailStateScope, cancellationToken);
await context.YieldOutputAsync($"Email marked as uncertain: {message.Reason}. Email content: {email?.EmailContent}", cancellationToken);
}
else
{
@@ -241,7 +241,7 @@ internal sealed class EmailAnalysisExecutor : ReflectingExecutor<EmailAnalysisEx
this._emailAnalysisAgent = emailAnalysisAgent;
}
public async ValueTask<AnalysisResult> HandleAsync(ChatMessage message, IWorkflowContext context)
public async ValueTask<AnalysisResult> HandleAsync(ChatMessage message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
// Generate a random email ID and store the email content
var newEmail = new Email
@@ -249,10 +249,10 @@ internal sealed class EmailAnalysisExecutor : ReflectingExecutor<EmailAnalysisEx
EmailId = Guid.NewGuid().ToString("N"),
EmailContent = message.Text
};
await context.QueueStateUpdateAsync(newEmail.EmailId, newEmail, scopeName: EmailStateConstants.EmailStateScope);
await context.QueueStateUpdateAsync(newEmail.EmailId, newEmail, scopeName: EmailStateConstants.EmailStateScope, cancellationToken);
// Invoke the agent
var response = await this._emailAnalysisAgent.RunAsync(message);
var response = await this._emailAnalysisAgent.RunAsync(message, cancellationToken: cancellationToken);
var AnalysisResult = JsonSerializer.Deserialize<AnalysisResult>(response.Text);
AnalysisResult!.EmailId = newEmail.EmailId;
@@ -287,7 +287,7 @@ internal sealed class EmailAssistantExecutor : ReflectingExecutor<EmailAssistant
this._emailAssistantAgent = emailAssistantAgent;
}
public async ValueTask<EmailResponse> HandleAsync(AnalysisResult message, IWorkflowContext context)
public async ValueTask<EmailResponse> HandleAsync(AnalysisResult message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
if (message.spamDecision == SpamDecision.Spam)
{
@@ -295,10 +295,10 @@ internal sealed class EmailAssistantExecutor : ReflectingExecutor<EmailAssistant
}
// Retrieve the email content from the context
var email = await context.ReadStateAsync<Email>(message.EmailId, scopeName: EmailStateConstants.EmailStateScope);
var email = await context.ReadStateAsync<Email>(message.EmailId, scopeName: EmailStateConstants.EmailStateScope, cancellationToken);
// Invoke the agent
var response = await this._emailAssistantAgent.RunAsync(email!.EmailContent);
var response = await this._emailAssistantAgent.RunAsync(email!.EmailContent, cancellationToken: cancellationToken);
var emailResponse = JsonSerializer.Deserialize<EmailResponse>(response.Text);
return emailResponse!;
@@ -313,8 +313,8 @@ internal sealed class SendEmailExecutor() : ReflectingExecutor<SendEmailExecutor
/// <summary>
/// Simulate the sending of an email.
/// </summary>
public async ValueTask HandleAsync(EmailResponse message, IWorkflowContext context) =>
await context.YieldOutputAsync($"Email sent: {message.Response}");
public async ValueTask HandleAsync(EmailResponse message, IWorkflowContext context, CancellationToken cancellationToken = default) =>
await context.YieldOutputAsync($"Email sent: {message.Response}", cancellationToken);
}
/// <summary>
@@ -325,11 +325,11 @@ internal sealed class HandleSpamExecutor() : ReflectingExecutor<HandleSpamExecut
/// <summary>
/// Simulate the handling of a spam message.
/// </summary>
public async ValueTask HandleAsync(AnalysisResult message, IWorkflowContext context)
public async ValueTask HandleAsync(AnalysisResult message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
if (message.spamDecision == SpamDecision.Spam)
{
await context.YieldOutputAsync($"Email marked as spam: {message.Reason}");
await context.YieldOutputAsync($"Email marked as spam: {message.Reason}", cancellationToken);
}
else
{
@@ -346,12 +346,12 @@ internal sealed class HandleUncertainExecutor() : ReflectingExecutor<HandleUncer
/// <summary>
/// Simulate the handling of an uncertain spam decision.
/// </summary>
public async ValueTask HandleAsync(AnalysisResult message, IWorkflowContext context)
public async ValueTask HandleAsync(AnalysisResult message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
if (message.spamDecision == SpamDecision.Uncertain)
{
var email = await context.ReadStateAsync<Email>(message.EmailId, scopeName: EmailStateConstants.EmailStateScope);
await context.YieldOutputAsync($"Email marked as uncertain: {message.Reason}. Email content: {email?.EmailContent}");
var email = await context.ReadStateAsync<Email>(message.EmailId, scopeName: EmailStateConstants.EmailStateScope, cancellationToken);
await context.YieldOutputAsync($"Email marked as uncertain: {message.Reason}. Email content: {email?.EmailContent}", cancellationToken);
}
else
{
@@ -385,13 +385,13 @@ internal sealed class EmailSummaryExecutor : ReflectingExecutor<EmailSummaryExec
this._emailSummaryAgent = emailSummaryAgent;
}
public async ValueTask<AnalysisResult> HandleAsync(AnalysisResult message, IWorkflowContext context)
public async ValueTask<AnalysisResult> HandleAsync(AnalysisResult message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
// Read the email content from the shared states
var email = await context.ReadStateAsync<Email>(message.EmailId, scopeName: EmailStateConstants.EmailStateScope);
var email = await context.ReadStateAsync<Email>(message.EmailId, scopeName: EmailStateConstants.EmailStateScope, cancellationToken);
// Invoke the agent
var response = await this._emailSummaryAgent.RunAsync(email!.EmailContent);
var response = await this._emailSummaryAgent.RunAsync(email!.EmailContent, cancellationToken: cancellationToken);
var emailSummary = JsonSerializer.Deserialize<EmailSummary>(response.Text);
message.EmailSummary = emailSummary!.Summary;
@@ -410,17 +410,17 @@ internal sealed class DatabaseEvent(string message) : WorkflowEvent(message) { }
/// </summary>
internal sealed class DatabaseAccessExecutor() : ReflectingExecutor<DatabaseAccessExecutor>("DatabaseAccessExecutor"), IMessageHandler<AnalysisResult>
{
public async ValueTask HandleAsync(AnalysisResult message, IWorkflowContext context)
public async ValueTask HandleAsync(AnalysisResult message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
// 1. Save the email content
await context.ReadStateAsync<Email>(message.EmailId, scopeName: EmailStateConstants.EmailStateScope);
await Task.Delay(100); // Simulate database access delay
await context.ReadStateAsync<Email>(message.EmailId, scopeName: EmailStateConstants.EmailStateScope, cancellationToken);
await Task.Delay(100, cancellationToken); // Simulate database access delay
// 2. Save the analysis result
await Task.Delay(100); // Simulate database access delay
await Task.Delay(100, cancellationToken); // Simulate database access delay
// Not using the `WorkflowCompletedEvent` because this is not the end of the workflow.
// The end of the workflow is signaled by the `SendEmailExecutor` or the `HandleUnknownExecutor`.
await context.AddEventAsync(new DatabaseEvent($"Email {message.EmailId} saved to database."));
await context.AddEventAsync(new DatabaseEvent($"Email {message.EmailId} saved to database."), cancellationToken);
}
}
@@ -76,102 +76,103 @@ internal sealed class Program
string? messageId = null;
await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))
await foreach (WorkflowEvent workflowEvent in run.WatchStreamAsync().ConfigureAwait(false))
{
if (evt is ExecutorInvokedEvent executorInvoked)
switch (workflowEvent)
{
Debug.WriteLine($"STEP ENTER #{executorInvoked.ExecutorId}");
}
else if (evt is ExecutorCompletedEvent executorComplete)
{
Debug.WriteLine($"STEP EXIT #{executorComplete.ExecutorId}");
}
else if (evt is ExecutorFailedEvent executorFailure)
{
Debug.WriteLine($"STEP ERROR #{executorFailure.ExecutorId}: {executorFailure.Data?.Message ?? "Unknown"}");
}
else if (evt is WorkflowErrorEvent workflowError)
{
Debug.WriteLine("WORKFLOW ERROR");
}
else if (evt is ConversationUpdateEvent invokeEvent)
{
Debug.WriteLine($"CONVERSATION: {invokeEvent.Data}");
}
else if (evt is AgentRunUpdateEvent streamEvent)
{
if (!string.Equals(messageId, streamEvent.Update.MessageId, StringComparison.Ordinal))
{
messageId = streamEvent.Update.MessageId;
case ExecutorInvokedEvent executorInvoked:
Debug.WriteLine($"STEP ENTER #{executorInvoked.ExecutorId}");
break;
if (messageId is not null)
case ExecutorCompletedEvent executorComplete:
Debug.WriteLine($"STEP EXIT #{executorComplete.ExecutorId}");
break;
case ExecutorFailedEvent executorFailure:
Debug.WriteLine($"STEP ERROR #{executorFailure.ExecutorId}: {executorFailure.Data?.Message ?? "Unknown"}");
break;
case WorkflowErrorEvent workflowError:
throw workflowError.Data as Exception ?? new InvalidOperationException("Unexpected failure...");
case ConversationUpdateEvent invokeEvent:
Debug.WriteLine($"CONVERSATION: {invokeEvent.Data}");
break;
case AgentRunUpdateEvent streamEvent:
if (!string.Equals(messageId, streamEvent.Update.MessageId, StringComparison.Ordinal))
{
string? agentId = streamEvent.Update.AuthorName;
if (agentId is not null)
messageId = streamEvent.Update.MessageId;
if (messageId is not null)
{
if (!s_nameCache.TryGetValue(agentId, out string? realName))
string? agentId = streamEvent.Update.AuthorName;
if (agentId is not null)
{
PersistentAgent agent = await this.FoundryClient.Administration.GetAgentAsync(agentId);
s_nameCache[agentId] = agent.Name;
realName = agent.Name;
if (!s_nameCache.TryGetValue(agentId, out string? realName))
{
PersistentAgent agent = await this.FoundryClient.Administration.GetAgentAsync(agentId);
s_nameCache[agentId] = agent.Name;
realName = agent.Name;
}
agentId = realName;
}
agentId = realName;
}
agentId ??= nameof(ChatRole.Assistant);
Console.ForegroundColor = ConsoleColor.Cyan;
Console.Write($"\n{agentId.ToUpperInvariant()}:");
Console.ForegroundColor = ConsoleColor.DarkGray;
Console.WriteLine($" [{messageId}]");
}
}
ChatResponseUpdate? chatUpdate = streamEvent.Update.RawRepresentation as ChatResponseUpdate;
switch (chatUpdate?.RawRepresentation)
{
case MessageContentUpdate messageUpdate:
string? fileId = messageUpdate.ImageFileId ?? messageUpdate.TextAnnotation?.OutputFileId;
if (fileId is not null && s_fileCache.Add(fileId))
{
BinaryData content = await this.FoundryClient.Files.GetFileContentAsync(fileId);
await DownloadFileContentAsync(Path.GetFileName(messageUpdate.TextAnnotation?.TextToReplace ?? "response.png"), content);
}
break;
}
try
{
Console.ResetColor();
Console.Write(streamEvent.Data);
}
finally
{
Console.ResetColor();
}
}
else if (evt is AgentRunResponseEvent messageEvent)
{
try
{
Console.WriteLine();
if (messageEvent.Response.AgentId is null)
{
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine("ACTIVITY:");
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine(messageEvent.Response?.Text.Trim());
}
else
{
if (messageEvent.Response.Usage is not null)
{
agentId ??= nameof(ChatRole.Assistant);
Console.ForegroundColor = ConsoleColor.Cyan;
Console.Write($"\n{agentId.ToUpperInvariant()}:");
Console.ForegroundColor = ConsoleColor.DarkGray;
Console.WriteLine($"[Tokens Total: {messageEvent.Response.Usage.TotalTokenCount}, Input: {messageEvent.Response.Usage.InputTokenCount}, Output: {messageEvent.Response.Usage.OutputTokenCount}]");
Console.WriteLine($" [{messageId}]");
}
}
}
finally
{
Console.ResetColor();
}
ChatResponseUpdate? chatUpdate = streamEvent.Update.RawRepresentation as ChatResponseUpdate;
switch (chatUpdate?.RawRepresentation)
{
case MessageContentUpdate messageUpdate:
string? fileId = messageUpdate.ImageFileId ?? messageUpdate.TextAnnotation?.OutputFileId;
if (fileId is not null && s_fileCache.Add(fileId))
{
BinaryData content = await this.FoundryClient.Files.GetFileContentAsync(fileId);
await DownloadFileContentAsync(Path.GetFileName(messageUpdate.TextAnnotation?.TextToReplace ?? "response.png"), content);
}
break;
}
try
{
Console.ResetColor();
Console.Write(streamEvent.Data);
}
finally
{
Console.ResetColor();
}
break;
case AgentRunResponseEvent messageEvent:
try
{
Console.WriteLine();
if (messageEvent.Response.AgentId is null)
{
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine("ACTIVITY:");
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine(messageEvent.Response?.Text.Trim());
}
else
{
if (messageEvent.Response.Usage is not null)
{
Console.ForegroundColor = ConsoleColor.DarkGray;
Console.WriteLine($"[Tokens Total: {messageEvent.Response.Usage.TotalTokenCount}, Input: {messageEvent.Response.Usage.InputTokenCount}, Output: {messageEvent.Response.Usage.OutputTokenCount}]");
}
}
}
finally
{
Console.ResetColor();
}
break;
}
}
}
@@ -1,10 +1,14 @@
// Copyright (c) Microsoft. All rights reserved.
// Uncomment this to enable JSON checkpointing to the local file system.
#define CHECKPOINT_JSON
using System.Diagnostics;
using System.Reflection;
using Azure.AI.Agents.Persistent;
using Azure.Identity;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Agents.AI.Workflows.Checkpointing;
using Microsoft.Agents.AI.Workflows.Declarative;
using Microsoft.Agents.AI.Workflows.Declarative.Events;
using Microsoft.Extensions.AI;
@@ -57,8 +61,15 @@ internal sealed class Program
// Run the workflow, just like any other workflow
string input = this.GetWorkflowInput();
CheckpointManager checkpointManager = CheckpointManager.CreateInMemory();
#if CHECKPOINT_JSON
// Use a file-system based JSON checkpoint store to persist checkpoints to disk.
DirectoryInfo checkpointFolder = Directory.CreateDirectory(Path.Combine(".", $"chk-{DateTime.Now:YYmmdd-hhMMss-ff}"));
CheckpointManager checkpointManager = CheckpointManager.CreateJson(new FileSystemJsonCheckpointStore(checkpointFolder));
Checkpointed<StreamingRun> run = await InProcessExecution.StreamAsync(workflow, input, checkpointManager);
#else
// Use an in-memory checkpoint store that will not persist checkpoints beyond the lifetime of the process.
CheckpointManager checkpointManager = CheckpointManager.CreateInMemory();
#endif
bool isComplete = false;
InputResponse? response = null;
@@ -163,6 +174,9 @@ internal sealed class Program
Debug.WriteLine($"STEP ERROR #{executorFailure.ExecutorId}: {executorFailure.Data?.Message ?? "Unknown"}");
break;
case WorkflowErrorEvent workflowError:
throw workflowError.Data as Exception ?? new InvalidOperationException("Unexpected failure...");
case SuperStepCompletedEvent checkpointCompleted:
this.LastCheckpoint = checkpointCompleted.CompletionInfo?.Checkpoint;
Debug.WriteLine($"CHECKPOINT x{checkpointCompleted.StepNumber} [{this.LastCheckpoint?.CheckpointId ?? "(none)"}]");
@@ -53,21 +53,21 @@ internal sealed class JudgeExecutor() : ReflectingExecutor<JudgeExecutor>("Judge
this._targetNumber = targetNumber;
}
public async ValueTask HandleAsync(int message, IWorkflowContext context)
public async ValueTask HandleAsync(int message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
this._tries++;
if (message == this._targetNumber)
{
await context.YieldOutputAsync($"{this._targetNumber} found in {this._tries} tries!")
await context.YieldOutputAsync($"{this._targetNumber} found in {this._tries} tries!", cancellationToken)
.ConfigureAwait(false);
}
else if (message < this._targetNumber)
{
await context.SendMessageAsync(NumberSignal.Below).ConfigureAwait(false);
await context.SendMessageAsync(NumberSignal.Below, cancellationToken: cancellationToken).ConfigureAwait(false);
}
else
{
await context.SendMessageAsync(NumberSignal.Above).ConfigureAwait(false);
await context.SendMessageAsync(NumberSignal.Above, cancellationToken: cancellationToken).ConfigureAwait(false);
}
}
}
@@ -83,20 +83,20 @@ internal sealed class GuessNumberExecutor : ReflectingExecutor<GuessNumberExecut
private int NextGuess => (this.LowerBound + this.UpperBound) / 2;
public async ValueTask HandleAsync(NumberSignal message, IWorkflowContext context)
public async ValueTask HandleAsync(NumberSignal message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
switch (message)
{
case NumberSignal.Init:
await context.SendMessageAsync(this.NextGuess).ConfigureAwait(false);
await context.SendMessageAsync(this.NextGuess, cancellationToken: cancellationToken).ConfigureAwait(false);
break;
case NumberSignal.Above:
this.UpperBound = this.NextGuess - 1;
await context.SendMessageAsync(this.NextGuess).ConfigureAwait(false);
await context.SendMessageAsync(this.NextGuess, cancellationToken: cancellationToken).ConfigureAwait(false);
break;
case NumberSignal.Below:
this.LowerBound = this.NextGuess + 1;
await context.SendMessageAsync(this.NextGuess).ConfigureAwait(false);
await context.SendMessageAsync(this.NextGuess, cancellationToken: cancellationToken).ConfigureAwait(false);
break;
}
}
@@ -120,21 +120,21 @@ internal sealed class JudgeExecutor : ReflectingExecutor<JudgeExecutor>, IMessag
this._targetNumber = targetNumber;
}
public async ValueTask HandleAsync(int message, IWorkflowContext context)
public async ValueTask HandleAsync(int message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
this._tries++;
if (message == this._targetNumber)
{
await context.YieldOutputAsync($"{this._targetNumber} found in {this._tries} tries!")
await context.YieldOutputAsync($"{this._targetNumber} found in {this._tries} tries!", cancellationToken)
.ConfigureAwait(false);
}
else if (message < this._targetNumber)
{
await context.SendMessageAsync(NumberSignal.Below).ConfigureAwait(false);
await context.SendMessageAsync(NumberSignal.Below, cancellationToken: cancellationToken).ConfigureAwait(false);
}
else
{
await context.SendMessageAsync(NumberSignal.Above).ConfigureAwait(false);
await context.SendMessageAsync(NumberSignal.Above, cancellationToken: cancellationToken).ConfigureAwait(false);
}
}
}
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.Monitor.OpenTelemetry.Exporter" />
<PackageReference Include="OpenTelemetry" />
<PackageReference Include="System.Diagnostics.DiagnosticSource" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Workflows\Microsoft.Agents.AI.Workflows.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,101 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Diagnostics;
using Azure.Monitor.OpenTelemetry.Exporter;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Agents.AI.Workflows.Reflection;
using OpenTelemetry;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
namespace WorkflowObservabilitySample;
/// <summary>
/// This sample shows how to enable observability in a workflow and send the traces
/// to be visualized in Application Insights.
///
/// In this example, we create a simple text processing pipeline that:
/// 1. Takes input text and converts it to uppercase using an UppercaseExecutor
/// 2. Takes the uppercase text and reverses it using a ReverseTextExecutor
///
/// The executors are connected sequentially, so data flows from one to the next in order.
/// For input "Hello, World!", the workflow produces "!DLROW ,OLLEH".
/// </summary>
public static class Program
{
private const string SourceName = "Workflow.ApplicationInsightsSample";
private static readonly ActivitySource s_activitySource = new(SourceName);
private static async Task Main()
{
var applicationInsightsConnectionString = Environment.GetEnvironmentVariable("APPLICATIONINSIGHTS_CONNECTION_STRING") ?? throw new InvalidOperationException("APPLICATIONINSIGHTS_CONNECTION_STRING is not set.");
var resourceBuilder = ResourceBuilder
.CreateDefault()
.AddService("WorkflowSample");
using var traceProvider = Sdk.CreateTracerProviderBuilder()
.SetResourceBuilder(resourceBuilder)
.AddSource("Microsoft.Agents.AI.Workflows*")
.AddSource(SourceName)
.AddAzureMonitorTraceExporter(options => options.ConnectionString = applicationInsightsConnectionString)
.Build();
// Start a root activity for the application
using var activity = s_activitySource.StartActivity("main");
Console.WriteLine($"Operation/Trace ID: {Activity.Current?.TraceId}");
// Create the executors
UppercaseExecutor uppercase = new();
ReverseTextExecutor reverse = new();
// Build the workflow by connecting executors sequentially
var workflow = new WorkflowBuilder(uppercase)
.AddEdge(uppercase, reverse)
.Build();
// Execute the workflow with input data
Run run = await InProcessExecution.RunAsync(workflow, "Hello, World!");
foreach (WorkflowEvent evt in run.NewEvents)
{
if (evt is ExecutorCompletedEvent executorComplete)
{
Console.WriteLine($"{executorComplete.ExecutorId}: {executorComplete.Data}");
}
}
}
}
/// <summary>
/// First executor: converts input text to uppercase.
/// </summary>
internal sealed class UppercaseExecutor() : ReflectingExecutor<UppercaseExecutor>("UppercaseExecutor"), IMessageHandler<string, string>
{
/// <summary>
/// Processes the input message by converting it to uppercase.
/// </summary>
/// <param name="message">The input text to convert</param>
/// <param name="context">Workflow context for accessing workflow services and adding events</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
/// The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>The input text converted to uppercase</returns>
public async ValueTask<string> HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default) =>
message.ToUpperInvariant(); // The return value will be sent as a message along an edge to subsequent executors
}
/// <summary>
/// Second executor: reverses the input text and completes the workflow.
/// </summary>
internal sealed class ReverseTextExecutor() : ReflectingExecutor<ReverseTextExecutor>("ReverseTextExecutor"), IMessageHandler<string, string>
{
/// <summary>
/// Processes the input message by reversing the text.
/// </summary>
/// <param name="message">The input text to reverse</param>
/// <param name="context">Workflow context for accessing workflow services and adding events</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
/// The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>The input text reversed</returns>
public async ValueTask<string> HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default)
=> new(message.Reverse().ToArray());
}
@@ -78,8 +78,10 @@ internal sealed class UppercaseExecutor() : ReflectingExecutor<UppercaseExecutor
/// </summary>
/// <param name="message">The input text to convert</param>
/// <param name="context">Workflow context for accessing workflow services and adding events</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
/// The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>The input text converted to uppercase</returns>
public async ValueTask<string> HandleAsync(string message, IWorkflowContext context) =>
public async ValueTask<string> HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default) =>
message.ToUpperInvariant(); // The return value will be sent as a message along an edge to subsequent executors
}
@@ -93,6 +95,9 @@ internal sealed class ReverseTextExecutor() : ReflectingExecutor<ReverseTextExec
/// </summary>
/// <param name="message">The input text to reverse</param>
/// <param name="context">Workflow context for accessing workflow services and adding events</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
/// The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>The input text reversed</returns>
public async ValueTask<string> HandleAsync(string message, IWorkflowContext context) => new string(message.Reverse().ToArray());
public async ValueTask<string> HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default)
=> new(message.Reverse().ToArray());
}
@@ -54,13 +54,13 @@ internal static class FileContentStateConstants
internal sealed class FileReadExecutor() : ReflectingExecutor<FileReadExecutor>("FileReadExecutor"), IMessageHandler<string, string>
{
public async ValueTask<string> HandleAsync(string message, IWorkflowContext context)
public async ValueTask<string> HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
// Read file content from embedded resource
string fileContent = Resources.Read(message);
// Store file content in a shared state for access by other executors
string fileID = Guid.NewGuid().ToString("N");
await context.QueueStateUpdateAsync(fileID, fileContent, scopeName: FileContentStateConstants.FileContentStateScope);
await context.QueueStateUpdateAsync(fileID, fileContent, scopeName: FileContentStateConstants.FileContentStateScope, cancellationToken);
return fileID;
}
@@ -74,10 +74,10 @@ internal sealed class FileStats
internal sealed class WordCountingExecutor() : ReflectingExecutor<WordCountingExecutor>("WordCountingExecutor"), IMessageHandler<string, FileStats>
{
public async ValueTask<FileStats> HandleAsync(string message, IWorkflowContext context)
public async ValueTask<FileStats> HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
// Retrieve the file content from the shared state
var fileContent = await context.ReadStateAsync<string>(message, scopeName: FileContentStateConstants.FileContentStateScope)
var fileContent = await context.ReadStateAsync<string>(message, scopeName: FileContentStateConstants.FileContentStateScope, cancellationToken)
?? throw new InvalidOperationException("File content state not found");
int wordCount = fileContent.Split([' ', '\n', '\r'], StringSplitOptions.RemoveEmptyEntries).Length;
@@ -86,12 +86,13 @@ internal sealed class WordCountingExecutor() : ReflectingExecutor<WordCountingEx
}
}
internal sealed class ParagraphCountingExecutor() : ReflectingExecutor<ParagraphCountingExecutor>("ParagraphCountingExecutor"), IMessageHandler<string, FileStats>
internal sealed class ParagraphCountingExecutor() : ReflectingExecutor<ParagraphCountingExecutor>("ParagraphCountingExecutor"),
IMessageHandler<string, FileStats>
{
public async ValueTask<FileStats> HandleAsync(string message, IWorkflowContext context)
public async ValueTask<FileStats> HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
// Retrieve the file content from the shared state
var fileContent = await context.ReadStateAsync<string>(message, scopeName: FileContentStateConstants.FileContentStateScope)
var fileContent = await context.ReadStateAsync<string>(message, scopeName: FileContentStateConstants.FileContentStateScope, cancellationToken)
?? throw new InvalidOperationException("File content state not found");
int paragraphCount = fileContent.Split(['\n', '\r'], StringSplitOptions.RemoveEmptyEntries).Length;
@@ -104,7 +105,7 @@ internal sealed class AggregationExecutor() : ReflectingExecutor<AggregationExec
{
private readonly List<FileStats> _messages = [];
public async ValueTask HandleAsync(FileStats message, IWorkflowContext context)
public async ValueTask HandleAsync(FileStats message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
this._messages.Add(message);
@@ -113,7 +114,7 @@ internal sealed class AggregationExecutor() : ReflectingExecutor<AggregationExec
// Aggregate the results from both executors
var totalParagraphCount = this._messages.Sum(m => m.ParagraphCount);
var totalWordCount = this._messages.Sum(m => m.WordCount);
await context.YieldOutputAsync($"Total Paragraphs: {totalParagraphCount}, Total Words: {totalWordCount}");
await context.YieldOutputAsync($"Total Paragraphs: {totalParagraphCount}, Total Words: {totalWordCount}", cancellationToken);
}
}
}
@@ -51,8 +51,10 @@ internal sealed class UppercaseExecutor() : ReflectingExecutor<UppercaseExecutor
/// </summary>
/// <param name="message">The input text to convert</param>
/// <param name="context">Workflow context for accessing workflow services and adding events</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
/// The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>The input text converted to uppercase</returns>
public async ValueTask<string> HandleAsync(string message, IWorkflowContext context) =>
public async ValueTask<string> HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default) =>
message.ToUpperInvariant(); // The return value will be sent as a message along an edge to subsequent executors
}
@@ -66,8 +68,10 @@ internal sealed class ReverseTextExecutor() : ReflectingExecutor<ReverseTextExec
/// </summary>
/// <param name="message">The input text to reverse</param>
/// <param name="context">Workflow context for accessing workflow services and adding events</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
/// The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>The input text reversed</returns>
public async ValueTask<string> HandleAsync(string message, IWorkflowContext context)
public async ValueTask<string> HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
// Because we do not suppress it, the returned result will be yielded as an output from this executor.
return string.Concat(message.Reverse());
@@ -50,8 +50,10 @@ internal sealed class UppercaseExecutor() : ReflectingExecutor<UppercaseExecutor
/// </summary>
/// <param name="message">The input text to convert</param>
/// <param name="context">Workflow context for accessing workflow services and adding events</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
/// The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>The input text converted to uppercase</returns>
public async ValueTask<string> HandleAsync(string message, IWorkflowContext context) =>
public async ValueTask<string> HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default) =>
message.ToUpperInvariant(); // The return value will be sent as a message along an edge to subsequent executors
}
@@ -65,8 +67,10 @@ internal sealed class ReverseTextExecutor() : ReflectingExecutor<ReverseTextExec
/// </summary>
/// <param name="message">The input text to reverse</param>
/// <param name="context">Workflow context for accessing workflow services and adding events</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
/// The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>The input text reversed</returns>
public async ValueTask<string> HandleAsync(string message, IWorkflowContext context)
public async ValueTask<string> HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken)
{
// Because we do not suppress it, the returned result will be yielded as an output from this executor.
return string.Concat(message.Reverse());
@@ -37,7 +37,13 @@ public sealed class AzureAgentProvider(string projectEndpoint, TokenCredential p
/// <inheritdoc/>
public override async Task<string> CreateConversationAsync(CancellationToken cancellationToken = default)
{
PersistentAgentThread conversation = await this.GetAgentsClient().Threads.CreateThreadAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
PersistentAgentThread conversation =
await this.GetAgentsClient().Threads.CreateThreadAsync(
messages: null,
toolResources: null,
metadata: null,
cancellationToken).ConfigureAwait(false);
return conversation.Id;
}
@@ -78,6 +84,7 @@ public sealed class AzureAgentProvider(string projectEndpoint, TokenCredential p
TextContent textContent => new MessageInputTextBlock(textContent.Text),
HostedFileContent fileContent => new MessageInputImageFileBlock(new MessageImageFileParam(fileContent.FileId)),
UriContent uriContent when uriContent.Uri is not null => new MessageInputImageUriBlock(new MessageImageUriParam(uriContent.Uri.ToString())),
DataContent dataContent when dataContent.Uri is not null => new MessageInputImageUriBlock(new MessageImageUriParam(dataContent.Uri)),
_ => null // Unsupported content type
};
@@ -91,7 +98,7 @@ public sealed class AzureAgentProvider(string projectEndpoint, TokenCredential p
/// <inheritdoc/>
public override async Task<AIAgent> GetAgentAsync(string agentId, CancellationToken cancellationToken = default) =>
await this.GetAgentsClient().GetAIAgentAsync(agentId, chatOptions: null, cancellationToken: cancellationToken).ConfigureAwait(false);
await this.GetAgentsClient().GetAIAgentAsync(agentId, chatOptions: null, clientFactory: null, cancellationToken).ConfigureAwait(false);
/// <inheritdoc/>
public override async Task<ChatMessage> GetMessageAsync(string conversationId, string messageId, CancellationToken cancellationToken = default)
@@ -1,5 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json.Serialization;
namespace Microsoft.Agents.AI.Workflows.Declarative.Events;
/// <summary>
@@ -12,6 +14,7 @@ public sealed class InputRequest
/// </summary>
public string Prompt { get; }
[JsonConstructor]
internal InputRequest(string prompt)
{
this.Prompt = prompt;
@@ -1,5 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json.Serialization;
namespace Microsoft.Agents.AI.Workflows.Declarative.Events;
/// <summary>
@@ -16,6 +18,7 @@ public sealed class InputResponse
/// Initializes a new instance of the <see cref="InputResponse"/> class.
/// </summary>
/// <param name="value">The response value.</param>
[JsonConstructor]
public InputResponse(string value)
{
this.Value = value;
@@ -4,12 +4,21 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Azure.AI.Agents.Persistent;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.Workflows.Declarative.Extensions;
internal static class AgentProviderExtensions
{
private static readonly HashSet<Azure.AI.Agents.Persistent.RunStatus> s_failureStatus =
[
Azure.AI.Agents.Persistent.RunStatus.Failed,
Azure.AI.Agents.Persistent.RunStatus.Cancelled,
Azure.AI.Agents.Persistent.RunStatus.Cancelling,
Azure.AI.Agents.Persistent.RunStatus.Expired,
];
public static async ValueTask<AgentRunResponse> InvokeAgentAsync(
this WorkflowAgentProvider agentProvider,
string executorId,
@@ -51,9 +60,16 @@ internal static class AgentProviderExtensions
updates.Add(update);
if (update.RawRepresentation is ChatResponseUpdate chatUpdate &&
chatUpdate.RawRepresentation is RunUpdate runUpdate &&
s_failureStatus.Contains(runUpdate.Value.Status))
{
throw new DeclarativeActionException($"Unexpected failure invoking agent, run {runUpdate.Value.Status}: {agent.Name ?? agent.Id} [{runUpdate.Value.Id}/{conversationId}]");
}
if (autoSend)
{
await context.AddEventAsync(new AgentRunUpdateEvent(executorId, update)).ConfigureAwait(false);
await context.AddEventAsync(new AgentRunUpdateEvent(executorId, update), cancellationToken).ConfigureAwait(false);
}
}
@@ -61,7 +77,7 @@ internal static class AgentProviderExtensions
if (autoSend)
{
await context.AddEventAsync(new AgentRunResponseEvent(executorId, response)).ConfigureAwait(false);
await context.AddEventAsync(new AgentRunResponseEvent(executorId, response), cancellationToken).ConfigureAwait(false);
}
if (autoSend && !isWorkflowConversation && workflowConversationId is not null)
@@ -87,7 +103,7 @@ internal static class AgentProviderExtensions
{
conversationId = assignValue;
await context.QueueConversationUpdateAsync(conversationId).ConfigureAwait(false);
await context.QueueConversationUpdateAsync(conversationId, cancellationToken).ConfigureAwait(false);
}
}
}
@@ -131,7 +131,7 @@ internal static class ChatMessageExtensions
return
contentType switch
{
AgentMessageContentType.ImageUrl => new UriContent(contentValue, "image/*"),
AgentMessageContentType.ImageUrl => GetImageContent(contentValue),
AgentMessageContentType.ImageFile => new HostedFileContent(contentValue),
_ => new TextContent(contentValue)
};
@@ -169,7 +169,7 @@ internal static class ChatMessageExtensions
yield return
contentItem?.GetProperty<StringDataValue>(TypeSchema.Message.Fields.ContentType)?.Value switch
{
TypeSchema.Message.ContentTypes.ImageUrl => new UriContent(contentValue.Value, "image/*"),
TypeSchema.Message.ContentTypes.ImageUrl => GetImageContent(contentValue.Value),
TypeSchema.Message.ContentTypes.ImageFile => new HostedFileContent(contentValue.Value),
_ => new TextContent(contentValue.Value)
};
@@ -177,6 +177,11 @@ internal static class ChatMessageExtensions
}
}
private static AIContent GetImageContent(string uriText) =>
uriText.StartsWith("data:", StringComparison.OrdinalIgnoreCase) ?
new DataContent(uriText, "image/*") :
new UriContent(uriText, "image/*");
private static TValue? GetProperty<TValue>(this RecordDataValue record, string name)
where TValue : DataValue
{
@@ -148,8 +148,6 @@ internal static class DataValueExtensions
IEnumerable<KeyValuePair<string, DataValue>> GetFields()
{
yield return new KeyValuePair<string, DataValue>(TypeSchema.Discriminator, nameof(ExpandoObject).ToDataValue());
foreach (string key in value.Keys)
{
yield return new KeyValuePair<string, DataValue>(key, value[key].ToDataValue());
@@ -252,7 +250,6 @@ internal static class DataValueExtensions
private static Dictionary<string, object?> ToDictionary(this RecordDataValue record)
{
Dictionary<string, object?> result = [];
result[TypeSchema.Discriminator] = nameof(ExpandoObject);
foreach (KeyValuePair<string, DataValue> property in record.Properties)
{
result[property.Key] = property.Value.ToObject();
@@ -0,0 +1,28 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
using Microsoft.PowerFx.Types;
namespace Microsoft.Agents.AI.Workflows.Declarative.Extensions;
internal static class ExpandoObjectExtensions
{
public static RecordType ToRecordType(this ExpandoObject value)
{
RecordType recordType = RecordType.Empty();
foreach (KeyValuePair<string, object?> property in value)
{
recordType.Add(property.Key, property.Value.GetFormulaType());
}
return recordType;
}
public static RecordValue ToRecord(this ExpandoObject value) =>
FormulaValue.NewRecordFromFields(
value.Select(
property => new NamedValue(property.Key, property.Value.ToFormula())));
}
@@ -123,6 +123,8 @@ internal static class FormulaValueExtensions
_ => DataType.Unspecified,
};
public static object AsPortable(this FormulaValue? value) => (value?.ToObject()).AsPortable();
public static string Format(this FormulaValue value) =>
value switch
{
@@ -161,6 +163,10 @@ internal static class FormulaValueExtensions
}
}
}
public static RecordValue ToRecord(this Dictionary<string, PortableValue> value) =>
FormulaValue.NewRecordFromFields(
value.Select(
property => new NamedValue(property.Key, property.Value.ToFormula())));
private static RecordDataType ToDataType(this RecordType record)
{
@@ -182,21 +188,6 @@ internal static class FormulaValueExtensions
return tableType;
}
private static RecordType ToRecordType(this ExpandoObject value)
{
RecordType recordType = RecordType.Empty();
foreach (KeyValuePair<string, object?> property in value)
{
recordType.Add(property.Key, property.Value.GetFormulaType());
}
return recordType;
}
private static RecordValue ToRecord(this ExpandoObject value) =>
FormulaValue.NewRecordFromFields(
value.Select(
property => new NamedValue(property.Key, property.Value.ToFormula())));
private static TableType ToTableType(this IEnumerable value)
{
foreach (object? element in value)
@@ -14,23 +14,11 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.Extensions;
internal static class IWorkflowContextExtensions
{
public static ValueTask RaiseInvocationEventAsync(this IWorkflowContext context, DialogAction action, string? priorEventId = null) =>
context.AddEventAsync(new DeclarativeActionInvokedEvent(action, priorEventId));
public static ValueTask RaiseInvocationEventAsync(this IWorkflowContext context, DialogAction action, string? priorEventId = null, CancellationToken cancellationToken = default) =>
context.AddEventAsync(new DeclarativeActionInvokedEvent(action, priorEventId), cancellationToken);
public static ValueTask RaiseCompletionEventAsync(this IWorkflowContext context, DialogAction action) =>
context.AddEventAsync(new DeclarativeActionCompletedEvent(action));
public static ValueTask SendResultMessageAsync(this IWorkflowContext context, string id, object? result = null, CancellationToken cancellationToken = default) =>
context.SendMessageAsync(new ActionExecutorResult(id, result));
public static ValueTask QueueStateResetAsync(this IWorkflowContext context, PropertyPath variablePath) =>
context.QueueStateUpdateAsync(Throw.IfNull(variablePath.VariableName), UnassignedValue.Instance, Throw.IfNull(variablePath.NamespaceAlias));
public static ValueTask QueueStateUpdateAsync<TValue>(this IWorkflowContext context, PropertyPath variablePath, TValue? value) =>
context.QueueStateUpdateAsync(Throw.IfNull(variablePath.VariableName), value, Throw.IfNull(variablePath.NamespaceAlias));
public static ValueTask QueueSystemUpdateAsync<TValue>(this IWorkflowContext context, string key, TValue? value) =>
DeclarativeContext(context).QueueSystemUpdateAsync(key, value);
public static ValueTask RaiseCompletionEventAsync(this IWorkflowContext context, DialogAction action, CancellationToken cancellationToken = default) =>
context.AddEventAsync(new DeclarativeActionCompletedEvent(action), cancellationToken);
public static FormulaValue ReadState(this IWorkflowContext context, PropertyPath variablePath) =>
context.ReadState(Throw.IfNull(variablePath.VariableName), Throw.IfNull(variablePath.NamespaceAlias));
@@ -38,18 +26,47 @@ internal static class IWorkflowContextExtensions
public static FormulaValue ReadState(this IWorkflowContext context, string key, string? scopeName = null) =>
DeclarativeContext(context).State.Get(key, scopeName);
public static async ValueTask QueueConversationUpdateAsync(this IWorkflowContext context, string conversationId, bool isExternal = false)
public static ValueTask SendResultMessageAsync(this IWorkflowContext context, string id, CancellationToken cancellationToken = default) =>
context.SendResultMessageAsync(id, result: null, cancellationToken);
public static ValueTask SendResultMessageAsync(this IWorkflowContext context, string id, object? result, CancellationToken cancellationToken = default) =>
context.SendMessageAsync(new ActionExecutorResult(id, result), targetId: null, cancellationToken);
public static ValueTask QueueStateResetAsync(this IWorkflowContext context, PropertyPath variablePath, CancellationToken cancellationToken = default) =>
context.QueueStateUpdateAsync(Throw.IfNull(variablePath.VariableName), UnassignedValue.Instance, Throw.IfNull(variablePath.NamespaceAlias), cancellationToken);
public static ValueTask QueueStateUpdateAsync<TValue>(this IWorkflowContext context, PropertyPath variablePath, TValue? value, CancellationToken cancellationToken = default) =>
context.QueueStateUpdateAsync(Throw.IfNull(variablePath.VariableName), value, Throw.IfNull(variablePath.NamespaceAlias), cancellationToken);
public static async ValueTask QueueEnvironmentUpdateAsync<TValue>(this IWorkflowContext context, string key, TValue? value, CancellationToken cancellationToken = default)
{
DeclarativeWorkflowContext declarativeContext = DeclarativeContext(context);
await declarativeContext.UpdateStateAsync(key, value, VariableScopeNames.Environment, allowSystem: true, cancellationToken).ConfigureAwait(false);
declarativeContext.State.Bind();
}
public static async ValueTask QueueSystemUpdateAsync<TValue>(this IWorkflowContext context, string key, TValue? value, CancellationToken cancellationToken = default)
{
DeclarativeWorkflowContext declarativeContext = DeclarativeContext(context);
await declarativeContext.UpdateStateAsync(key, value, VariableScopeNames.System, allowSystem: true, cancellationToken).ConfigureAwait(false);
declarativeContext.State.Bind();
}
public static ValueTask QueueConversationUpdateAsync(this IWorkflowContext context, string conversationId, CancellationToken cancellationToken = default) =>
context.QueueConversationUpdateAsync(conversationId, isExternal: false, cancellationToken);
public static async ValueTask QueueConversationUpdateAsync(this IWorkflowContext context, string conversationId, bool isExternal = false, CancellationToken cancellationToken = default)
{
RecordValue conversation = (RecordValue)context.ReadState(SystemScope.Names.Conversation, VariableScopeNames.System);
if (isExternal)
{
conversation.UpdateField("Id", FormulaValue.New(conversationId));
await context.QueueSystemUpdateAsync(SystemScope.Names.Conversation, conversation).ConfigureAwait(false);
await context.QueueSystemUpdateAsync(SystemScope.Names.ConversationId, FormulaValue.New(conversationId)).ConfigureAwait(false);
await context.QueueSystemUpdateAsync(SystemScope.Names.Conversation, conversation, cancellationToken).ConfigureAwait(false);
await context.QueueSystemUpdateAsync(SystemScope.Names.ConversationId, FormulaValue.New(conversationId), cancellationToken).ConfigureAwait(false);
}
await context.AddEventAsync(new ConversationUpdateEvent(conversationId) { IsWorkflow = isExternal }).ConfigureAwait(false);
await context.AddEventAsync(new ConversationUpdateEvent(conversationId) { IsWorkflow = isExternal }, cancellationToken).ConfigureAwait(false);
}
public static bool IsWorkflowConversation(
@@ -7,6 +7,7 @@ using System.Linq;
using System.Text.Json;
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
using Microsoft.Bot.ObjectModel;
using Microsoft.Extensions.AI;
using Microsoft.PowerFx.Types;
namespace Microsoft.Agents.AI.Workflows.Declarative.Extensions;
@@ -46,6 +47,56 @@ internal static class ObjectExtensions
}
}
public static object AsPortable(this object? value) =>
value switch
{
null => UnassignedValue.Instance,
string or
bool or
int or
float or
long or
decimal or
double or
DateTime or
TimeSpan =>
value,
ChatMessage messageValue => messageValue.ToRecord().AsPortable(),
IDictionary<string, object?> objectValue => objectValue.AsPortable(),
IDictionary recordValue => recordValue.AsPortable(),
IEnumerable tableValue => tableValue.AsPortable(),
_ => throw new DeclarativeModelException($"Unsupported data type: {value.GetType().Name}"),
};
public static object AsPortable(this IDictionary<string, object?> value) => value.ToDictionary(kvp => kvp.Key, kvp => new PortableValue(kvp.Value.AsPortable()));
public static object AsPortable(this IDictionary value)
{
return GetEntries().ToDictionary(kvp => kvp.Key, kvp => new PortableValue(kvp.Value.AsPortable()));
IEnumerable<KeyValuePair<string, object?>> GetEntries()
{
foreach (string key in value.Keys)
{
yield return new KeyValuePair<string, object?>(key, value[key]);
}
}
}
public static object AsPortable(this IEnumerable value)
{
return GetValues().ToArray();
IEnumerable<PortableValue> GetValues()
{
IEnumerator enumerator = value.GetEnumerator();
while (enumerator.MoveNext())
{
yield return new PortableValue(enumerator.Current.AsPortable());
}
}
}
public static object? ConvertType(this object? sourceValue, VariableType targetType)
{
if (!targetType.IsValid())
@@ -0,0 +1,109 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
using Microsoft.Extensions.AI;
using Microsoft.PowerFx.Types;
namespace Microsoft.Agents.AI.Workflows.Declarative.Extensions;
internal static class PortableValueExtensions
{
public static FormulaValue ToFormula(this PortableValue value) =>
value.TypeId switch
{
null => FormulaValue.NewBlank(),
_ when value.TypeId.IsMatch<UnassignedValue>() => FormulaValue.NewBlank(),
_ when value.IsType(out string? stringValue) => FormulaValue.New(stringValue),
_ when value.IsSystemType(out bool? boolValue) => FormulaValue.New(boolValue.Value),
_ when value.IsSystemType(out int? intValue) => FormulaValue.New(intValue.Value),
_ when value.IsSystemType(out long? longValue) => FormulaValue.New(longValue.Value),
_ when value.IsSystemType(out decimal? decimalValue) => FormulaValue.New(decimalValue.Value),
_ when value.IsSystemType(out float? floatValue) => FormulaValue.New(floatValue.Value),
_ when value.IsSystemType(out double? doubleValue) => FormulaValue.New(doubleValue.Value),
_ when value.IsParentType(out Dictionary<string, PortableValue>? recordValue) => recordValue.ToRecord(),
_ when value.IsParentType(out IDictionary? recordValue) => recordValue.ToRecord(),
_ when value.IsType(out PortableValue[]? tableValue) => tableValue.ToTable(),
_ when value.IsType(out ChatMessage? messageValue) => messageValue.ToRecord(),
_ when value.IsType(out DateTime dateValue) =>
dateValue.TimeOfDay == TimeSpan.Zero ?
FormulaValue.NewDateOnly(dateValue.Date) :
FormulaValue.New(dateValue),
_ when value.IsType(out TimeSpan timeValue) => FormulaValue.New(timeValue),
_ => throw new DeclarativeModelException($"Unsupported portable type: {value.TypeId.TypeName}"),
};
private static TableValue ToTable(this PortableValue[] values)
{
FormulaValue[] formulaValues = values.Select(value => value.ToFormula()).ToArray();
if (formulaValues[0] is RecordValue recordValue)
{
return FormulaValue.NewTable(ParseRecordType(recordValue), formulaValues.OfType<RecordValue>());
}
return
formulaValues[0] switch
{
PrimitiveValue<bool> => NewSingleColumnTable<bool>(),
PrimitiveValue<string> => NewSingleColumnTable<string>(),
PrimitiveValue<int> => NewSingleColumnTable<int>(),
PrimitiveValue<long> => NewSingleColumnTable<long>(),
PrimitiveValue<float> => NewSingleColumnTable<float>(),
PrimitiveValue<decimal> => NewSingleColumnTable<decimal>(),
PrimitiveValue<double> => NewSingleColumnTable<double>(),
PrimitiveValue<TimeSpan> => NewSingleColumnTable<TimeSpan>(),
PrimitiveValue<DateTime> => NewSingleColumnTable<DateTime>(),
_ => throw new DeclarativeModelException($"Unsupported table element type: {formulaValues[0].Type.GetType().Name}"),
};
TableValue NewSingleColumnTable<TValue>() =>
FormulaValue.NewSingleColumnTable(formulaValues.OfType<PrimitiveValue<TValue>>());
}
private static RecordType ParseRecordType(this RecordValue record)
{
RecordType recordType = RecordType.Empty();
foreach (NamedValue property in record.Fields)
{
recordType = recordType.Add(property.Name, property.Value.Type);
}
return recordType;
}
private static bool IsParentType<TValue>(this PortableValue value, [NotNullWhen(true)] out TValue? typedValue)
{
if (value.TypeId.IsMatchPolymorphic(typeof(TValue)))
{
return value.Is(out typedValue);
}
typedValue = default;
return false;
}
private static bool IsSystemType<TValue>(this PortableValue value, [NotNullWhen(true)] out TValue? typedValue) where TValue : struct
{
if (value.TypeId.IsMatch<TValue>() || value.TypeId.IsMatch(typeof(TValue).UnderlyingSystemType))
{
return value.Is(out typedValue);
}
typedValue = default;
return false;
}
private static bool IsType<TValue>(this PortableValue value, [NotNullWhen(true)] out TValue? typedValue)
{
if (value.TypeId.IsMatch<TValue>())
{
return value.Is(out typedValue);
}
typedValue = default;
return false;
}
}
@@ -61,7 +61,7 @@ internal abstract class DeclarativeActionExecutor : Executor<ActionExecutorResul
}
/// <inheritdoc/>
public override async ValueTask HandleAsync(ActionExecutorResult message, IWorkflowContext context)
public override async ValueTask HandleAsync(ActionExecutorResult message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
if (this.Model.Disabled)
{
@@ -69,7 +69,7 @@ internal abstract class DeclarativeActionExecutor : Executor<ActionExecutorResul
return;
}
await context.RaiseInvocationEventAsync(this.Model, message.ExecutorId).ConfigureAwait(false);
await context.RaiseInvocationEventAsync(this.Model, message.ExecutorId, cancellationToken).ConfigureAwait(false);
try
{
@@ -78,7 +78,7 @@ internal abstract class DeclarativeActionExecutor : Executor<ActionExecutorResul
if (this.EmitResultEvent)
{
await context.SendResultMessageAsync(this.Id, result).ConfigureAwait(false);
await context.SendResultMessageAsync(this.Id, result, cancellationToken).ConfigureAwait(false);
}
}
catch (DeclarativeActionException exception)
@@ -95,7 +95,7 @@ internal abstract class DeclarativeActionExecutor : Executor<ActionExecutorResul
{
if (this.IsDiscreteAction)
{
await context.RaiseCompletionEventAsync(this.Model).ConfigureAwait(false);
await context.RaiseCompletionEventAsync(this.Model, cancellationToken).ConfigureAwait(false);
}
}
}
@@ -3,6 +3,7 @@
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
@@ -32,16 +33,18 @@ internal sealed class DeclarativeWorkflowContext : IWorkflowContext
public IReadOnlyDictionary<string, string>? TraceContext => this.Source.TraceContext;
/// <inheritdoc/>
public ValueTask AddEventAsync(WorkflowEvent workflowEvent) => this.Source.AddEventAsync(workflowEvent);
public ValueTask AddEventAsync(WorkflowEvent workflowEvent, CancellationToken cancellationToken = default)
=> this.Source.AddEventAsync(workflowEvent, cancellationToken);
/// <inheritdoc/>
public ValueTask YieldOutputAsync(object output) => this.Source.YieldOutputAsync(output);
public ValueTask YieldOutputAsync(object output, CancellationToken cancellationToken = default)
=> this.Source.YieldOutputAsync(output, cancellationToken);
/// <inheritdoc/>
public ValueTask RequestHaltAsync() => this.Source.RequestHaltAsync();
/// <inheritdoc/>
public async ValueTask QueueClearScopeAsync(string? scopeName = null)
public async ValueTask QueueClearScopeAsync(string? scopeName = null, CancellationToken cancellationToken = default)
{
if (scopeName is not null)
{
@@ -50,12 +53,12 @@ internal sealed class DeclarativeWorkflowContext : IWorkflowContext
// Copy keys to array to avoid modifying collection during enumeration.
foreach (string key in this.State.Keys(scopeName).ToArray())
{
await this.UpdateStateAsync(key, UnassignedValue.Instance, scopeName).ConfigureAwait(false);
await this.UpdateStateAsync(key, UnassignedValue.Instance, scopeName, allowSystem: false, cancellationToken).ConfigureAwait(false);
}
}
else
{
await this.Source.QueueClearScopeAsync(scopeName).ConfigureAwait(false);
await this.Source.QueueClearScopeAsync(scopeName, cancellationToken).ConfigureAwait(false);
}
this.State.Bind();
@@ -63,20 +66,14 @@ internal sealed class DeclarativeWorkflowContext : IWorkflowContext
}
/// <inheritdoc/>
public async ValueTask QueueStateUpdateAsync<T>(string key, T? value, string? scopeName = null)
public async ValueTask QueueStateUpdateAsync<T>(string key, T? value, string? scopeName = null, CancellationToken cancellationToken = default)
{
await this.UpdateStateAsync(key, value, scopeName).ConfigureAwait(false);
this.State.Bind();
}
public async ValueTask QueueSystemUpdateAsync<TValue>(string key, TValue? value)
{
await this.UpdateStateAsync(key, value, VariableScopeNames.System, allowSystem: true).ConfigureAwait(false);
await this.UpdateStateAsync(key, value, scopeName, allowSystem: false, cancellationToken).ConfigureAwait(false);
this.State.Bind();
}
/// <inheritdoc/>
public async ValueTask<TValue?> ReadStateAsync<TValue>(string key, string? scopeName = null)
public async ValueTask<TValue?> ReadStateAsync<TValue>(string key, string? scopeName = null, CancellationToken cancellationToken = default)
{
bool isManagedScope =
scopeName is not null && // null scope cannot be managed
@@ -86,21 +83,23 @@ internal sealed class DeclarativeWorkflowContext : IWorkflowContext
{
// Not a managed scope, just pass through. This is valid when a declarative
// workflow has been ejected to code (where DeclarativeWorkflowContext is also utilized).
_ when !isManagedScope => await this.Source.ReadStateAsync<TValue>(key, scopeName).ConfigureAwait(false),
_ when !isManagedScope => await this.Source.ReadStateAsync<TValue>(key, scopeName, cancellationToken).ConfigureAwait(false),
// Retrieve formula values directly from the managed state to avoid conversion.
_ when typeof(TValue) == typeof(FormulaValue) => (TValue?)(object?)this.State.Get(key, scopeName),
// Retrieve native types from the source context to avoid conversion.
_ => await this.Source.ReadStateAsync<TValue>(key, scopeName).ConfigureAwait(false),
_ => await this.Source.ReadStateAsync<TValue>(key, scopeName, cancellationToken).ConfigureAwait(false),
};
}
/// <inheritdoc/>
public ValueTask<HashSet<string>> ReadStateKeysAsync(string? scopeName = null) => this.Source.ReadStateKeysAsync(scopeName);
public ValueTask<HashSet<string>> ReadStateKeysAsync(string? scopeName = null, CancellationToken cancellationToken = default)
=> this.Source.ReadStateKeysAsync(scopeName, cancellationToken);
/// <inheritdoc/>
public ValueTask SendMessageAsync(object message, string? targetId = null) => this.Source.SendMessageAsync(message, targetId);
public ValueTask SendMessageAsync(object message, string? targetId = null, CancellationToken cancellationToken = default)
=> this.Source.SendMessageAsync(message, targetId, cancellationToken);
private ValueTask UpdateStateAsync<T>(string key, T? value, string? scopeName, bool allowSystem = true)
public ValueTask UpdateStateAsync<T>(string key, T? value, string? scopeName, bool allowSystem, CancellationToken cancellationToken = default)
{
bool isManagedScope =
scopeName is not null && // null scope cannot be managed
@@ -110,7 +109,7 @@ internal sealed class DeclarativeWorkflowContext : IWorkflowContext
{
// Not a managed scope, just pass through. This is valid when a declarative
// workflow has been ejected to code (where DeclarativeWorkflowContext is also utilized).
return this.Source.QueueStateUpdateAsync(key, value, scopeName);
return this.Source.QueueStateUpdateAsync(key, value, scopeName, cancellationToken);
}
if (!ManagedScopes.Contains(scopeName!) && !allowSystem)
@@ -134,7 +133,7 @@ internal sealed class DeclarativeWorkflowContext : IWorkflowContext
{
this.State.Set(key, FormulaValue.NewBlank(), scopeName);
}
return this.Source.QueueStateUpdateAsync(key, UnassignedValue.Instance, scopeName);
return this.Source.QueueStateUpdateAsync(key, UnassignedValue.Instance, scopeName, cancellationToken);
}
ValueTask QueueFormulaStateAsync(FormulaValue formulaValue)
@@ -143,27 +142,32 @@ internal sealed class DeclarativeWorkflowContext : IWorkflowContext
{
this.State.Set(key, formulaValue, scopeName);
}
return this.Source.QueueStateUpdateAsync(key, formulaValue.ToObject(), scopeName);
return this.Source.QueueStateUpdateAsync(key, formulaValue.AsPortable(), scopeName, cancellationToken);
}
ValueTask QueueDataValueStateAsync(DataValue dataValue)
{
FormulaValue formulaValue = dataValue.ToFormula();
if (isManagedScope)
{
FormulaValue formulaValue = dataValue.ToFormula();
this.State.Set(key, formulaValue, scopeName);
}
return this.Source.QueueStateUpdateAsync(key, dataValue.ToObject(), scopeName);
return this.Source.QueueStateUpdateAsync(key, formulaValue.AsPortable(), scopeName, cancellationToken);
}
ValueTask QueueNativeStateAsync(object? rawValue)
ValueTask QueueNativeStateAsync(object rawValue)
{
FormulaValue formulaValue = rawValue.ToFormula();
if (isManagedScope)
{
FormulaValue formulaValue = rawValue.ToFormula();
this.State.Set(key, formulaValue, scopeName);
}
return this.Source.QueueStateUpdateAsync(key, rawValue, scopeName);
return this.Source.QueueStateUpdateAsync(key, formulaValue.AsPortable(), scopeName, cancellationToken);
}
}
}
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
@@ -24,7 +25,7 @@ internal sealed class DeclarativeWorkflowExecutor<TInput>(
return default;
}
public override async ValueTask HandleAsync(TInput message, IWorkflowContext context)
public override async ValueTask HandleAsync(TInput message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
// No state to restore if we're starting from the beginning.
state.SetInitialized();
@@ -35,13 +36,13 @@ internal sealed class DeclarativeWorkflowExecutor<TInput>(
string? conversationId = options.ConversationId;
if (string.IsNullOrWhiteSpace(conversationId))
{
conversationId = await options.AgentProvider.CreateConversationAsync(cancellationToken: default).ConfigureAwait(false);
conversationId = await options.AgentProvider.CreateConversationAsync(cancellationToken).ConfigureAwait(false);
}
await declarativeContext.QueueConversationUpdateAsync(conversationId, isExternal: true).ConfigureAwait(false);
await declarativeContext.QueueConversationUpdateAsync(conversationId, isExternal: true, cancellationToken).ConfigureAwait(false);
await options.AgentProvider.CreateMessageAsync(conversationId, input, cancellationToken: default).ConfigureAwait(false);
await declarativeContext.SetLastMessageAsync(input).ConfigureAwait(false);
ChatMessage inputMessage = await options.AgentProvider.CreateMessageAsync(conversationId, input, cancellationToken).ConfigureAwait(false);
await declarativeContext.SetLastMessageAsync(inputMessage).ConfigureAwait(false);
await context.SendResultMessageAsync(this.Id).ConfigureAwait(false);
await context.SendResultMessageAsync(this.Id, cancellationToken).ConfigureAwait(false);
}
}
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
@@ -11,11 +12,11 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.Interpreter;
internal sealed class DelegateActionExecutor(string actionId, WorkflowFormulaState state, DelegateAction<ActionExecutorResult>? action = null, bool emitResult = true)
: DelegateActionExecutor<ActionExecutorResult>(actionId, state, action, emitResult)
{
public override ValueTask HandleAsync(ActionExecutorResult message, IWorkflowContext context)
public override ValueTask HandleAsync(ActionExecutorResult message, IWorkflowContext context, CancellationToken cancellationToken)
{
Debug.WriteLine($"RESULT #{this.Id} - {message.Result ?? "(null)"}");
return base.HandleAsync(message, context);
return base.HandleAsync(message, context, cancellationToken);
}
}
@@ -39,16 +40,16 @@ internal class DelegateActionExecutor<TMessage> : Executor<TMessage>, IResettabl
return default;
}
public override async ValueTask HandleAsync(TMessage message, IWorkflowContext context)
public override async ValueTask HandleAsync(TMessage message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
if (this._action is not null)
{
await this._action.Invoke(new DeclarativeWorkflowContext(context, this._state), message, default).ConfigureAwait(false);
await this._action.Invoke(new DeclarativeWorkflowContext(context, this._state), message, cancellationToken).ConfigureAwait(false);
}
if (this._emitResult)
{
await context.SendResultMessageAsync(this.Id).ConfigureAwait(false);
await context.SendResultMessageAsync(this.Id, cancellationToken).ConfigureAwait(false);
}
}
}
@@ -73,12 +73,12 @@ public abstract class ActionExecutor<TMessage> : Executor<TMessage>, IResettable
}
/// <inheritdoc/>
public override async ValueTask HandleAsync(TMessage message, IWorkflowContext context)
public override async ValueTask HandleAsync(TMessage message, IWorkflowContext context, CancellationToken cancellationToken)
{
object? result = await this.ExecuteAsync(new DeclarativeWorkflowContext(context, this._session.State), message, cancellationToken: default).ConfigureAwait(false);
Debug.WriteLine($"RESULT #{this.Id} - {result ?? "(null)"}");
await context.SendResultMessageAsync(this.Id, result).ConfigureAwait(false);
await context.SendResultMessageAsync(this.Id, result, cancellationToken).ConfigureAwait(false);
}
/// <summary>
@@ -132,7 +132,7 @@ public static class IWorkflowContextExtensions
/// <returns>The converted value</returns>
public static async ValueTask<object?> ConvertValueAsync(this IWorkflowContext context, VariableType targetType, string key, string? scopeName = null, CancellationToken cancellationToken = default)
{
object? sourceValue = await context.ReadStateAsync<object>(key, scopeName).ConfigureAwait(false);
object? sourceValue = await context.ReadStateAsync<object>(key, scopeName, cancellationToken).ConfigureAwait(false);
return sourceValue.ConvertType(targetType);
}
@@ -143,10 +143,11 @@ public static class IWorkflowContextExtensions
/// <param name="context">The workflow execution context used to restore persisted state prior to formatting.</param>
/// <param name="key">The key of the state value.</param>
/// <param name = "scopeName" > An optional name that specifies the scope to read.If null, the default scope is used.</param>
/// <param name="cancellationToken">A token that propagates notification when operation should be canceled.</param>
/// <returns>The evaluated list expression</returns>
public static async ValueTask<IList<TElement>?> ReadListAsync<TElement>(this IWorkflowContext context, string key, string? scopeName = null)
public static async ValueTask<IList<TElement>?> ReadListAsync<TElement>(this IWorkflowContext context, string key, string? scopeName = null, CancellationToken cancellationToken = default)
{
object? value = await context.ReadStateAsync<object>(key, scopeName).ConfigureAwait(false);
object? value = await context.ReadStateAsync<object>(key, scopeName, cancellationToken).ConfigureAwait(false);
return value.AsList<TElement>();
}
@@ -6,7 +6,6 @@ using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
using Microsoft.Agents.AI.Workflows.Declarative.Interpreter;
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
using Microsoft.Bot.ObjectModel;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Configuration;
@@ -55,23 +54,23 @@ public abstract class RootExecutor<TInput> : Executor<TInput>, IResettableExecut
}
/// <inheritdoc/>
public override async ValueTask HandleAsync(TInput message, IWorkflowContext context)
public override async ValueTask HandleAsync(TInput message, IWorkflowContext context, CancellationToken cancellationToken)
{
DeclarativeWorkflowContext declarativeContext = new(context, this._state);
await this.ExecuteAsync(message, declarativeContext, cancellationToken: default).ConfigureAwait(false);
await this.ExecuteAsync(message, declarativeContext, cancellationToken).ConfigureAwait(false);
ChatMessage input = (this._inputTransform ?? DefaultInputTransform).Invoke(message);
if (string.IsNullOrWhiteSpace(this._conversationId))
{
this._conversationId = await this._agentProvider.CreateConversationAsync(cancellationToken: default).ConfigureAwait(false);
this._conversationId = await this._agentProvider.CreateConversationAsync(cancellationToken).ConfigureAwait(false);
}
await declarativeContext.QueueConversationUpdateAsync(this._conversationId, isExternal: true).ConfigureAwait(false);
await declarativeContext.QueueConversationUpdateAsync(this._conversationId, isExternal: true, cancellationToken).ConfigureAwait(false);
await this._agentProvider.CreateMessageAsync(this._conversationId, input, cancellationToken: default).ConfigureAwait(false);
await declarativeContext.SetLastMessageAsync(input).ConfigureAwait(false);
ChatMessage inputMessage = await this._agentProvider.CreateMessageAsync(this._conversationId, input, cancellationToken).ConfigureAwait(false);
await declarativeContext.SetLastMessageAsync(inputMessage).ConfigureAwait(false);
await declarativeContext.SendMessageAsync(new ActionExecutorResult(this.Id)).ConfigureAwait(false);
await declarativeContext.SendResultMessageAsync(this.Id, cancellationToken).ConfigureAwait(false);
}
/// <summary>
@@ -94,7 +93,7 @@ public abstract class RootExecutor<TInput> : Executor<TInput>, IResettableExecut
{
foreach (string variableName in variableNames)
{
await context.QueueStateUpdateAsync(variableName, GetEnvironmentVariable(variableName), VariableScopeNames.Environment).ConfigureAwait(false);
await context.QueueEnvironmentUpdateAsync(variableName, GetEnvironmentVariable(variableName)).ConfigureAwait(false);
}
string GetEnvironmentVariable(string name)
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(ProjectsTargetFrameworks)</TargetFrameworks>
@@ -21,16 +21,15 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.Agents.Persistent" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.Bot.ObjectModel" />
<PackageReference Include="Microsoft.Bot.ObjectModel.Json" />
<PackageReference Include="Microsoft.Bot.ObjectModel.PowerFx" />
<PackageReference Include="Microsoft.PowerFx.Interpreter" />
<PackageReference Include="System.CodeDom" />
<PackageReference Include="System.Collections.Immutable" />
<PackageReference Include="Microsoft.Extensions.Configuration" />
<PackageReference Include="Microsoft.Extensions.Logging" />
<PackageReference Include="System.CodeDom" />
<PackageReference Include="System.Collections.Immutable" />
</ItemGroup>
<ItemGroup>
@@ -28,7 +28,7 @@ internal sealed class ClearAllVariablesExecutor(ClearAllVariables model, Workflo
if (scope is not null)
{
await context.QueueClearScopeAsync(scope).ConfigureAwait(false);
await context.QueueClearScopeAsync(scope, cancellationToken).ConfigureAwait(false);
Debug.WriteLine(
$"""
STATE: {this.GetType().Name} [{this.Id}]
@@ -69,5 +69,5 @@ internal sealed class ConditionGroupExecutor : DeclarativeActionExecutor<Conditi
}
public async ValueTask DoneAsync(IWorkflowContext context, ActionExecutorResult _, CancellationToken cancellationToken) =>
await context.RaiseCompletionEventAsync(this.Model).ConfigureAwait(false);
await context.RaiseCompletionEventAsync(this.Model, cancellationToken).ConfigureAwait(false);
}
@@ -17,7 +17,7 @@ internal sealed class CreateConversationExecutor(CreateConversation model, Workf
{
string conversationId = await agentProvider.CreateConversationAsync(cancellationToken).ConfigureAwait(false);
await this.AssignAsync(this.Model.ConversationId?.Path, FormulaValue.New(conversationId), context).ConfigureAwait(false);
await context.QueueConversationUpdateAsync(conversationId).ConfigureAwait(false);
await context.QueueConversationUpdateAsync(conversationId, cancellationToken).ConfigureAwait(false);
return default;
}
@@ -68,11 +68,11 @@ internal sealed class ForeachExecutor : DeclarativeActionExecutor<Foreach>
{
FormulaValue value = this._values[this._index];
await context.QueueStateUpdateAsync(Throw.IfNull(this.Model.Value), value).ConfigureAwait(false);
await context.QueueStateUpdateAsync(Throw.IfNull(this.Model.Value), value, cancellationToken).ConfigureAwait(false);
if (this.Model.Index is not null)
{
await context.QueueStateUpdateAsync(this.Model.Index.Path, FormulaValue.New(this._index)).ConfigureAwait(false);
await context.QueueStateUpdateAsync(this.Model.Index.Path, FormulaValue.New(this._index), cancellationToken).ConfigureAwait(false);
}
this._index++;
@@ -83,15 +83,15 @@ internal sealed class ForeachExecutor : DeclarativeActionExecutor<Foreach>
{
try
{
await context.QueueStateResetAsync(Throw.IfNull(this.Model.Value)).ConfigureAwait(false);
await context.QueueStateResetAsync(Throw.IfNull(this.Model.Value), cancellationToken).ConfigureAwait(false);
if (this.Model.Index is not null)
{
await context.QueueStateResetAsync(this.Model.Index).ConfigureAwait(false);
await context.QueueStateResetAsync(this.Model.Index, cancellationToken).ConfigureAwait(false);
}
}
finally
{
await context.RaiseCompletionEventAsync(this.Model).ConfigureAwait(false);
await context.RaiseCompletionEventAsync(this.Model, cancellationToken).ConfigureAwait(false);
}
}
}
@@ -65,7 +65,7 @@ internal sealed class QuestionExecutor(Question model, WorkflowFormulaState stat
}
else
{
await context.SendResultMessageAsync(this.Id, result: null, cancellationToken).ConfigureAwait(false);
await context.SendResultMessageAsync(this.Id, cancellationToken).ConfigureAwait(false);
}
return default;
@@ -75,7 +75,7 @@ internal sealed class QuestionExecutor(Question model, WorkflowFormulaState stat
{
int count = await this._promptCount.ReadAsync(context).ConfigureAwait(false);
InputRequest inputRequest = new(this.FormatPrompt(this.Model.Prompt));
await context.SendMessageAsync(inputRequest).ConfigureAwait(false);
await context.SendMessageAsync(inputRequest, targetId: null, cancellationToken).ConfigureAwait(false);
await this._promptCount.WriteAsync(context, count + 1).ConfigureAwait(false);
}
@@ -85,7 +85,7 @@ internal sealed class QuestionExecutor(Question model, WorkflowFormulaState stat
if (string.IsNullOrWhiteSpace(message.Value))
{
string unrecognizedResponse = this.FormatPrompt(this.Model.UnrecognizedPrompt);
await context.AddEventAsync(new MessageActivityEvent(unrecognizedResponse.Trim())).ConfigureAwait(false);
await context.AddEventAsync(new MessageActivityEvent(unrecognizedResponse.Trim()), cancellationToken).ConfigureAwait(false);
}
else
{
@@ -97,7 +97,7 @@ internal sealed class QuestionExecutor(Question model, WorkflowFormulaState stat
else
{
string invalidResponse = this.FormatPrompt(this.Model.InvalidPrompt);
await context.AddEventAsync(new MessageActivityEvent(invalidResponse.Trim())).ConfigureAwait(false);
await context.AddEventAsync(new MessageActivityEvent(invalidResponse.Trim()), cancellationToken).ConfigureAwait(false);
}
}
@@ -109,13 +109,13 @@ internal sealed class QuestionExecutor(Question model, WorkflowFormulaState stat
{
await this.AssignAsync(this.Model.Variable?.Path, extractedValue, context).ConfigureAwait(false);
await this._hasExecuted.WriteAsync(context, true).ConfigureAwait(false);
await context.SendResultMessageAsync(this.Id, result: null, cancellationToken).ConfigureAwait(false);
await context.SendResultMessageAsync(this.Id, cancellationToken).ConfigureAwait(false);
}
}
public async ValueTask CompleteAsync(IWorkflowContext context, ActionExecutorResult message, CancellationToken cancellationToken)
{
await context.RaiseCompletionEventAsync(this.Model).ConfigureAwait(false);
await context.RaiseCompletionEventAsync(this.Model, cancellationToken).ConfigureAwait(false);
}
private async ValueTask PromptAsync(IWorkflowContext context, CancellationToken cancellationToken)
@@ -128,8 +128,8 @@ internal sealed class QuestionExecutor(Question model, WorkflowFormulaState stat
DataValue defaultValue = this.Evaluator.GetValue(defaultValueExpression).Value;
await this.AssignAsync(this.Model.Variable?.Path, defaultValue.ToFormula(), context).ConfigureAwait(false);
string defaultValueResponse = this.FormatPrompt(this.Model.DefaultValueResponse);
await context.AddEventAsync(new MessageActivityEvent(defaultValueResponse.Trim())).ConfigureAwait(false);
await context.SendResultMessageAsync(this.Id, result: null, cancellationToken).ConfigureAwait(false);
await context.AddEventAsync(new MessageActivityEvent(defaultValueResponse.Trim()), cancellationToken).ConfigureAwait(false);
await context.SendResultMessageAsync(this.Id, cancellationToken).ConfigureAwait(false);
}
else
{
@@ -17,7 +17,7 @@ internal sealed class ResetVariableExecutor(ResetVariable model, WorkflowFormula
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken = default)
{
Throw.IfNull(this.Model.Variable, $"{nameof(this.Model)}.{nameof(model.Variable)}");
await context.QueueStateResetAsync(this.Model.Variable).ConfigureAwait(false);
await context.QueueStateResetAsync(this.Model.Variable, cancellationToken).ConfigureAwait(false);
Debug.WriteLine(
$"""
STATE: {this.GetType().Name} [{this.Id}]
@@ -18,7 +18,7 @@ internal sealed class SendActivityExecutor(SendActivity model, WorkflowFormulaSt
{
string activityText = this.Engine.Format(messageActivity.Text).Trim();
await context.AddEventAsync(new MessageActivityEvent(activityText.Trim())).ConfigureAwait(false);
await context.AddEventAsync(new MessageActivityEvent(activityText.Trim()), cancellationToken).ConfigureAwait(false);
}
return default;
@@ -274,6 +274,13 @@ internal sealed class WorkflowExpressionEngine
expression.VariableReference?.ToString() :
expression.ExpressionText;
return new(this._engine.Eval(expressionText), SensitivityLevel.None);
FormulaValue result = this._engine.Eval(expressionText);
if (result is ErrorValue errorValue)
{
throw new DeclarativeActionException(errorValue.Format());
}
return new(result, SensitivityLevel.None);
}
}
@@ -2,11 +2,11 @@
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
using Microsoft.Bot.ObjectModel;
using Microsoft.PowerFx;
using Microsoft.PowerFx.Types;
@@ -68,20 +68,25 @@ internal sealed class WorkflowFormulaState
return;
}
Stopwatch timer = Stopwatch.StartNew();
Debug.WriteLine("RESTORE CHECKPOINT - BEGIN");
await Task.WhenAll(RestorableScopes.Select(scopeName => ReadScopeAsync(scopeName))).ConfigureAwait(false);
Debug.WriteLine($"RESTORE CHECKPOINT - COMPLETE [{timer.Elapsed}]");
async Task ReadScopeAsync(string scopeName)
{
HashSet<string> keys = await context.ReadStateKeysAsync(scopeName).ConfigureAwait(false);
HashSet<string> keys = await context.ReadStateKeysAsync(scopeName, cancellationToken).ConfigureAwait(false);
foreach (string key in keys)
{
object? value = await context.ReadStateAsync<object>(key, scopeName).ConfigureAwait(false);
if (value is null or UnassignedValue)
PortableValue? value = await context.ReadStateAsync<PortableValue>(key, scopeName, cancellationToken).ConfigureAwait(false);
if (value is null)
{
value = FormulaValue.NewBlank();
this.Set(key, FormulaValue.NewBlank(), scopeName);
continue;
}
this.Set(key, value.ToFormula(), scopeName);
FormulaValue formulaValue = value.ToFormula();
this.Set(key, formulaValue, scopeName);
Debug.WriteLine($"RESTORED: {scopeName}.{key} => {formulaValue.Type}");
}
this.Bind(scopeName);
@@ -150,12 +150,12 @@ public static partial class AgentWorkflowBuilder
protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder) =>
routeBuilder
.AddHandler<string>((message, context) => this._pendingMessages.Add(new(ChatRole.User, message)))
.AddHandler<ChatMessage>((message, context) => this._pendingMessages.Add(message))
.AddHandler<IEnumerable<ChatMessage>>((messages, _) => this._pendingMessages.AddRange(messages))
.AddHandler<ChatMessage[]>((messages, _) => this._pendingMessages.AddRange(messages)) // TODO: Remove once https://github.com/microsoft/agent-framework/issues/782 is addressed
.AddHandler<List<ChatMessage>>((messages, _) => this._pendingMessages.AddRange(messages)) // TODO: Remove once https://github.com/microsoft/agent-framework/issues/782 is addressed
.AddHandler<TurnToken>(async (token, context) =>
.AddHandler<string>((message, _, __) => this._pendingMessages.Add(new(ChatRole.User, message)))
.AddHandler<ChatMessage>((message, _, __) => this._pendingMessages.Add(message))
.AddHandler<IEnumerable<ChatMessage>>((messages, _, __) => this._pendingMessages.AddRange(messages))
.AddHandler<ChatMessage[]>((messages, _, __) => this._pendingMessages.AddRange(messages)) // TODO: Remove once https://github.com/microsoft/agent-framework/issues/782 is addressed
.AddHandler<List<ChatMessage>>((messages, _, __) => this._pendingMessages.AddRange(messages)) // TODO: Remove once https://github.com/microsoft/agent-framework/issues/782 is addressed
.AddHandler<TurnToken>(async (token, context, cancellationToken) =>
{
List<ChatMessage> messages = [.. this._pendingMessages];
this._pendingMessages.Clear();
@@ -163,12 +163,12 @@ public static partial class AgentWorkflowBuilder
List<ChatMessage>? roleChanged = ChangeAssistantToUserForOtherParticipants(agent.DisplayName, messages);
List<AgentRunResponseUpdate> updates = [];
await foreach (var update in agent.RunStreamingAsync(messages).ConfigureAwait(false))
await foreach (var update in agent.RunStreamingAsync(messages, cancellationToken: cancellationToken).ConfigureAwait(false))
{
updates.Add(update);
if (token.EmitEvents is true)
{
await context.AddEventAsync(new AgentRunUpdateEvent(this.Id, update)).ConfigureAwait(false);
await context.AddEventAsync(new AgentRunUpdateEvent(this.Id, update), cancellationToken).ConfigureAwait(false);
}
}
@@ -181,8 +181,8 @@ public static partial class AgentWorkflowBuilder
messages.AddRange(updates.ToAgentRunResponse().Messages);
await context.SendMessageAsync(messages).ConfigureAwait(false);
await context.SendMessageAsync(token).ConfigureAwait(false);
await context.SendMessageAsync(messages, cancellationToken: cancellationToken).ConfigureAwait(false);
await context.SendMessageAsync(token, cancellationToken: cancellationToken).ConfigureAwait(false);
});
public ValueTask ResetAsync()
@@ -199,7 +199,7 @@ public static partial class AgentWorkflowBuilder
private sealed class OutputMessagesExecutor() : ChatProtocolExecutor("OutputMessages"), IResettableExecutor
{
protected override ValueTask TakeTurnAsync(List<ChatMessage> messages, IWorkflowContext context, bool? emitEvents, CancellationToken cancellationToken = default)
=> context.YieldOutputAsync(messages);
=> context.YieldOutputAsync(messages, cancellationToken);
ValueTask IResettableExecutor.ResetAsync() => this.ResetAsync();
}
@@ -209,10 +209,10 @@ public static partial class AgentWorkflowBuilder
{
protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder) =>
routeBuilder
.AddHandler<string>((message, context) => context.SendMessageAsync(new ChatMessage(ChatRole.User, message)))
.AddHandler<ChatMessage>((message, context) => context.SendMessageAsync(message))
.AddHandler<List<ChatMessage>>((messages, context) => context.SendMessageAsync(messages))
.AddHandler<TurnToken>((turnToken, context) => context.SendMessageAsync(turnToken));
.AddHandler<string>((message, context, cancellationToken) => context.SendMessageAsync(new ChatMessage(ChatRole.User, message), cancellationToken: cancellationToken))
.AddHandler<ChatMessage>((message, context, cancellationToken) => context.SendMessageAsync(message, cancellationToken: cancellationToken))
.AddHandler<List<ChatMessage>>((messages, context, cancellationToken) => context.SendMessageAsync(messages, cancellationToken: cancellationToken))
.AddHandler<TurnToken>((turnToken, context, cancellationToken) => context.SendMessageAsync(turnToken, cancellationToken: cancellationToken));
public ValueTask ResetAsync() => default;
}
@@ -224,7 +224,7 @@ public static partial class AgentWorkflowBuilder
private sealed class BatchChatMessagesToListExecutor(string id) : ChatProtocolExecutor(id), IResettableExecutor
{
protected override ValueTask TakeTurnAsync(List<ChatMessage> messages, IWorkflowContext context, bool? emitEvents, CancellationToken cancellationToken = default)
=> context.SendMessageAsync(messages);
=> context.SendMessageAsync(messages, cancellationToken: cancellationToken);
ValueTask IResettableExecutor.ResetAsync() => this.ResetAsync();
}
@@ -256,7 +256,7 @@ public static partial class AgentWorkflowBuilder
}
protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder) =>
routeBuilder.AddHandler<List<ChatMessage>>(async (messages, context) =>
routeBuilder.AddHandler<List<ChatMessage>>(async (messages, context, cancellationToken) =>
{
// TODO: https://github.com/microsoft/agent-framework/issues/784
// This locking should not be necessary.
@@ -273,7 +273,7 @@ public static partial class AgentWorkflowBuilder
var results = this._allResults;
this._allResults = new List<List<ChatMessage>>(this._expectedInputs);
await context.YieldOutputAsync(this._aggregator(results)).ConfigureAwait(false);
await context.YieldOutputAsync(this._aggregator(results), cancellationToken).ConfigureAwait(false);
}
});
@@ -457,16 +457,17 @@ public static partial class AgentWorkflowBuilder
protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder) =>
routeBuilder
.AddHandler<string>((message, context) => this._pendingMessages.Add(new(ChatRole.User, message)))
.AddHandler<ChatMessage>((message, context) => this._pendingMessages.Add(message))
.AddHandler<IEnumerable<ChatMessage>>((messages, _) => this._pendingMessages.AddRange(messages))
.AddHandler<ChatMessage[]>((messages, _) => this._pendingMessages.AddRange(messages)) // TODO: Remove once https://github.com/microsoft/agent-framework/issues/782 is addressed
.AddHandler<List<ChatMessage>>((messages, _) => this._pendingMessages.AddRange(messages)) // TODO: Remove once https://github.com/microsoft/agent-framework/issues/782 is addressed
.AddHandler<TurnToken>(async (token, context) =>
.AddHandler<string>((message, context, _) => this._pendingMessages.Add(new(ChatRole.User, message)))
.AddHandler<ChatMessage>((message, context, _) => this._pendingMessages.Add(message))
.AddHandler<IEnumerable<ChatMessage>>((messages, _, __) => this._pendingMessages.AddRange(messages))
.AddHandler<ChatMessage[]>((messages, _, __) => this._pendingMessages.AddRange(messages)) // TODO: Remove once https://github.com/microsoft/agent-framework/issues/782 is addressed
.AddHandler<List<ChatMessage>>((messages, _, __) => this._pendingMessages.AddRange(messages)) // TODO: Remove once https://github.com/microsoft/agent-framework/issues/782 is addressed
.AddHandler<TurnToken>(async (token, context, cancellationToken) =>
{
var messages = new List<ChatMessage>(this._pendingMessages);
this._pendingMessages.Clear();
await context.SendMessageAsync(new HandoffState(token, null, messages)).ConfigureAwait(false);
await context.SendMessageAsync(new HandoffState(token, null, messages), cancellationToken: cancellationToken)
.ConfigureAwait(false);
});
public ValueTask ResetAsync()
@@ -480,8 +481,8 @@ public static partial class AgentWorkflowBuilder
private sealed class EndHandoffsExecutor() : Executor("HandoffEnd"), IResettableExecutor
{
protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder) =>
routeBuilder.AddHandler<HandoffState>((handoff, context) =>
context.YieldOutputAsync(handoff.Messages));
routeBuilder.AddHandler<HandoffState>((handoff, context, cancellationToken) =>
context.YieldOutputAsync(handoff.Messages, cancellationToken));
public ValueTask ResetAsync() => default;
}
@@ -534,7 +535,7 @@ public static partial class AgentWorkflowBuilder
});
protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder) =>
routeBuilder.AddHandler<HandoffState>(async (handoffState, context) =>
routeBuilder.AddHandler<HandoffState>(async (handoffState, context, cancellationToken) =>
{
string? requestedHandoff = null;
List<AgentRunResponseUpdate> updates = [];
@@ -542,24 +543,31 @@ public static partial class AgentWorkflowBuilder
List<ChatMessage>? roleChanges = ChangeAssistantToUserForOtherParticipants(this._agent.DisplayName, allMessages);
await foreach (var update in this._agent.RunStreamingAsync(allMessages, options: this._agentOptions).ConfigureAwait(false))
await foreach (var update in this._agent.RunStreamingAsync(allMessages,
options: this._agentOptions,
cancellationToken: cancellationToken)
.ConfigureAwait(false))
{
await AddUpdateAsync(update).ConfigureAwait(false);
await AddUpdateAsync(update, cancellationToken).ConfigureAwait(false);
foreach (var c in update.Contents)
{
if (c is FunctionCallContent fcc && this._handoffFunctionNames.Contains(fcc.Name))
{
requestedHandoff = fcc.Name;
await AddUpdateAsync(new AgentRunResponseUpdate
{
AgentId = this._agent.Id,
AuthorName = this._agent.DisplayName,
Contents = [new FunctionResultContent(fcc.CallId, "Transferred.")],
CreatedAt = DateTimeOffset.UtcNow,
MessageId = Guid.NewGuid().ToString("N"),
Role = ChatRole.Tool,
}).ConfigureAwait(false);
await AddUpdateAsync(
new AgentRunResponseUpdate
{
AgentId = this._agent.Id,
AuthorName = this._agent.DisplayName,
Contents = [new FunctionResultContent(fcc.CallId, "Transferred.")],
CreatedAt = DateTimeOffset.UtcNow,
MessageId = Guid.NewGuid().ToString("N"),
Role = ChatRole.Tool,
},
cancellationToken
)
.ConfigureAwait(false);
}
}
}
@@ -568,14 +576,14 @@ public static partial class AgentWorkflowBuilder
ResetUserToAssistantForChangedRoles(roleChanges);
await context.SendMessageAsync(new HandoffState(handoffState.TurnToken, requestedHandoff, allMessages)).ConfigureAwait(false);
await context.SendMessageAsync(new HandoffState(handoffState.TurnToken, requestedHandoff, allMessages), cancellationToken: cancellationToken).ConfigureAwait(false);
async Task AddUpdateAsync(AgentRunResponseUpdate update)
async Task AddUpdateAsync(AgentRunResponseUpdate update, CancellationToken cancellationToken)
{
updates.Add(update);
if (handoffState.TurnToken.EmitEvents is true)
{
await context.AddEventAsync(new AgentRunUpdateEvent(this.Id, update)).ConfigureAwait(false);
await context.AddEventAsync(new AgentRunUpdateEvent(this.Id, update), cancellationToken).ConfigureAwait(false);
}
}
});
@@ -623,7 +631,8 @@ public static partial class AgentWorkflowBuilder
/// Selects the next agent to participate in the group chat based on the provided chat history and team.
/// </summary>
/// <param name="history">The chat history to consider.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
/// The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>The next <see cref="AIAgent"/> to speak. This agent must be part of the chat.</returns>
protected internal abstract ValueTask<AIAgent> SelectNextAgentAsync(
IReadOnlyList<ChatMessage> history,
@@ -633,7 +642,8 @@ public static partial class AgentWorkflowBuilder
/// Filters the chat history before it's passed to the next agent.
/// </summary>
/// <param name="history">The chat history to filter.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
/// The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>The filtered chat history.</returns>
protected internal virtual ValueTask<IEnumerable<ChatMessage>> UpdateHistoryAsync(
IReadOnlyList<ChatMessage> history,
@@ -644,7 +654,8 @@ public static partial class AgentWorkflowBuilder
/// Determines whether the group chat should be terminated based on the provided chat history and iteration count.
/// </summary>
/// <param name="history">The chat history to consider.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</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="bool"/> indicating whether the chat should be terminated.</returns>
protected internal virtual ValueTask<bool> ShouldTerminateAsync(
IReadOnlyList<ChatMessage> history,
@@ -789,35 +800,35 @@ public static partial class AgentWorkflowBuilder
private GroupChatManager? _manager;
protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder) => routeBuilder
.AddHandler<string>((message, context) => this._pendingMessages.Add(new(ChatRole.User, message)))
.AddHandler<ChatMessage>((message, context) => this._pendingMessages.Add(message))
.AddHandler<IEnumerable<ChatMessage>>((messages, _) => this._pendingMessages.AddRange(messages))
.AddHandler<ChatMessage[]>((messages, _) => this._pendingMessages.AddRange(messages)) // TODO: Remove once https://github.com/microsoft/agent-framework/issues/782 is addressed
.AddHandler<List<ChatMessage>>((messages, _) => this._pendingMessages.AddRange(messages)) // TODO: Remove once https://github.com/microsoft/agent-framework/issues/782 is addressed
.AddHandler<TurnToken>(async (token, context) =>
.AddHandler<string>((message, context, _) => this._pendingMessages.Add(new(ChatRole.User, message)))
.AddHandler<ChatMessage>((message, context, _) => this._pendingMessages.Add(message))
.AddHandler<IEnumerable<ChatMessage>>((messages, _, __) => this._pendingMessages.AddRange(messages))
.AddHandler<ChatMessage[]>((messages, _, __) => this._pendingMessages.AddRange(messages)) // TODO: Remove once https://github.com/microsoft/agent-framework/issues/782 is addressed
.AddHandler<List<ChatMessage>>((messages, _, __) => this._pendingMessages.AddRange(messages)) // TODO: Remove once https://github.com/microsoft/agent-framework/issues/782 is addressed
.AddHandler<TurnToken>(async (token, context, cancellationToken) =>
{
List<ChatMessage> messages = [.. this._pendingMessages];
this._pendingMessages.Clear();
this._manager ??= this._managerFactory(this._agents);
if (!await this._manager.ShouldTerminateAsync(messages).ConfigureAwait(false))
if (!await this._manager.ShouldTerminateAsync(messages, cancellationToken).ConfigureAwait(false))
{
var filtered = await this._manager.UpdateHistoryAsync(messages).ConfigureAwait(false);
var filtered = await this._manager.UpdateHistoryAsync(messages, cancellationToken).ConfigureAwait(false);
messages = filtered is null || ReferenceEquals(filtered, messages) ? messages : [.. filtered];
if (await this._manager.SelectNextAgentAsync(messages).ConfigureAwait(false) is AIAgent nextAgent &&
if (await this._manager.SelectNextAgentAsync(messages, cancellationToken).ConfigureAwait(false) is AIAgent nextAgent &&
this._agentMap.TryGetValue(nextAgent, out var executor))
{
this._manager.IterationCount++;
await context.SendMessageAsync(messages, executor.Id).ConfigureAwait(false);
await context.SendMessageAsync(token, executor.Id).ConfigureAwait(false);
await context.SendMessageAsync(messages, executor.Id, cancellationToken).ConfigureAwait(false);
await context.SendMessageAsync(token, executor.Id, cancellationToken).ConfigureAwait(false);
return;
}
}
this._manager = null;
await context.YieldOutputAsync(messages).ConfigureAwait(false);
await context.YieldOutputAsync(messages, cancellationToken).ConfigureAwait(false);
});
public ValueTask ResetAsync()
@@ -28,7 +28,7 @@ public class AggregatingExecutor<TInput, TAggregate>(string id,
private TAggregate? _runningAggregate;
/// <inheritdoc/>
public override ValueTask<TAggregate?> HandleAsync(TInput message, IWorkflowContext context)
public override ValueTask<TAggregate?> HandleAsync(TInput message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
this._runningAggregate = aggregator(this._runningAggregate, message);
return new(this._runningAggregate);
@@ -37,7 +37,7 @@ public class AggregatingExecutor<TInput, TAggregate>(string id,
/// <inheritdoc/>
protected internal override async ValueTask OnCheckpointingAsync(IWorkflowContext context, CancellationToken cancellationToken = default)
{
await context.QueueStateUpdateAsync(AggregateStateKey, this._runningAggregate).ConfigureAwait(false);
await context.QueueStateUpdateAsync(AggregateStateKey, this._runningAggregate, cancellationToken: cancellationToken).ConfigureAwait(false);
await base.OnCheckpointingAsync(context, cancellationToken).ConfigureAwait(false);
}
@@ -47,6 +47,6 @@ public class AggregatingExecutor<TInput, TAggregate>(string id,
{
await base.OnCheckpointRestoredAsync(context, cancellationToken).ConfigureAwait(false);
this._runningAggregate = await context.ReadStateAsync<TAggregate>(AggregateStateKey).ConfigureAwait(false);
this._runningAggregate = await context.ReadStateAsync<TAggregate>(AggregateStateKey, cancellationToken: cancellationToken).ConfigureAwait(false);
}
}
@@ -10,7 +10,7 @@ internal sealed class AsyncBarrier()
{
private readonly InitLocked<TaskCompletionSource<object>> _completionSource = new();
public async ValueTask<bool> JoinAsync(CancellationToken cancellation = default)
public async ValueTask<bool> JoinAsync(CancellationToken cancellationToken = default)
{
this._completionSource.Init(() => new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously));
TaskCompletionSource<object> completionSource = this._completionSource.Get()!;
@@ -19,10 +19,10 @@ internal sealed class AsyncBarrier()
// should not cancel the entire barrier.
TaskCompletionSource<object> cancellationSource = new();
using CancellationTokenRegistration registration = cancellation.Register(() => cancellationSource.SetResult(new()));
using CancellationTokenRegistration registration = cancellationToken.Register(() => cancellationSource.SetResult(new()));
await Task.WhenAny(completionSource.Task, cancellationSource.Task).ConfigureAwait(false);
return !cancellation.IsCancellationRequested;
return !cancellationToken.IsCancellationRequested;
}
public bool ReleaseBarrier()
@@ -12,12 +12,13 @@ internal sealed class AsyncCoordinator
/// <summary>
/// Wait for the Coordination owner to mark the next coordination point, then continue execution.
/// </summary>
/// <param name="cancellation">A cancellation token that can be used to cancel the wait.</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 is <see langword="true"/>
/// if the wait was completed; otherwise, for example, if the wait was cancelled, <see langword="false"/>.
/// </returns>
public async ValueTask<bool> WaitForCoordinationAsync(CancellationToken cancellation = default)
public async ValueTask<bool> WaitForCoordinationAsync(CancellationToken cancellationToken = default)
{
// There is a chance that we might get a stale barrier that is getting released if there is a
// release happening concurrently with this call. This is by design, and should be considered
@@ -26,7 +27,7 @@ internal sealed class AsyncCoordinator
?? Interlocked.CompareExchange(ref this._coordinationBarrier, new(), null)
?? this._coordinationBarrier!; // Re-read after setting
return await actualBarrier.JoinAsync(cancellation).ConfigureAwait(false);
return await actualBarrier.JoinAsync(cancellationToken).ConfigureAwait(false);
}
/// <summary>
@@ -30,19 +30,21 @@ internal abstract class ChatProtocolExecutor(string id, ChatProtocolExecutorOpti
{
if (this._stringMessageChatRole.HasValue)
{
routeBuilder = routeBuilder.AddHandler<string>((message, _) => this._pendingMessages.Add(new(this._stringMessageChatRole.Value, message)));
routeBuilder = routeBuilder.AddHandler<string>((message, _, __) => this._pendingMessages.Add(new(this._stringMessageChatRole.Value, message)));
}
return routeBuilder.AddHandler<ChatMessage>((message, _) => this._pendingMessages.Add(message))
.AddHandler<List<ChatMessage>>((messages, _) => this._pendingMessages.AddRange(messages))
// Routing requires exact type matches. The runtime may dispatch either List<ChatMessage> or ChatMessage[].
return routeBuilder.AddHandler<ChatMessage>((message, _, __) => this._pendingMessages.Add(message))
.AddHandler<List<ChatMessage>>((messages, _, __) => this._pendingMessages.AddRange(messages))
.AddHandler<ChatMessage[]>((messages, _, __) => this._pendingMessages.AddRange(messages))
.AddHandler<TurnToken>(this.TakeTurnAsync);
}
public async ValueTask TakeTurnAsync(TurnToken token, IWorkflowContext context)
public async ValueTask TakeTurnAsync(TurnToken token, IWorkflowContext context, CancellationToken cancellationToken = default)
{
await this.TakeTurnAsync(this._pendingMessages, context, token.EmitEvents).ConfigureAwait(false);
await this.TakeTurnAsync(this._pendingMessages, context, token.EmitEvents, cancellationToken).ConfigureAwait(false);
this._pendingMessages = [];
await context.SendMessageAsync(token).ConfigureAwait(false);
await context.SendMessageAsync(token, cancellationToken: cancellationToken).ConfigureAwait(false);
}
protected abstract ValueTask TakeTurnAsync(List<ChatMessage> messages, IWorkflowContext context, bool? emitEvents, CancellationToken cancellationToken = default);
@@ -54,7 +56,7 @@ internal abstract class ChatProtocolExecutor(string id, ChatProtocolExecutorOpti
if (this._pendingMessages.Count > 0)
{
JsonElement messagesValue = this._pendingMessages.Serialize();
messagesTask = context.QueueStateUpdateAsync(PendingMessagesStateKey, messagesValue).AsTask();
messagesTask = context.QueueStateUpdateAsync(PendingMessagesStateKey, messagesValue, cancellationToken: cancellationToken).AsTask();
}
await messagesTask.ConfigureAwait(false);
@@ -62,7 +64,7 @@ internal abstract class ChatProtocolExecutor(string id, ChatProtocolExecutorOpti
protected internal override async ValueTask OnCheckpointRestoredAsync(IWorkflowContext context, CancellationToken cancellationToken = default)
{
JsonElement? messagesValue = await context.ReadStateAsync<JsonElement?>(PendingMessagesStateKey).ConfigureAwait(false);
JsonElement? messagesValue = await context.ReadStateAsync<JsonElement?>(PendingMessagesStateKey, cancellationToken: cancellationToken).ConfigureAwait(false);
if (messagesValue.HasValue)
{
List<ChatMessage> messages = messagesValue.Value.DeserializeMessages();
@@ -56,7 +56,12 @@ public sealed class FileSystemJsonCheckpointStore : JsonCheckpointStore, IDispos
{
// read the lines of indexfile and parse them as CheckpointInfos
this.CheckpointIndex = [];
using StreamReader reader = new(this._indexFile, encoding: Encoding.UTF8, detectEncodingFromByteOrderMarks: false, bufferSize: -1, leaveOpen: true);
#if NET
const int BufferSize = -1;
#else
const int BufferSize = 1024;
#endif
using StreamReader reader = new(this._indexFile, encoding: Encoding.UTF8, detectEncodingFromByteOrderMarks: false, BufferSize, leaveOpen: true);
while (reader.ReadLine() is string line)
{
if (JsonSerializer.Deserialize(line, KeyTypeInfo) is { } info)
@@ -65,9 +70,9 @@ public sealed class FileSystemJsonCheckpointStore : JsonCheckpointStore, IDispos
}
}
}
catch
catch (Exception exception)
{
throw new InvalidOperationException($"Could not load store at '{directory.FullName}'. Index corrupted.");
throw new InvalidOperationException($"Could not load store at '{directory.FullName}'. Index corrupted.", exception);
}
}
@@ -44,19 +44,14 @@ internal sealed class AsyncRunHandle : ICheckpointingHandle, IAsyncDisposable
}
}
//private readonly AsyncCoordinator _waitForResponseCoordinator = new();
//public ValueTask<bool> WaitForNextInputAsync(CancellationToken cancellation = default)
// => this._waitForResponseCoordinator.WaitForCoordinationAsync(cancellation);
public string RunId => this._stepRunner.RunId;
public IReadOnlyList<CheckpointInfo> Checkpoints => this._checkpointingHandle.Checkpoints;
public ValueTask<RunStatus> GetStatusAsync(CancellationToken cancellation = default)
=> this._eventStream.GetStatusAsync(cancellation);
public ValueTask<RunStatus> GetStatusAsync(CancellationToken cancellationToken = default)
=> this._eventStream.GetStatusAsync(cancellationToken);
public async IAsyncEnumerable<WorkflowEvent> TakeEventStreamAsync(bool blockOnPendingRequest, [EnumeratorCancellation] CancellationToken cancellation = default)
public async IAsyncEnumerable<WorkflowEvent> TakeEventStreamAsync(bool blockOnPendingRequest, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
//Debug.Assert(breakOnHalt);
// Enforce single active enumerator (this runs when enumeration begins)
@@ -68,7 +63,7 @@ internal sealed class AsyncRunHandle : ICheckpointingHandle, IAsyncDisposable
CancellationTokenSource? linked = null;
try
{
linked = CancellationTokenSource.CreateLinkedTokenSource(cancellation, this._endRunSource.Token);
linked = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, this._endRunSource.Token);
var token = linked.Token;
// Build the inner stream before the loop so synchronous exceptions still release the gate
@@ -92,21 +87,21 @@ internal sealed class AsyncRunHandle : ICheckpointingHandle, IAsyncDisposable
}
}
public ValueTask<bool> IsValidInputTypeAsync<T>(CancellationToken cancellation = default)
=> this._stepRunner.IsValidInputTypeAsync<T>(cancellation);
public ValueTask<bool> IsValidInputTypeAsync<T>(CancellationToken cancellationToken = default)
=> this._stepRunner.IsValidInputTypeAsync<T>(cancellationToken);
public async ValueTask<bool> EnqueueMessageAsync<T>(T message, CancellationToken cancellation = default)
public async ValueTask<bool> EnqueueMessageAsync<T>(T message, CancellationToken cancellationToken = default)
{
if (message is ExternalResponse response)
{
// EnqueueResponseAsync handles signaling
await this.EnqueueResponseAsync(response, cancellation)
await this.EnqueueResponseAsync(response, cancellationToken)
.ConfigureAwait(false);
return true;
}
bool result = await this._stepRunner.EnqueueMessageAsync(message, cancellation)
bool result = await this._stepRunner.EnqueueMessageAsync(message, cancellationToken)
.ConfigureAwait(false);
// Signal the run loop that new input is available
@@ -115,7 +110,7 @@ internal sealed class AsyncRunHandle : ICheckpointingHandle, IAsyncDisposable
return result;
}
public async ValueTask<bool> EnqueueMessageUntypedAsync([NotNull] object message, Type? declaredType = null, CancellationToken cancellation = default)
public async ValueTask<bool> EnqueueMessageUntypedAsync([NotNull] object message, Type? declaredType = null, CancellationToken cancellationToken = default)
{
if (declaredType?.IsInstanceOfType(message) == false)
{
@@ -125,7 +120,7 @@ internal sealed class AsyncRunHandle : ICheckpointingHandle, IAsyncDisposable
if (declaredType != null && typeof(ExternalResponse).IsAssignableFrom(declaredType))
{
// EnqueueResponseAsync handles signaling
await this.EnqueueResponseAsync((ExternalResponse)message, cancellation)
await this.EnqueueResponseAsync((ExternalResponse)message, cancellationToken)
.ConfigureAwait(false);
return true;
@@ -133,13 +128,13 @@ internal sealed class AsyncRunHandle : ICheckpointingHandle, IAsyncDisposable
else if (declaredType == null && message is ExternalResponse response)
{
// EnqueueResponseAsync handles signaling
await this.EnqueueResponseAsync(response, cancellation)
await this.EnqueueResponseAsync(response, cancellationToken)
.ConfigureAwait(false);
return true;
}
bool result = await this._stepRunner.EnqueueMessageUntypedAsync(message, declaredType ?? message.GetType(), cancellation)
bool result = await this._stepRunner.EnqueueMessageUntypedAsync(message, declaredType ?? message.GetType(), cancellationToken)
.ConfigureAwait(false);
// Signal the run loop that new input is available
@@ -148,9 +143,9 @@ internal sealed class AsyncRunHandle : ICheckpointingHandle, IAsyncDisposable
return result;
}
public async ValueTask EnqueueResponseAsync(ExternalResponse response, CancellationToken cancellation = default)
public async ValueTask EnqueueResponseAsync(ExternalResponse response, CancellationToken cancellationToken = default)
{
await this._stepRunner.EnqueueResponseAsync(response, cancellation).ConfigureAwait(false);
await this._stepRunner.EnqueueResponseAsync(response, cancellationToken).ConfigureAwait(false);
// Signal the run loop that new input is available
this.SignalInputToRunLoop();
@@ -14,33 +14,33 @@ internal static class AsyncRunHandleExtensions
return new Checkpointed<TRunType>(run, runHandle);
}
public static async ValueTask<StreamingRun> EnqueueAndStreamAsync<TInput>(this AsyncRunHandle runHandle, TInput input, CancellationToken cancellation = default)
public static async ValueTask<StreamingRun> EnqueueAndStreamAsync<TInput>(this AsyncRunHandle runHandle, TInput input, CancellationToken cancellationToken = default)
{
await runHandle.EnqueueMessageAsync(input, cancellation).ConfigureAwait(false);
await runHandle.EnqueueMessageAsync(input, cancellationToken).ConfigureAwait(false);
return new(runHandle);
}
public static async ValueTask<StreamingRun> EnqueueUntypedAndStreamAsync(this AsyncRunHandle runHandle, object input, CancellationToken cancellation = default)
public static async ValueTask<StreamingRun> EnqueueUntypedAndStreamAsync(this AsyncRunHandle runHandle, object input, CancellationToken cancellationToken = default)
{
await runHandle.EnqueueMessageUntypedAsync(input, cancellation: cancellation).ConfigureAwait(false);
await runHandle.EnqueueMessageUntypedAsync(input, cancellationToken: cancellationToken).ConfigureAwait(false);
return new(runHandle);
}
public static async ValueTask<Run> EnqueueAndRunAsync<TInput>(this AsyncRunHandle runHandle, TInput input, CancellationToken cancellation = default)
public static async ValueTask<Run> EnqueueAndRunAsync<TInput>(this AsyncRunHandle runHandle, TInput input, CancellationToken cancellationToken = default)
{
await runHandle.EnqueueMessageAsync(input, cancellation).ConfigureAwait(false);
await runHandle.EnqueueMessageAsync(input, cancellationToken).ConfigureAwait(false);
Run run = new(runHandle);
await run.RunToNextHaltAsync(cancellation).ConfigureAwait(false);
await run.RunToNextHaltAsync(cancellationToken).ConfigureAwait(false);
return run;
}
public static async ValueTask<Run> EnqueueUntypedAndRunAsync(this AsyncRunHandle runHandle, object input, CancellationToken cancellation = default)
public static async ValueTask<Run> EnqueueUntypedAndRunAsync(this AsyncRunHandle runHandle, object input, CancellationToken cancellationToken = default)
{
await runHandle.EnqueueMessageUntypedAsync(input, cancellation: cancellation).ConfigureAwait(false);
await runHandle.EnqueueMessageUntypedAsync(input, cancellationToken: cancellationToken).ConfigureAwait(false);
Run run = new(runHandle);
await run.RunToNextHaltAsync(cancellation).ConfigureAwait(false);
await run.RunToNextHaltAsync(cancellationToken).ConfigureAwait(false);
return run;
}
}
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Threading;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI.Workflows.Execution;
@@ -26,16 +27,22 @@ internal sealed class CallResult
/// </summary>
public Exception? Exception { get; init; }
/// <summary>
/// Indicated whether the call was cancelled (e.g., via a <see cref="CancellationToken"/>).
/// </summary>
public bool IsCancelled { get; init; }
/// <summary>
/// Indicates whether the call was successful. A call is considered successful if it returned
/// without throwing an exception.
/// </summary>
public bool IsSuccess => this.Exception is null;
public bool IsSuccess => this.Exception is null && !this.IsCancelled;
private CallResult(bool isVoid = false)
private CallResult(bool isVoid = false, bool isCancelled = false)
{
// Private constructor to enforce use of static methods.
this.IsVoid = isVoid;
this.IsCancelled = isCancelled;
}
/// <summary>
@@ -51,6 +58,14 @@ internal sealed class CallResult
/// <returns>A <see cref="CallResult"/> indicating the result of the call.</returns>
public static CallResult ReturnVoid() => new(isVoid: true);
/// <summary>
/// Create a <see cref="CallResult"/> indicating that the call was cancelled.
/// </summary>
/// <param name="wasVoid">A boolean specifying whether the call was void (was not expected to return
/// a value).</param>
/// <returns>A <see cref="CallResult"/> indicating the result of the call.</returns>
public static CallResult Cancelled(bool wasVoid) => new(wasVoid, isCancelled: true);
/// <summary>
/// Create a <see cref="CallResult"/> indicating that an exception was raised during the call.
/// </summary>
@@ -1,11 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.Agents.AI.Workflows.Execution;
internal interface IInputCoordinator
{
ValueTask<bool> WaitForNextInputAsync(CancellationToken cancellation = default);
}
@@ -15,7 +15,7 @@ internal interface IRunEventStream : IAsyncDisposable
// this cannot be cancelled
ValueTask StopAsync();
ValueTask<RunStatus> GetStatusAsync(CancellationToken cancellation = default);
ValueTask<RunStatus> GetStatusAsync(CancellationToken cancellationToken = default);
IAsyncEnumerable<WorkflowEvent> TakeEventStreamAsync(bool blockOnPendingRequest, CancellationToken cancellation = default);
IAsyncEnumerable<WorkflowEvent> TakeEventStreamAsync(bool blockOnPendingRequest, CancellationToken cancellationToken = default);
}
@@ -1,16 +1,17 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.Agents.AI.Workflows.Execution;
internal interface IRunnerContext : IExternalRequestSink, ISuperStepJoinContext
{
ValueTask AddEventAsync(WorkflowEvent workflowEvent);
ValueTask SendMessageAsync(string sourceId, object message, string? targetId = null);
ValueTask AddEventAsync(WorkflowEvent workflowEvent, CancellationToken cancellationToken = default);
ValueTask SendMessageAsync(string sourceId, object message, string? targetId = null, CancellationToken cancellationToken = default);
ValueTask<StepContext> AdvanceAsync();
ValueTask<StepContext> AdvanceAsync(CancellationToken cancellationToken = default);
IWorkflowContext Bind(string executorId, Dictionary<string, string>? traceContext = null);
ValueTask<Executor> EnsureExecutorAsync(string executorId, IStepTracer? tracer);
ValueTask<Executor> EnsureExecutorAsync(string executorId, IStepTracer? tracer, CancellationToken cancellationToken = default);
}
@@ -10,8 +10,8 @@ internal interface ISuperStepJoinContext
{
bool WithCheckpointing { get; }
ValueTask ForwardWorkflowEventAsync(WorkflowEvent workflowEvent, CancellationToken cancellation = default);
ValueTask SendMessageAsync<TMessage>(string senderId, [DisallowNull] TMessage message, CancellationToken cancellation = default);
ValueTask ForwardWorkflowEventAsync(WorkflowEvent workflowEvent, CancellationToken cancellationToken = default);
ValueTask SendMessageAsync<TMessage>(string senderId, [DisallowNull] TMessage message, CancellationToken cancellationToken = default);
ValueTask AttachSuperstepAsync(ISuperStepRunner superStepRunner, CancellationToken cancellation = default);
ValueTask AttachSuperstepAsync(ISuperStepRunner superStepRunner, CancellationToken cancellationToken = default);
}
@@ -15,11 +15,11 @@ internal interface ISuperStepRunner
bool HasUnservicedRequests { get; }
bool HasUnprocessedMessages { get; }
ValueTask EnqueueResponseAsync(ExternalResponse response, CancellationToken cancellation = default);
ValueTask EnqueueResponseAsync(ExternalResponse response, CancellationToken cancellationToken = default);
ValueTask<bool> IsValidInputTypeAsync<T>(CancellationToken cancellation = default);
ValueTask<bool> EnqueueMessageAsync<T>(T message, CancellationToken cancellation = default);
ValueTask<bool> EnqueueMessageUntypedAsync(object message, Type declaredType, CancellationToken cancellation = default);
ValueTask<bool> IsValidInputTypeAsync<T>(CancellationToken cancellationToken = default);
ValueTask<bool> EnqueueMessageAsync<T>(T message, CancellationToken cancellationToken = default);
ValueTask<bool> EnqueueMessageUntypedAsync(object message, Type declaredType, CancellationToken cancellationToken = default);
ConcurrentEventSink OutgoingEvents { get; }
@@ -33,10 +33,10 @@ internal sealed class InputWaiter : IDisposable
}
}
public Task WaitForInputAsync(CancellationToken cancellation = default) => this.WaitForInputAsync(null, cancellation);
public Task WaitForInputAsync(CancellationToken cancellationToken = default) => this.WaitForInputAsync(null, cancellationToken);
public async Task WaitForInputAsync(TimeSpan? timeout = null, CancellationToken cancellation = default)
public async Task WaitForInputAsync(TimeSpan? timeout = null, CancellationToken cancellationToken = default)
{
await this._inputSignal.WaitAsync(timeout ?? TimeSpan.FromMilliseconds(-1), cancellation).ConfigureAwait(false);
await this._inputSignal.WaitAsync(timeout ?? TimeSpan.FromMilliseconds(-1), cancellationToken).ConfigureAwait(false);
}
}
@@ -22,7 +22,7 @@ internal sealed class LockstepRunEventStream : IRunEventStream
private readonly ISuperStepRunner _stepRunner;
public ValueTask<RunStatus> GetStatusAsync(CancellationToken cancellation = default) => new(this.RunStatus);
public ValueTask<RunStatus> GetStatusAsync(CancellationToken cancellationToken = default) => new(this.RunStatus);
public LockstepRunEventStream(ISuperStepRunner stepRunner)
{
@@ -36,7 +36,7 @@ internal sealed class LockstepRunEventStream : IRunEventStream
// No-op for lockstep execution
}
public async IAsyncEnumerable<WorkflowEvent> TakeEventStreamAsync(bool blockOnPendingRequest, [EnumeratorCancellation] CancellationToken cancellation = default)
public async IAsyncEnumerable<WorkflowEvent> TakeEventStreamAsync(bool blockOnPendingRequest, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
#if NET
ObjectDisposedException.ThrowIf(Volatile.Read(ref this._isDisposed) == 1, this);
@@ -47,7 +47,7 @@ internal sealed class LockstepRunEventStream : IRunEventStream
}
#endif
CancellationTokenSource linkedSource = CancellationTokenSource.CreateLinkedTokenSource(this._stopCancellation.Token, cancellation);
CancellationTokenSource linkedSource = CancellationTokenSource.CreateLinkedTokenSource(this._stopCancellation.Token, cancellationToken);
ConcurrentQueue<WorkflowEvent> eventSink = [];
@@ -4,6 +4,7 @@ using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows.Checkpointing;
using Microsoft.Shared.Diagnostics;
@@ -11,12 +12,14 @@ using CatchAllF =
System.Func<
Microsoft.Agents.AI.Workflows.PortableValue, // message
Microsoft.Agents.AI.Workflows.IWorkflowContext, // context
System.Threading.CancellationToken, // cancellation
System.Threading.Tasks.ValueTask<Microsoft.Agents.AI.Workflows.Execution.CallResult>
>;
using MessageHandlerF =
System.Func<
object, // message
Microsoft.Agents.AI.Workflows.IWorkflowContext, // context
System.Threading.CancellationToken, // cancellation
System.Threading.Tasks.ValueTask<Microsoft.Agents.AI.Workflows.Execution.CallResult>
>;
@@ -56,7 +59,7 @@ internal sealed class MessageRouter
public HashSet<Type> DefaultOutputTypes { get; }
public async ValueTask<CallResult?> RouteMessageAsync(object message, IWorkflowContext context, bool requireRoute = false)
public async ValueTask<CallResult?> RouteMessageAsync(object message, IWorkflowContext context, bool requireRoute = false, CancellationToken cancellationToken = default)
{
Throw.IfNull(message);
@@ -74,13 +77,13 @@ internal sealed class MessageRouter
{
if (this._typedHandlers.TryGetValue(message.GetType(), out MessageHandlerF? handler))
{
result = await handler(message, context).ConfigureAwait(false);
result = await handler(message, context, cancellationToken).ConfigureAwait(false);
}
else if (this.HasCatchAll)
{
portableValue ??= new PortableValue(message);
result = await this._catchAllFunc(portableValue, context).ConfigureAwait(false);
result = await this._catchAllFunc(portableValue, context, cancellationToken).ConfigureAwait(false);
}
}
catch (Exception e)
@@ -99,6 +99,13 @@ internal sealed class StateManager
public ValueTask<T?> ReadStateAsync<T>(ScopeId scopeId, string key)
{
if (typeof(T) == typeof(object))
{
// Reading as object will break across serialize/deserialize boundaries, e.g. checkpointing, distributed runtime, etc.
// Disabled pending upstream updates for this change; see https://github.com/microsoft/agent-framework/issues/1369
//throw new NotSupportedException("Reading state as 'object' is not supported. Use 'PortableValue' instead for variants.");
}
Throw.IfNullOrEmpty(key);
UpdateKey stateKey = new(scopeId, key);
@@ -116,6 +123,16 @@ internal sealed class StateManager
{
return new((T?)result.Value);
}
else if (result.Value == null)
{
// Technically should only happen if T is nullable, but we don't have the ability to express that
// so we cannot `return new((T?)null);` directly.
return new((T?)default);
}
else if (typeof(T) == typeof(PortableValue))
{
return new((T)(object)new PortableValue(result.Value));
}
throw new InvalidOperationException($"State for key '{key}' in scope '{scopeId}' is not of type '{typeof(T).Name}'.");
}
@@ -51,6 +51,13 @@ internal sealed class StateScope
Throw.IfNullOrEmpty(key);
if (this._stateData.TryGetValue(key, out PortableValue? value))
{
if (typeof(T) == typeof(PortableValue) && !value.TypeId.IsMatch(typeof(PortableValue)))
{
// value is PortableValue, and we do not need to unwrap a PortableValue instance inside of it
// Unfortunately we need to cast through object here.
return new((T)(object)value);
}
return new(value.As<T>());
}
@@ -50,10 +50,10 @@ internal sealed class StreamingRunEventStream : IRunEventStream
}
}
private async Task RunLoopAsync(CancellationToken cancellation)
private async Task RunLoopAsync(CancellationToken cancellationToken)
{
using CancellationTokenSource errorSource = new();
CancellationTokenSource linkedSource = CancellationTokenSource.CreateLinkedTokenSource(errorSource.Token, cancellation);
CancellationTokenSource linkedSource = CancellationTokenSource.CreateLinkedTokenSource(errorSource.Token, cancellationToken);
// Subscribe to events - they will flow directly to the channel as they're raised
this._stepRunner.OutgoingEvents.EventRaised += OnEventRaisedAsync;
@@ -62,7 +62,7 @@ internal sealed class StreamingRunEventStream : IRunEventStream
{
// Wait for the first input before starting
// The consumer will call EnqueueMessageAsync which signals the run loop
await this._inputWaiter.WaitForInputAsync(cancellation: linkedSource.Token).ConfigureAwait(false);
await this._inputWaiter.WaitForInputAsync(cancellationToken: linkedSource.Token).ConfigureAwait(false);
this._runStatus = RunStatus.Running;
@@ -134,7 +134,7 @@ internal sealed class StreamingRunEventStream : IRunEventStream
public async IAsyncEnumerable<WorkflowEvent> TakeEventStreamAsync(
bool blockOnPendingRequest,
[EnumeratorCancellation] CancellationToken cancellation = default)
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
// Get the current epoch - we'll only respond to completion signals from this epoch or later
int myEpoch = Volatile.Read(ref this._completionEpoch) + 1;
@@ -143,7 +143,7 @@ internal sealed class StreamingRunEventStream : IRunEventStream
// Note: When cancellation is requested, ReadAllAsync may throw OperationCanceledException
// or may complete the enumeration. We check IsCancellationRequested explicitly at superstep
// boundaries to ensure clean cancellation.
await foreach (WorkflowEvent evt in this._eventChannel.Reader.ReadAllAsync(cancellation).ConfigureAwait(false))
await foreach (WorkflowEvent evt in this._eventChannel.Reader.ReadAllAsync(cancellationToken).ConfigureAwait(false))
{
// Filter out internal signals used for run loop coordination
if (evt is InternalHaltSignal completionSignal)
@@ -156,7 +156,7 @@ internal sealed class StreamingRunEventStream : IRunEventStream
// Check for cancellation at superstep boundaries (before processing completion signal)
// This allows consumers to stop reading events cleanly between supersteps
if (cancellation.IsCancellationRequested)
if (cancellationToken.IsCancellationRequested)
{
yield break;
}
@@ -186,7 +186,7 @@ internal sealed class StreamingRunEventStream : IRunEventStream
yield break;
}
if (cancellation.IsCancellationRequested)
if (cancellationToken.IsCancellationRequested)
{
yield break;
}
@@ -195,7 +195,7 @@ internal sealed class StreamingRunEventStream : IRunEventStream
}
}
public ValueTask<RunStatus> GetStatusAsync(CancellationToken cancellation = default)
public ValueTask<RunStatus> GetStatusAsync(CancellationToken cancellationToken = default)
{
// Thread-safe read of status (enum is read atomically on most platforms)
return new ValueTask<RunStatus>(this._runStatus);
@@ -90,10 +90,12 @@ public abstract class Executor : IIdentified
/// <param name="messageType">The "declared" type of the message (captured when it was being sent). This is
/// used to enable routing messages as their base types, in absence of true polymorphic type routing.</param>
/// <param name="context">The workflow context in which the executor executes.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
/// The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A ValueTask representing the asynchronous operation, wrapping the output from the executor.</returns>
/// <exception cref="NotSupportedException">No handler found for the message type.</exception>
/// <exception cref="TargetInvocationException">An exception is generated while handling the message.</exception>
public async ValueTask<object?> ExecuteAsync(object message, TypeId messageType, IWorkflowContext context)
public async ValueTask<object?> ExecuteAsync(object message, TypeId messageType, IWorkflowContext context, CancellationToken cancellationToken = default)
{
using var activity = s_activitySource.StartActivity(ActivityNames.ExecutorProcess, ActivityKind.Internal);
activity?.SetTag(Tags.ExecutorId, this.Id)
@@ -101,9 +103,9 @@ public abstract class Executor : IIdentified
.SetTag(Tags.MessageType, messageType.TypeName)
.CreateSourceLinks(context.TraceContext);
await context.AddEventAsync(new ExecutorInvokedEvent(this.Id, message)).ConfigureAwait(false);
await context.AddEventAsync(new ExecutorInvokedEvent(this.Id, message), cancellationToken).ConfigureAwait(false);
CallResult? result = await this.Router.RouteMessageAsync(message, context, requireRoute: true)
CallResult? result = await this.Router.RouteMessageAsync(message, context, requireRoute: true, cancellationToken)
.ConfigureAwait(false);
ExecutorEvent executionResult;
@@ -116,7 +118,7 @@ public abstract class Executor : IIdentified
executionResult = new ExecutorFailedEvent(this.Id, result.Exception);
}
await context.AddEventAsync(executionResult).ConfigureAwait(false);
await context.AddEventAsync(executionResult, cancellationToken).ConfigureAwait(false);
if (result is null)
{
@@ -137,11 +139,11 @@ public abstract class Executor : IIdentified
// If we had a real return type, raise it as a SendMessage; TODO: Should we have a way to disable this behaviour?
if (result.Result is not null && this.Options.AutoSendMessageHandlerResultObject)
{
await context.SendMessageAsync(result.Result).ConfigureAwait(false);
await context.SendMessageAsync(result.Result, cancellationToken: cancellationToken).ConfigureAwait(false);
}
if (result.Result is not null && this.Options.AutoYieldOutputHandlerResultObject)
{
await context.YieldOutputAsync(result.Result).ConfigureAwait(false);
await context.YieldOutputAsync(result.Result, cancellationToken).ConfigureAwait(false);
}
return result.Result;
@@ -152,7 +154,8 @@ public abstract class Executor : IIdentified
/// </summary>
/// <param name="context">The workflow context.</param>
/// <returns>A ValueTask representing the asynchronous operation.</returns>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
/// The default is <see cref="CancellationToken.None"/>.</param>
protected internal virtual ValueTask OnCheckpointingAsync(IWorkflowContext context, CancellationToken cancellationToken = default) => default;
/// <summary>
@@ -160,7 +163,8 @@ public abstract class Executor : IIdentified
/// </summary>
/// <param name="context">The workflow context.</param>
/// <returns>A ValueTask representing the asynchronous operation.</returns>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
/// The default is <see cref="CancellationToken.None"/>.</param>
protected internal virtual ValueTask OnCheckpointRestoredAsync(IWorkflowContext context, CancellationToken cancellationToken = default) => default;
/// <summary>
@@ -210,7 +214,7 @@ public abstract class Executor<TInput>(string id, ExecutorOptions? options = nul
routeBuilder.AddHandler<TInput>(this.HandleAsync);
/// <inheritdoc/>
public abstract ValueTask HandleAsync(TInput message, IWorkflowContext context);
public abstract ValueTask HandleAsync(TInput message, IWorkflowContext context, CancellationToken cancellationToken = default);
}
/// <summary>
@@ -229,5 +233,5 @@ public abstract class Executor<TInput, TOutput>(string id, ExecutorOptions? opti
routeBuilder.AddHandler<TInput, TOutput>(this.HandleAsync);
/// <inheritdoc/>
public abstract ValueTask<TOutput> HandleAsync(TInput message, IWorkflowContext context);
public abstract ValueTask<TOutput> HandleAsync(TInput message, IWorkflowContext context, CancellationToken cancellationToken = default);
}
@@ -29,7 +29,7 @@ public class FunctionExecutor<TInput>(string id,
}
/// <inheritdoc/>
public override ValueTask HandleAsync(TInput message, IWorkflowContext context) => handlerAsync(message, context, default);
public override ValueTask HandleAsync(TInput message, IWorkflowContext context, CancellationToken cancellationToken) => handlerAsync(message, context, cancellationToken);
/// <summary>
/// Creates a new instance of the <see cref="FunctionExecutor{TInput}"/> class.
@@ -65,7 +65,7 @@ public class FunctionExecutor<TInput, TOutput>(string id,
}
/// <inheritdoc/>
public override ValueTask<TOutput> HandleAsync(TInput message, IWorkflowContext context) => handlerAsync(message, context, default);
public override ValueTask<TOutput> HandleAsync(TInput message, IWorkflowContext context, CancellationToken cancellationToken) => handlerAsync(message, context, cancellationToken);
/// <summary>
/// Creates a new instance of the <see cref="FunctionExecutor{TInput,TOutput}"/> class.
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.Agents.AI.Workflows;
@@ -15,8 +16,10 @@ public interface IWorkflowContext
/// end of the current SuperStep.
/// </summary>
/// <param name="workflowEvent">The event to be raised.</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="ValueTask"/> representing the asynchronous operation.</returns>
ValueTask AddEventAsync(WorkflowEvent workflowEvent);
ValueTask AddEventAsync(WorkflowEvent workflowEvent, CancellationToken cancellationToken = default);
/// <summary>
/// Queues a message to be sent to connected executors. The message will be sent during the next SuperStep.
@@ -25,8 +28,22 @@ public interface IWorkflowContext
/// <param name="targetId">An optional identifier of the target executor. If null, the message is sent to all connected
/// executors. If the target executor is not connected from this executor via an edge, it will still not receive the
/// message.</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="ValueTask"/> representing the asynchronous operation.</returns>
ValueTask SendMessageAsync(object message, string? targetId = null);
ValueTask SendMessageAsync(object message, string? targetId = null, CancellationToken cancellationToken = default);
#if NET // What's the right way to do this so we do not make life a misery for netstandard2.0 targets?
// What's the value if they have to still write `cancellationToken: cancellationToken` to skip the targetId parameter?
// TODO: Remove this? (Maybe not: NET will eventually be the only target framework, right?)
/// <summary>
/// Queues a message to be sent to connected executors. The message will be sent during the next SuperStep.
/// </summary>
/// <param name="message">The message to be sent.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
/// <returns>A <see cref="ValueTask"/> representing the asynchronous operation.</returns>
ValueTask SendMessageAsync(object message, CancellationToken cancellationToken) => this.SendMessageAsync(message, null, cancellationToken);
#endif
/// <summary>
/// Adds an output value to the workflow's output queue. These outputs will be bubbled out of the workflow using the
@@ -37,8 +54,10 @@ public interface IWorkflowContext
/// types of registered message handlers are considered output types, unless otherwise specified using <see cref="ExecutorOptions"/>.
/// </remarks>
/// <param name="output">The output value to be returned.</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="ValueTask"/> representing the asynchronous operation.</returns>
ValueTask YieldOutputAsync(object output);
ValueTask YieldOutputAsync(object output, CancellationToken cancellationToken = default);
/// <summary>
/// Adds a request to "halt" workflow execution at the end of the current SuperStep.
@@ -54,15 +73,32 @@ public interface IWorkflowContext
/// <param name="key">The key of the state value.</param>
/// <param name = "scopeName" > An optional name that specifies the scope to read.If null, the default scope is
/// used.</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="ValueTask{T}"/> representing the asynchronous operation.</returns>
ValueTask<T?> ReadStateAsync<T>(string key, string? scopeName = null);
ValueTask<T?> ReadStateAsync<T>(string key, string? scopeName = null, CancellationToken cancellationToken = default);
#if NET // See above for musings about this construction
/// <summary>
/// Reads a state value from the workflow's state store. If no scope is provided, the executor's
/// default scope is used.
/// </summary>
/// <typeparam name="T">The type of the state value.</typeparam>
/// <param name="key">The key of the state value.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
/// <returns>A <see cref="ValueTask{T}"/> representing the asynchronous operation.</returns>
ValueTask<T?> ReadStateAsync<T>(string key, CancellationToken cancellationToken) => this.ReadStateAsync<T>(key, null, cancellationToken);
#endif
/// <summary>
/// Asynchronously reads all state keys within the specified scope.
/// </summary>
/// <param name="scopeName">An optional name that specifies the scope to read. If null, the default scope is
/// used.</param>
ValueTask<HashSet<string>> ReadStateKeysAsync(string? scopeName = null);
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
/// The default is <see cref="CancellationToken.None"/>.</param>
ValueTask<HashSet<string>> ReadStateKeysAsync(string? scopeName = null, CancellationToken cancellationToken = default);
/// <summary>
/// Asynchronously updates the state of a queue entry identified by the specified key and optional scope.
@@ -77,8 +113,27 @@ public interface IWorkflowContext
/// implementation.</param>
/// <param name="scopeName">An optional name that specifies the scope to update. If null, the default scope is
/// used.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
/// The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A ValueTask that represents the asynchronous update operation.</returns>
ValueTask QueueStateUpdateAsync<T>(string key, T? value, string? scopeName = null);
ValueTask QueueStateUpdateAsync<T>(string key, T? value, string? scopeName = null, CancellationToken cancellationToken = default);
#if NET // See above for musings about this construction
/// <summary>
/// Asynchronously updates the state of a queue entry identified by the specified key and optional scope.
/// </summary>
/// <remarks>
/// Subsequent reads by this executor will result in the new value of the state. Other executors will only see
/// the new state starting from the next SuperStep.
/// </remarks>
/// <typeparam name="T">The type of the value to associate with the queue entry.</typeparam>
/// <param name="key">The unique identifier for the queue entry to update. Cannot be null or empty.</param>
/// <param name="value">The value to set for the queue entry. If null, the entry's state may be cleared or reset depending on
/// implementation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
/// <returns>A ValueTask that represents the asynchronous update operation.</returns>
ValueTask QueueStateUpdateAsync<T>(string key, T? value, CancellationToken cancellationToken) => this.QueueStateUpdateAsync(key, value, null, cancellationToken);
#endif
/// <summary>
/// Asynchronously clears all state entries within the specified scope.
@@ -90,8 +145,25 @@ public interface IWorkflowContext
/// see the cleared state starting from the next SuperStep.
/// </remarks>
/// <param name="scopeName">An optional name that specifies the scope to clear. If null, the default scope is used.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
/// The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A ValueTask that represents the asynchronous clear operation.</returns>
ValueTask QueueClearScopeAsync(string? scopeName = null);
ValueTask QueueClearScopeAsync(string? scopeName = null, CancellationToken cancellationToken = default);
#if NET // See above for musings about this construction
/// <summary>
/// Asynchronously clears all state entries within the specified scope.
///
/// This semantically equivalent to retrieving all keys in the scope and deleting them one-by-one.
/// </summary>
/// <remarks>
/// Subsequent reads by this executor will not find any entries in the cleared scope. Other executors will only
/// see the cleared state starting from the next SuperStep.
/// </remarks>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
/// <returns>A ValueTask that represents the asynchronous clear operation.</returns>
ValueTask QueueClearScopeAsync(CancellationToken cancellationToken) => this.QueueClearScopeAsync(null, cancellationToken);
#endif
/// <summary>
/// The trace context associated with the current message about to be processed by the executor, if any.
@@ -46,14 +46,14 @@ internal sealed class InProcessRunner : ISuperStepRunner, ICheckpointingHandle
public string StartExecutorId { get; }
private readonly HashSet<Type> _knownValidInputTypes;
public async ValueTask<bool> IsValidInputTypeAsync(Type messageType, CancellationToken cancellation = default)
public async ValueTask<bool> IsValidInputTypeAsync(Type messageType, CancellationToken cancellationToken = default)
{
if (this._knownValidInputTypes.Contains(messageType))
{
return true;
}
Executor startingExecutor = await this.RunContext.EnsureExecutorAsync(this.Workflow.StartExecutorId, tracer: null).ConfigureAwait(false);
Executor startingExecutor = await this.RunContext.EnsureExecutorAsync(this.Workflow.StartExecutorId, tracer: null, cancellationToken).ConfigureAwait(false);
if (startingExecutor.CanHandle(messageType))
{
this._knownValidInputTypes.Add(messageType);
@@ -63,10 +63,10 @@ internal sealed class InProcessRunner : ISuperStepRunner, ICheckpointingHandle
return false;
}
public ValueTask<bool> IsValidInputTypeAsync<T>(CancellationToken cancellation = default)
=> this.IsValidInputTypeAsync(typeof(T), cancellation);
public ValueTask<bool> IsValidInputTypeAsync<T>(CancellationToken cancellationToken = default)
=> this.IsValidInputTypeAsync(typeof(T), cancellationToken);
public async ValueTask<bool> EnqueueMessageUntypedAsync(object message, Type declaredType, CancellationToken cancellation = default)
public async ValueTask<bool> EnqueueMessageUntypedAsync(object message, Type declaredType, CancellationToken cancellationToken = default)
{
this.RunContext.CheckEnded();
Throw.IfNull(message);
@@ -78,7 +78,7 @@ internal sealed class InProcessRunner : ISuperStepRunner, ICheckpointingHandle
// Check that the type of the incoming message is compatible with the starting executor's
// input type.
if (!await this.IsValidInputTypeAsync(declaredType, cancellation).ConfigureAwait(false))
if (!await this.IsValidInputTypeAsync(declaredType, cancellationToken).ConfigureAwait(false))
{
return false;
}
@@ -87,13 +87,13 @@ internal sealed class InProcessRunner : ISuperStepRunner, ICheckpointingHandle
return true;
}
public ValueTask<bool> EnqueueMessageAsync<T>(T message, CancellationToken cancellation = default)
=> this.EnqueueMessageUntypedAsync(Throw.IfNull(message), typeof(T), cancellation);
public ValueTask<bool> EnqueueMessageAsync<T>(T message, CancellationToken cancellationToken = default)
=> this.EnqueueMessageUntypedAsync(Throw.IfNull(message), typeof(T), cancellationToken);
public ValueTask<bool> EnqueueMessageAsync(object message, CancellationToken cancellation = default)
=> this.EnqueueMessageUntypedAsync(Throw.IfNull(message), message.GetType(), cancellation);
public ValueTask<bool> EnqueueMessageUntypedAsync(object message, CancellationToken cancellationToken = default)
=> this.EnqueueMessageUntypedAsync(Throw.IfNull(message), message.GetType(), cancellationToken);
ValueTask ISuperStepRunner.EnqueueResponseAsync(ExternalResponse response, CancellationToken cancellation)
ValueTask ISuperStepRunner.EnqueueResponseAsync(ExternalResponse response, CancellationToken cancellationToken)
{
// TODO: Check that there exists a corresponding input port?
return this.RunContext.AddExternalResponseAsync(response);
@@ -110,13 +110,13 @@ internal sealed class InProcessRunner : ISuperStepRunner, ICheckpointingHandle
private ValueTask RaiseWorkflowEventAsync(WorkflowEvent workflowEvent)
=> this.OutgoingEvents.EnqueueAsync(workflowEvent);
public ValueTask<AsyncRunHandle> BeginStreamAsync(ExecutionMode mode, CancellationToken cancellation = default)
public ValueTask<AsyncRunHandle> BeginStreamAsync(ExecutionMode mode, CancellationToken cancellationToken = default)
{
this.RunContext.CheckEnded();
return new(new AsyncRunHandle(this, this, mode));
}
public async ValueTask<AsyncRunHandle> ResumeStreamAsync(ExecutionMode mode, CheckpointInfo fromCheckpoint, CancellationToken cancellation = default)
public async ValueTask<AsyncRunHandle> ResumeStreamAsync(ExecutionMode mode, CheckpointInfo fromCheckpoint, CancellationToken cancellationToken = default)
{
this.RunContext.CheckEnded();
Throw.IfNull(fromCheckpoint);
@@ -125,7 +125,7 @@ internal sealed class InProcessRunner : ISuperStepRunner, ICheckpointingHandle
throw new InvalidOperationException("This runner was not configured with a CheckpointManager, so it cannot restore checkpoints.");
}
await this.RestoreCheckpointAsync(fromCheckpoint, cancellation).ConfigureAwait(false);
await this.RestoreCheckpointAsync(fromCheckpoint, cancellationToken).ConfigureAwait(false);
return new AsyncRunHandle(this, this, mode);
}
@@ -142,7 +142,7 @@ internal sealed class InProcessRunner : ISuperStepRunner, ICheckpointingHandle
return false;
}
StepContext currentStep = await this.RunContext.AdvanceAsync().ConfigureAwait(false);
StepContext currentStep = await this.RunContext.AdvanceAsync(cancellationToken).ConfigureAwait(false);
if (currentStep.HasMessages ||
this.RunContext.HasQueuedExternalDeliveries ||
@@ -150,7 +150,7 @@ internal sealed class InProcessRunner : ISuperStepRunner, ICheckpointingHandle
{
try
{
await this.RunSuperstepAsync(currentStep).ConfigureAwait(false);
await this.RunSuperstepAsync(currentStep, cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{ }
@@ -165,9 +165,9 @@ internal sealed class InProcessRunner : ISuperStepRunner, ICheckpointingHandle
return false;
}
private async ValueTask DeliverMessagesAsync(string receiverId, ConcurrentQueue<MessageEnvelope> envelopes)
private async ValueTask DeliverMessagesAsync(string receiverId, ConcurrentQueue<MessageEnvelope> envelopes, CancellationToken cancellationToken)
{
Executor executor = await this.RunContext.EnsureExecutorAsync(receiverId, this.StepTracer).ConfigureAwait(false);
Executor executor = await this.RunContext.EnsureExecutorAsync(receiverId, this.StepTracer, cancellationToken).ConfigureAwait(false);
this.StepTracer.TraceActivated(receiverId);
while (envelopes.TryDequeue(out var envelope))
@@ -175,19 +175,20 @@ internal sealed class InProcessRunner : ISuperStepRunner, ICheckpointingHandle
await executor.ExecuteAsync(
envelope.Message,
envelope.MessageType,
this.RunContext.Bind(receiverId, envelope.TraceContext)
this.RunContext.Bind(receiverId, envelope.TraceContext),
cancellationToken
).ConfigureAwait(false);
}
}
private async ValueTask RunSuperstepAsync(StepContext currentStep)
private async ValueTask RunSuperstepAsync(StepContext currentStep, CancellationToken cancellationToken)
{
await this.RaiseWorkflowEventAsync(this.StepTracer.Advance(currentStep)).ConfigureAwait(false);
// Deliver the messages and queue the next step
List<Task> receiverTasks =
currentStep.QueuedMessages.Keys
.Select(receiverId => this.DeliverMessagesAsync(receiverId, currentStep.MessagesFor(receiverId)).AsTask())
.Select(receiverId => this.DeliverMessagesAsync(receiverId, currentStep.MessagesFor(receiverId), cancellationToken).AsTask())
.ToList();
// TODO: Should we let the user specify that they want strictly turn-based execution of the edges, vs. concurrent?
@@ -202,12 +203,12 @@ internal sealed class InProcessRunner : ISuperStepRunner, ICheckpointingHandle
List<Task> subworkflowTasks = new();
foreach (ISuperStepRunner subworkflowRunner in this.RunContext.JoinedSubworkflowRunners)
{
subworkflowTasks.Add(subworkflowRunner.RunSuperStepAsync(CancellationToken.None).AsTask());
subworkflowTasks.Add(subworkflowRunner.RunSuperStepAsync(cancellationToken).AsTask());
}
await Task.WhenAll(subworkflowTasks).ConfigureAwait(false);
await this.CheckpointAsync().ConfigureAwait(false);
await this.CheckpointAsync(cancellationToken).ConfigureAwait(false);
await this.RaiseWorkflowEventAsync(this.StepTracer.Complete(this.RunContext.NextStepHasActions, this.RunContext.HasUnservicedRequests))
.ConfigureAwait(false);
@@ -57,7 +57,7 @@ internal sealed class InProcessRunnerContext : IRunnerContext
this.OutgoingEvents = outgoingEvents;
}
public async ValueTask<Executor> EnsureExecutorAsync(string executorId, IStepTracer? tracer)
public async ValueTask<Executor> EnsureExecutorAsync(string executorId, IStepTracer? tracer, CancellationToken cancellationToken = default)
{
this.CheckEnded();
Task<Executor> executorTask = this._executors.GetOrAdd(executorId, CreateExecutorAsync);
@@ -88,9 +88,9 @@ internal sealed class InProcessRunnerContext : IRunnerContext
return await executorTask.ConfigureAwait(false);
}
public async ValueTask<IEnumerable<Type>> GetStartingExecutorInputTypesAsync(CancellationToken cancellation = default)
public async ValueTask<IEnumerable<Type>> GetStartingExecutorInputTypesAsync(CancellationToken cancellationToken = default)
{
Executor startingExecutor = await this.EnsureExecutorAsync(this._workflow.StartExecutorId, tracer: null)
Executor startingExecutor = await this.EnsureExecutorAsync(this._workflow.StartExecutorId, tracer: null, cancellationToken)
.ConfigureAwait(false);
return startingExecutor.InputTypes;
@@ -145,7 +145,7 @@ internal sealed class InProcessRunnerContext : IRunnerContext
public bool HasUnservicedRequests => !this._externalRequests.IsEmpty ||
this._joinedSubworkflowRunners.Any(joinedRunner => joinedRunner.HasUnservicedRequests);
public async ValueTask<StepContext> AdvanceAsync()
public async ValueTask<StepContext> AdvanceAsync(CancellationToken cancellationToken = default)
{
this.CheckEnded();
@@ -159,7 +159,7 @@ internal sealed class InProcessRunnerContext : IRunnerContext
return Interlocked.Exchange(ref this._nextStep, new StepContext());
}
public ValueTask AddEventAsync(WorkflowEvent workflowEvent)
public ValueTask AddEventAsync(WorkflowEvent workflowEvent, CancellationToken cancellationToken = default)
{
this.CheckEnded();
return this.OutgoingEvents.EnqueueAsync(workflowEvent);
@@ -168,7 +168,7 @@ internal sealed class InProcessRunnerContext : IRunnerContext
private static readonly string s_namespace = typeof(IWorkflowContext).Namespace!;
private static readonly ActivitySource s_activitySource = new(s_namespace);
public async ValueTask SendMessageAsync(string sourceId, object message, string? targetId = null)
public async ValueTask SendMessageAsync(string sourceId, object message, string? targetId = null, CancellationToken cancellationToken = default)
{
using Activity? activity = s_activitySource.StartActivity(ActivityNames.MessageSend, ActivityKind.Producer);
// Create a carrier for trace context propagation
@@ -231,19 +231,19 @@ internal sealed class InProcessRunnerContext : IRunnerContext
OutputFilter outputFilter,
Dictionary<string, string>? traceContext) : IWorkflowContext
{
public ValueTask AddEventAsync(WorkflowEvent workflowEvent) => RunnerContext.AddEventAsync(workflowEvent);
public ValueTask AddEventAsync(WorkflowEvent workflowEvent, CancellationToken cancellationToken = default) => RunnerContext.AddEventAsync(workflowEvent, cancellationToken);
public ValueTask SendMessageAsync(object message, string? targetId = null)
public ValueTask SendMessageAsync(object message, string? targetId = null, CancellationToken cancellationToken = default)
{
return RunnerContext.SendMessageAsync(ExecutorId, message, targetId);
return RunnerContext.SendMessageAsync(ExecutorId, message, targetId, cancellationToken);
}
public async ValueTask YieldOutputAsync(object output)
public async ValueTask YieldOutputAsync(object output, CancellationToken cancellationToken = default)
{
RunnerContext.CheckEnded();
Throw.IfNull(output);
Executor sourceExecutor = await RunnerContext.EnsureExecutorAsync(ExecutorId, tracer: null).ConfigureAwait(false);
Executor sourceExecutor = await RunnerContext.EnsureExecutorAsync(ExecutorId, tracer: null, cancellationToken).ConfigureAwait(false);
if (!sourceExecutor.CanOutput(output.GetType()))
{
throw new InvalidOperationException($"Cannot output object of type {output.GetType().Name}. Expecting one of [{string.Join(", ", sourceExecutor.OutputTypes)}].");
@@ -251,22 +251,22 @@ internal sealed class InProcessRunnerContext : IRunnerContext
if (outputFilter.CanOutput(ExecutorId, output))
{
await this.AddEventAsync(new WorkflowOutputEvent(output, ExecutorId)).ConfigureAwait(false);
await this.AddEventAsync(new WorkflowOutputEvent(output, ExecutorId), cancellationToken).ConfigureAwait(false);
}
}
public ValueTask RequestHaltAsync() => this.AddEventAsync(new RequestHaltEvent());
public ValueTask<T?> ReadStateAsync<T>(string key, string? scopeName = null)
public ValueTask<T?> ReadStateAsync<T>(string key, string? scopeName = null, CancellationToken cancellationToken = default)
=> RunnerContext.StateManager.ReadStateAsync<T>(ExecutorId, scopeName, key);
public ValueTask<HashSet<string>> ReadStateKeysAsync(string? scopeName = null)
public ValueTask<HashSet<string>> ReadStateKeysAsync(string? scopeName = null, CancellationToken cancellationToken = default)
=> RunnerContext.StateManager.ReadKeysAsync(ExecutorId, scopeName);
public ValueTask QueueStateUpdateAsync<T>(string key, T? value, string? scopeName = null)
public ValueTask QueueStateUpdateAsync<T>(string key, T? value, string? scopeName = null, CancellationToken cancellationToken = default)
=> RunnerContext.StateManager.WriteStateAsync(ExecutorId, scopeName, key, value);
public ValueTask QueueClearScopeAsync(string? scopeName = null)
public ValueTask QueueClearScopeAsync(string? scopeName = null, CancellationToken cancellationToken = default)
=> RunnerContext.StateManager.ClearStateAsync(ExecutorId, scopeName);
public IReadOnlyDictionary<string, string>? TraceContext => traceContext;
@@ -274,7 +274,7 @@ internal sealed class InProcessRunnerContext : IRunnerContext
public bool WithCheckpointing { get; }
internal Task PrepareForCheckpointAsync(CancellationToken cancellation = default)
internal Task PrepareForCheckpointAsync(CancellationToken cancellationToken = default)
{
this.CheckEnded();
@@ -283,7 +283,7 @@ internal sealed class InProcessRunnerContext : IRunnerContext
async Task InvokeCheckpointingAsync(Task<Executor> executorTask)
{
Executor executor = await executorTask.ConfigureAwait(false);
await executor.OnCheckpointingAsync(this.Bind(executor.Id), cancellation).ConfigureAwait(false);
await executor.OnCheckpointingAsync(this.Bind(executor.Id), cancellationToken).ConfigureAwait(false);
}
}
@@ -320,7 +320,7 @@ internal sealed class InProcessRunnerContext : IRunnerContext
{
foreach (string requestId in this._externalRequests.Keys)
{
await this.AddEventAsync(new RequestInfoEvent(this._externalRequests[requestId]))
await this.AddEventAsync(new RequestInfoEvent(this._externalRequests[requestId]), cancellationToken)
.ConfigureAwait(false);
}
}
@@ -386,7 +386,7 @@ internal sealed class InProcessRunnerContext : IRunnerContext
public IEnumerable<ISuperStepRunner> JoinedSubworkflowRunners => this._joinedSubworkflowRunners;
public ValueTask AttachSuperstepAsync(ISuperStepRunner superStepRunner, CancellationToken cancellation = default)
public ValueTask AttachSuperstepAsync(ISuperStepRunner superStepRunner, CancellationToken cancellationToken = default)
{
// This needs to be a thread-safe ordered collection because we can potentially instantiate executors
// in parallel, which means multiple sub-workflows could be attaching at the same time.
@@ -394,9 +394,9 @@ internal sealed class InProcessRunnerContext : IRunnerContext
return default;
}
ValueTask ISuperStepJoinContext.ForwardWorkflowEventAsync(WorkflowEvent workflowEvent, CancellationToken cancellation)
=> this.AddEventAsync(workflowEvent);
ValueTask ISuperStepJoinContext.ForwardWorkflowEventAsync(WorkflowEvent workflowEvent, CancellationToken cancellationToken)
=> this.AddEventAsync(workflowEvent, cancellationToken);
ValueTask ISuperStepJoinContext.SendMessageAsync<TMessage>(string senderId, [DisallowNull] TMessage message, CancellationToken cancellation)
=> this.SendMessageAsync(senderId, Throw.IfNull(message));
ValueTask ISuperStepJoinContext.SendMessageAsync<TMessage>(string senderId, [DisallowNull] TMessage message, CancellationToken cancellationToken)
=> this.SendMessageAsync(senderId, Throw.IfNull(message), cancellationToken: cancellationToken);
}

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