Microsoft Agent Framework (C#) を見てみよう その14 Durable Agent を試してみよう
シリーズ記事
- 「雑感」とハローワールド
- ざっとリポジトリを見てみる
- ワークフローを見てみよう
- ワークフローの Executor を掘り下げる
- ワークフローで条件分岐とループを扱う
- Executor のステータス管理
- チェックポイントの永続化
- Human in the loop を試してみよう
- Semantic Kernel の Plugin の移行
- Durable Functions でワークフロー
- エージェントを見てみよう
- A2A対応のエージェントを作ってみよう
- .NET 10 用の Agent プロジェクトテンプレート
- Durable Agent を試してみよう
はじめに
先日 Microsoft Agent Framework に対して Durable Functions と連携する機能が実装されました。少し前から Microsoft Agent Framework のリポジトリのプルリクエストとして追加されていたので、いつ公開されるんだろうと楽しみにしていましたがついにアナウンスされました…!
以下のブログがアナウンス記事です。
Bulletproof agents with the durable task extension for Microsoft Agent Framework
Microsoft Docs にもドキュメントページが出来ていました。機械翻訳ですが一応日本語でも読めます。
個人的に気に入っている Durable Functions の上で Agent のワークフローが簡単に作れるというものなので早速試してみようと思います。
Durable task extension for Microsoft Agent Framework
Durable task extension for Microsoft Agent Framework が今回試す機能の正式名称っぽいです。
ざっと見る限り、この機能は Agent Framework が提供している AIAgent クラスを簡単に Azure Functions で呼び出せるようにする機能が一番簡単に使える機能としてあるように見えます。
他にも Durable Functions で Agent を複数組み合わせて呼んだり、人間の承認を待ったりといったことも出来ますが、ひとまず一番簡単に使えそうな機能から試してみようと思います。使い方は簡単です。まずは、Azure Functions のプロジェクトを Visual Studio で新規作成をします。後で色々試したいので Aspire 13 との連携オプションをオンにして作成しました。この記事の本題ではないですが .NET Aspire も .NET を名前から外して、より多くの言語で使用できるツールとして進化したのも興味深いですね。
プロジェクトの作成が終わったら以下のパッケージを追加します。
- Microsoft.Azure.Functions.Worker (v2.51.0 にしました)
- Microsoft.Agents.AI.Hosting.AzureFunctions (v1.0.0-preview.251114.1)
実際に Azure AI Foundry の OpenAI モデルを呼び出す Agent を作りたい場合は他のパッケージも追加しないといけませんが、まずはシンプルに LLM につながない形の自作 Echo Agent を作って、それを 使ってみようと思います。
Durable Task Scheduler の設定
これは必須のステップではないのですが、Durable Task Scheduler と連携をするといい感じに実行結果が見れるみたいなのでやっておこうと思います。
Aspire を使っている場合は以下の記事の手順で簡単に追加できるのでやっておきます。
Durable Task Scheduler を .NET Aspire で起動する
Echo Agent を作ってみる
ということで、まずは Echo Agent を作ってみましょう。Echo Agent は入力されたメッセージをそのまま返すだけのシンプルなエージェントです。以下のように AIAgent クラスを継承して実装します。
実装方法は Microsoft Agent Framework のサンプルにある Agent with Custom Implementation を参考にしました。
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using System.Runtime.CompilerServices;
using System.Text.Json;
namespace FunctionApp15;
internal class EchoAgent : AIAgent
{
public override AgentThread DeserializeThread(JsonElement serializedThread,
JsonSerializerOptions? jsonSerializerOptions = null) =>
new EchoAgentThread(serializedThread, jsonSerializerOptions);
public override AgentThread GetNewThread() => new EchoAgentThread();
public override Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
var lastMessage = messages.LastOrDefault();
if (lastMessage == null)
{
throw new InvalidOperationException("No messages provided to the agent.");
}
var response = new AgentRunResponse
{
Messages =
[
new(ChatRole.Assistant, $"{DateTimeOffset.UtcNow.ToString()}: {lastMessage.Text}")
]
};
return Task.FromResult(response);
}
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
var response = await RunAsync(messages, thread, options, cancellationToken);
foreach (var update in response.ToAgentRunResponseUpdates())
{
yield return update;
}
}
class EchoAgentThread : InMemoryAgentThread
{
public EchoAgentThread() { }
public EchoAgentThread(JsonElement serializedThread,
JsonSerializerOptions? jsonSerializer) :base (serializedThread, jsonSerializer) { }
}
}
Durable Agent の構成を行う
Agent を登録するには Program.cs で ConfigureDurableAgents メソッドを使います。以下のように AddAIAgent メソッドで先ほど作成した EchoAgent を登録します。
using FunctionApp15;
using Microsoft.Agents.AI.DurableTask;
using Microsoft.Agents.AI.Hosting.AzureFunctions;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
var builder = FunctionsApplication.CreateBuilder(args);
builder.AddServiceDefaults();
builder.ConfigureFunctionsWebApplication();
// Druable Agents の設定
builder.ConfigureDurableAgents(options =>
{
// "echo" という名前で EchoAgent を登録
options.AddAIAgentFactory("echo", sp => new EchoAgent());
});
builder.Services
.AddApplicationInsightsTelemetryWorkerService()
.ConfigureFunctionsApplicationInsights();
builder.Build().Run();
これで Echo Agent が Azure Functions の Durable Agent として登録されました。実際に呼び出してみましょう。実行して関数アプリの標準出力を見ると以下のように Echo Agent が呼び出されているのが分かります。
Functions:
Function1: [GET,POST] http://localhost:7186/api/Function1
http-echo: [POST] http://localhost:7186/api/agents/echo/run
dafx-echo: entityTrigger
dafx-echo という名前の entityTrigger がありますね。これは Durable Agent が エンティティ関数 を使って実装されているためです。エンティティ関数は Durable Functions の一種で、状態を持つオブジェクトを定義できるようなものです。恐らく Agent の状態管理に使われているのだと思います。
そして Echo Agent を呼び出すための HTTP エンドポイントも用意されています。http-echo という名前のエンドポイントがそれです。
では、実際に Echo Agent を呼び出してみましょう。以下のように HTTP POST リクエストを送ることで呼び出せます。使い方は非常にシンプルで、リクエストボディにテキストを入れて送るだけです。
POST http://localhost:7186/api/agents/echo/run
Content-Type: text/plain
やっほーーー!!
こうすると以下のようなレスポンスが返ってきます。
2025/11/19 11:05:21 +00:00: やっほーーー!!
これで Durable Agent の Echo Agent が動作しているのが確認できました。非常にシンプルにエージェントを Azure Functions の Durable Agent として動かせるのが分かりますね。
さらに、この状態で Durable Task Scheduler の画面を開いて Entities タブを開くと dafx-echo エンティティが確認できます。

各エンティティの行の一番右端にあるアイコンを選択すると以下のようにエンティティの状態が確認できます。ここにはエージェントのスレッドが保存されていることが確認できます。

公式ドキュメントでは、以下のようにエージェントのスレッドを確認する画面があるみたいなのですが、ローカルのエミュレーター上には表示されませんでした。将来的にはローカルのエミュレーター上でも見られるようになるのかもしれません。

Durable Agent の機能 から引用
今の状態だと単純に text/plane をやり取りしているだけですが JSON も使用できます。リクエストを JSON 形式にするには Content-Type ヘッダーを application/json にして、レスポンスを JOSN にするには Accept ヘッダーを application/json にします。以下のようにリクエストを送ると JSON 形式でリクエストを送って、JSON 形式でレスポンスが返ってきます。
POST http://localhost:7186/api/agents/echo/run
Content-Type: application/json
Accept: application/json
{
"message": "Hello"
}
レスポンスは以下のようになりました。
{
"status": 200,
"thread_id": "@dafx-echo@458c6b06-085b-44f2-87a4-150cd5aa3f7b",
"response": {
"Messages": [
{
"AuthorName": null,
"CreatedAt": null,
"Role": "assistant",
"Contents": [
{
"$type": "text",
"Text": "2025/11/19 11:26:56 +00:00: Hello",
"Annotations": null,
"AdditionalProperties": null
}
],
"MessageId": null,
"AdditionalProperties": null
}
],
"AgentId": null,
"ResponseId": null,
"ContinuationToken": null,
"CreatedAt": "2025-11-19T11:26:56.8335466+00:00",
"Usage": null,
"AdditionalProperties": null
}
}
ここで返ってくる thread_id を使うと会話スレッドを継続できます。今は エンティティの ID がそのまま帰ってきていますが、この間 GitHub Issues で GUID になるとかなんとかっていう話を見たので将来的には値の書式が変わっているかもしれませんが基本的にはプログラム的には thread_id を保存しておいて、次回以降のリクエストで使う形でいいはずです。
スレッド ID は以下のようにリクエストの thread_id クエリパラメーターで指定することで使えます。
以下のようにリクエストを送ると、前回のスレッドを継続してメッセージを送れます。
POST http://localhost:7186/api/agents/echo/run?thread_id=@dafx-echo@458c6b06-085b-44f2-87a4-150cd5aa3f7b
Content-Type: application/json
Accept: application/json
{
"message": "Hello"
}
もしくは、JSON 形式で送る場合は thread_id プロパティで以下のように指定することも出来ます。
POST http://localhost:7186/api/agents/echo/run
Content-Type: application/json
Accept: application/json
{
"thread_id": "@dafx-echo@458c6b06-085b-44f2-87a4-150cd5aa3f7b",
"message": "Hello"
}
レスポンスは先ほどと同じ感じです。Durable Task Scheduler の画面で該当の ID を持つエンティティの状態を確認すると以下のような JSON になっています。会話履歴が保存されているのが分かりますね。
{
"schemaVersion": "1.0.0",
"data": {
"conversationHistory": [
{
"$type": "request",
"responseType": "text",
"correlationId": "3c1e29a49a0041e09a0dcd6a3aef1cfb",
"createdAt": "2025-11-19T11:26:56.8324195+00:00",
"messages": [
{
"contents": [
{
"$type": "text",
"text": "Hello"
}
],
"role": "user"
}
]
},
{
"$type": "response",
"correlationId": "3c1e29a49a0041e09a0dcd6a3aef1cfb",
"createdAt": "2025-11-19T11:26:56.8335466+00:00",
"messages": [
{
"contents": [
{
"$type": "text",
"text": "2025/11/19 11:26:56 +00:00: Hello"
}
],
"role": "assistant"
}
]
},
{
"$type": "request",
"responseType": "text",
"correlationId": "974f7de86ea1435ab161f33ad3f0ffe6",
"createdAt": "2025-11-19T11:34:42.4299999+00:00",
"messages": [
{
"contents": [
{
"$type": "text",
"text": "Hello"
}
],
"role": "user"
}
]
},
{
"$type": "response",
"correlationId": "974f7de86ea1435ab161f33ad3f0ffe6",
"createdAt": "2025-11-19T11:34:42.4310924+00:00",
"messages": [
{
"contents": [
{
"$type": "text",
"text": "2025/11/19 11:34:42 +00:00: Hello"
}
],
"role": "assistant"
}
]
}
]
}
}
Content-Type ヘッダーと Accept ヘッダーの組み合わせでリクエストとレスポンスを text/plain と application/json を組み合わせてリクエスト・レスポンスのフォーマットが選べるので状況に応じて使い分ける感じです。Accept が text/plain で会話を継続するための thread_id を取得したい場合は x-ms-thread-id レスポンスヘッダーにスレッド ID が返ってくるのでそれを使う形になります。
一応どちらを使ってもシンプルなテキストによる会話の継続は出来るようになっています。JSON の方がもしかしたら画像みたいなテキスト以外のコンテンツを含むような場合に便利かもしれませんが、画像とかをサポートされているのかはまだ試せていません。
ここまでの確認で非常に簡単に Microsoft Agent Framework の Agent を Durable Agent として動かせることがわかりました。簡単にゼロスケールする Azure Functions の上で動かせるのは非常にいい感じだと思います。
これだと単純にシングルエージェントを呼び出すだけだけなので、次は Durable Functions のオーケストレーター関数でエージェントを使ってみようと思います。
オーケストレーター関数でエージェントを呼ぶ
オーケストレーターでエージェントを使うには TaskOrchestrationContext の GetAgent メソッドを使います。以下のようにオーケストレーター関数を作成してみました。
using Microsoft.Agents.AI.DurableTask;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.DurableTask;
using Microsoft.DurableTask.Client;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Logging;
using System.Net;
namespace FunctionApp15;
public class Function1
{
// Orchestrator 関数 (ワークフロー本体)
[Function(nameof(RunWorkflowAsync))]
public async Task<IEnumerable<string>> RunWorkflowAsync([OrchestrationTrigger] TaskOrchestrationContext context)
{
// 入力を受け取る
var input = context.GetInput<string>();
// context から Agent を取得できる
var agent = context.GetAgent("echo");
// 普通の Agent Framework のように使える
// 新しいスレッドを作って
var thread = agent.GetNewThread();
// エージェントを呼んで結果を返す
List<string> responses =
[
(await agent.RunAsync(new ChatMessage(ChatRole.User, $"Seattle({input})"), thread)).Text,
(await agent.RunAsync(new ChatMessage(ChatRole.User, $"Tokyo({input})"), thread)).Text,
(await agent.RunAsync(new ChatMessage(ChatRole.User, $"London({input})"), thread)).Text,
];
return responses;
}
// Orchestrator のスターター関数
[Function("Function1_Start")]
public async Task<HttpResponseData> StartAsync([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequestData req,
[DurableClient]DurableTaskClient client)
{
// 入力を受け取ってオーケストレーターを起動
var input = await req.ReadAsStringAsync();
if (string.IsNullOrEmpty(input))
{
return req.CreateResponse(HttpStatusCode.BadRequest);
}
var instanceId = await client.ScheduleNewOrchestrationInstanceAsync(
nameof(RunWorkflowAsync),
input);
// 状態確認用エンドポイントなどを返す
return await client.CreateCheckStatusResponseAsync(req, instanceId);
}
}
個人的に気に入っているのは、従来のオーケストレーター関数で AI を呼ぶような処理をしようとしたら AI を呼び出す部分を Activity 関数として切り出して定義をしてといったことが必要でした。しかし、この Durable extensions for Microsoft Agent Framework を使うとオーケストレーター関数の中で直接エージェントを呼び出せるので非常にシンプルに書けるのがいいですね。いい感じ。
では、実際にオーケストレーター関数を呼び出してみましょう。以下のように HTTP リクエストを送ります。
POST http://localhost:7186/api/Function1_Start
Content-Type: text/plain
やっほーーー!!
そうすると Durable Functions を使っている人にはおなじみの以下のようなレスポンスが返ってきます。
{
"Id": "90d8e2156f494f4e950d5d419547ff4b",
"StatusQueryGetUri": "http://localhost:7186/runtime/webhooks/durabletask/instances/90d8e2156f494f4e950d5d419547ff4b?code=XapbywQlTl1Gx2fEgGuYBcEmS9XYoONZXkcuSWtS-i4EAzFuhPTV0A==",
"SendEventPostUri": "http://localhost:7186/runtime/webhooks/durabletask/instances/90d8e2156f494f4e950d5d419547ff4b/raiseEvent/{eventName}?code=XapbywQlTl1Gx2fEgGuYBcEmS9XYoONZXkcuSWtS-i4EAzFuhPTV0A==",
"TerminatePostUri": "http://localhost:7186/runtime/webhooks/durabletask/instances/90d8e2156f494f4e950d5d419547ff4b/terminate?reason={{text}}&code=XapbywQlTl1Gx2fEgGuYBcEmS9XYoONZXkcuSWtS-i4EAzFuhPTV0A==",
"RewindPostUri": null,
"PurgeHistoryDeleteUri": "http://localhost:7186/runtime/webhooks/durabletask/instances/90d8e2156f494f4e950d5d419547ff4b?code=XapbywQlTl1Gx2fEgGuYBcEmS9XYoONZXkcuSWtS-i4EAzFuhPTV0A==",
"RestartPostUri": "http://localhost:7186/runtime/webhooks/durabletask/instances/90d8e2156f494f4e950d5d419547ff4b/restart?code=XapbywQlTl1Gx2fEgGuYBcEmS9XYoONZXkcuSWtS-i4EAzFuhPTV0A==",
"SuspendPostUri": "http://localhost:7186/runtime/webhooks/durabletask/instances/90d8e2156f494f4e950d5d419547ff4b/suspend?reason={{text}}&code=XapbywQlTl1Gx2fEgGuYBcEmS9XYoONZXkcuSWtS-i4EAzFuhPTV0A==",
"ResumePostUri": "http://localhost:7186/runtime/webhooks/durabletask/instances/90d8e2156f494f4e950d5d419547ff4b/resume?reason={{text}}&code=XapbywQlTl1Gx2fEgGuYBcEmS9XYoONZXkcuSWtS-i4EAzFuhPTV0A=="
}
この中から StatusQueryGetUri を使ってオーケストレーター関数の実行状況を確認できます。以下のように GET リクエストを送ります。
GET http://localhost:7186/runtime/webhooks/durabletask/instances/90d8e2156f494f4e950d5d419547ff4b?code=XapbywQlTl1Gx2fEgGuYBcEmS9XYoONZXkcuSWtS-i4EAzFuhPTV0A==
そうするとオーケストレーターの実行結果が返ってきます。
{
"name": "RunWorkflowAsync",
"instanceId": "90d8e2156f494f4e950d5d419547ff4b",
"runtimeStatus": "Completed",
"input": "やっほーーー!!",
"customStatus": null,
"output": [
"2025/11/19 12:05:35 +00:00: Seattle(やっほーーー!!)",
"2025/11/19 12:05:35 +00:00: Tokyo(やっほーーー!!)",
"2025/11/19 12:05:35 +00:00: London(やっほーーー!!)"
],
"createdTime": "2025-11-19T12:05:35Z",
"lastUpdatedTime": "2025-11-19T12:05:35Z"
}
ちゃんと実行結果が取れています。いい感じ。
今回は 1 つのエージェントを呼び出しましたが、オーケストレーター関数の中で複数のエージェントを組み合わせて呼び出すことも出来ます。その他にツール呼び出しや人間の承認待ちなども組み合わせられるので、より複雑なワークフローも簡単に作れます。
個人的には、この Durable Extensions for Microsoft Agent Framework を使うと、Durable Functions のスケーラビリティや耐久性を活かしつつ、Microsoft Agent Framework の強力なエージェント機能を利用できるので、非常に魅力的だと感じました。特に、オーケストレーター関数内で直接エージェントを呼び出せる点は、従来の方法よりもシンプルで凄く良いと思います。
まとめ
今回は Microsoft Agent Framework の Durable Extensions を使って、Durable Agent を試してみました。非常にシンプルに Azure Functions 上でエージェントを動かせるのが分かりました。
Durable Functions のオーケストレーター関数内でエージェントを直接呼び出せるのも非常に便利で、これまでよりも簡単に複雑なワークフローを構築できると感じました。
これは良いものなので今後も、もう少し機能を試してみようと思います。
Discussion