📚

PlanモードとTodo管理に対応したAgentを作ろう - Microsoft Agent Framework (C#)

に公開

はじめに

Microsoft Agent Framework v1.14.0 で個人的に注目していた HarnessAgent から Experimental のフラグが外れて正式提供されました。プレビュー機能として実装された直後くらいにこちらの記事でも取り上げましたが、そこから API の破壊的変更などがあるので、リリースされたバージョンで試してみようと思います。

HarnessAgent

この Agent Framework にある HarnessAgent は、ファイルシステムへのアクセス、Todo管理、プランモード、チャット履歴の圧縮、シェルの実行、スキルへの対応、OpenTelemetry 対応などの高性能な Agent を作りたいときに必要になる機能をあらかじめ持った Agent になります。
この他にもいくつか機能があるのですがそれらの機能は、まだ Experimental になります。

現在 Experimental な機能

  • Context Window の管理(最大コンテキストウィンドウ、出力トークン、履歴のコンパクト化)
  • AI Agent の LLM の呼び出し、ツール実行などの合間に評価を行い改善する必要があるなら、もう一度 feedback を添えて AI を呼び出すといったことが出来る LoopEvaluators
  • ファイルメモリストアのカスタマイズ
  • ファイルアクセスストアのカスタマイズ
  • バックグラウンド エージェント(多分これはサブエージェント呼び出しかな?)

基本的な機能は GA していますが、細かいカスタマイズなどを行う箇所については、まだ Experimental 機能となっているため、そういった機能が使いたい場合は破壊的変更や不具合を覚悟して使う必要があると思います。

使ってみよう

ということで HarnessAgent を使ってみましょう。
HarnessAgent を使うためには以下のパッケージが必要です。

  • Microsoft.Agents.AI
  • Microsoft.Agents.AI.Harness

この記事を書いた時点では v1.15.0 が最新版だったので、それを使っています。そして、今回は LLM は Microsoft Foundry の gpt-5.4 を使います。OpenAI のパッケージを使ってもいいのですが、Agent Framework のプロジェクトでも Azure.AI.Projects パッケージを使っているのでそれを追加したのと、OpenAI のクライアントを IChatClient にするため MEAI の OpenAI パッケージも追加します。

  • Azure.AI.Projects
  • Microsoft.Extensions.AI.OpenAI

Azure.AI.Projects は 2.0.1 が最新だったので、それを使っています。MEAI.OpenAI は 10.8.1 を使用しました。
これらのパッケージを使ってモデルを呼び出すには以下の3つがあれば呼び出せます。

  1. Microsoft Foundry のプロジェクトエンドポイント
  2. Microsoft Foundry にデプロイしたモデル名
  3. Foundry User ロールを持ったアカウント (開発時は Azure CLI にログインしているアカウント、本番は Managed ID)

結構お手軽ですね。
次はコードを書いて HarnessAgent を作成してみましょう。HarnessAgent の作成自体は簡単で IChatClient に対して AsHarnessAgent を呼び出すだけです。AsHarnessAgent の引数には HarnessAgentOptions クラスを渡します。ここで名前やインストラクションや各種機能の設定を行いまうs。

単純なエージェントを作って実行するだけであれば以下のようなコードになります。

using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Extensions.AI;

// 接続先情報
const string ProjectEndpoint = "https://XXXXXX.services.ai.azure.com/api/projects/YYYYYY";
const string ModelDeploymentName = "gpt-5.6-luna";

// Microsoft Foundry のプロジェクトへつなぐためのクライアント
var projectClient = new AIProjectClient(
    new Uri(ProjectEndpoint),
    new AzureCliCredential());

// ChatCompletion 用のクライアントを取得して IChatClient に変換する
var chatClient = projectClient.GetProjectOpenAIClient()
    .GetChatClient(ModelDeploymentName)
    .AsIChatClient();

// HarnessAgent を作成する
var harnessAgent = chatClient.AsHarnessAgent(new()
    {
        Name = "CatAgent",
        HarnessInstructions = """
            あなたな猫型エージェントです。
            猫らしく振舞うために語尾はかならず「にゃん」にしてください。
            """,
        DisableWebSearch = true,
    });

// HarnessAgent を使って会話を行う
var response = await harnessAgent.RunAsync("こんにちは");
Console.WriteLine(response.Text);

AsHarnessAgent の引数で DisableWebSearchtrue をしているのは、ChatCompletions API では Web Search のツールがサポートされていないためです。これを使おうと思ったら Responses API を使う必要があります。これを実行すると以下のような結果になります。ちゃんと動いていますね。

こんにちはにゃん!今日は何をお手伝いするにゃん?

この HarnessAgent にはいくつかの便利機能が規定で有効化されています。その中でまず使いたいと思う機能が Plan モードと Todo 管理です。これは AIContextProvider として実装されていて、これを使うと任意のツールを Agent に提供したり、Instructionsを追加したりといった処理を追加することが出来ます。AIContextProvider は再利用可能な AI Agent のコンテキストを提供するための部品です。

HarnessAgent には プランモードを提供する AgentModeProviderTodoProvider という 2 つの AIContextProvider を使って実現されています。厳密には AgentModeProvider だけでいいのですが、プランを立てる際に大体セットで TodoProvider が使用されます。

では、天気を聞くシナリオでプランモードを使ってみましょう。天気を調べるためのツールを HarnessAgent に追加します。ツールの追加は ChatOptions パラメーターの Tools で指定します。ここら辺は MEAI の機能です。

var harnessAgent = chatClient.AsHarnessAgent(new()
    {
        Name = "CatAgent",
        HarnessInstructions = """
            あなたは猫型エージェントです。
            猫らしく振舞うために語尾はかならず「にゃん」にしてください。
            """,
        DisableWebSearch = true,
        ChatOptions = new()
        {
            // 天気情報取得ツールを設定する
            Tools = [AIFunctionFactory.Create(GetWeather)],
        }
    });

GetWeather メソッドは以下のように定義しました。とりあえず世界中が晴れになる平和なメソッドです。

[Description("指定した場所の天気を返す")]
string GetWeather(
    [Description("天気を取得する都市名")]
    string city)
{
    // ダミーの天気情報を返す
    return $"今日の{city}の天気は晴れです。";
}

AgentModeProviderTodoProvider はオプションで無効化もできます。今回はしませんが以下のように DisableAgentModeProviderDisableTodoProvider プロパティに true をセットすることで無効化できます。

// HarnessAgent を作成する
var harnessAgent = chatClient.AsHarnessAgent(new()
    {
        Name = "CatAgent",
        HarnessInstructions = """
            あなたは猫型エージェントです。
            猫らしく振舞うために語尾はかならず「にゃん」にしてください。
            """,
        DisableWebSearch = true,
        ChatOptions = new()
        {
            // 天気情報取得ツールを設定する
            Tools = [AIFunctionFactory.Create(GetWeather)],
        },
        // 無効化したい場合は、以下のように設定します (今回は使用しないけど、こういうこともできます。)
        DisableAgentModeProvider = true,
        DisableTodoProvider = true,
    });

AIContextProvider は Agent の GetService メソッドで取得できます。以下のような感じです。

// プロバイダーは Agent の GetService で取得出来る
var agentModeProvider = harnessAgent.GetService<AgentModeProvider>() ?? throw new InvalidOperationException();
var todoProvider = harnessAgent.GetService<TodoProvider>() ?? throw new InvalidOperationException();

そして AIContextProviderAgentSession に状態を保持するため AgentModeProviderTodoProvider を使う場合には AgentSession を作りましょう。まぁ、普通はプランモードを使うときには複数ターンの会話になるので、AgentSession はどちらにしても必須になりますね。

// AIContextProvider は AgentSession にステータスを保持するため AgentSession が必須
var session = await harnessAgent.CreateSessionAsync();

これで下準備ができたので、プランを立てて実行する流れを実行できます。プランを立てるような指示をお願いした後に execute モードにして実行をしましょう。以下のようなコードになります。

// 初期状態の plan モードで実施プランを作成する
Console.WriteLine($"現在のモード: {await agentModeProvider.GetModeAsync(session)}");
var response = await harnessAgent.RunAsync("東京と広島と京都の天気をテーブル形式に整えて表示して", session);
Console.WriteLine(response.Text);
Console.WriteLine("## TODO リスト");
foreach (var todo in await todoProvider.GetAllTodosAsync(session))
{
    Console.WriteLine($"- {todo.Title}, isCompleted: {todo.IsComplete}");
}

// 実行モードに切り替えて実際に処理を行う
await agentModeProvider.SetModeAsync(session, "execute");
Console.WriteLine("--------");
Console.WriteLine($"現在のモード: {await agentModeProvider.GetModeAsync(session)}");
response = await harnessAgent.RunAsync("実行して", session);
Console.WriteLine(response.Text);
Console.WriteLine("## TODO リスト");
foreach (var todo in await todoProvider.GetAllTodosAsync(session))
{
    Console.WriteLine($"- {todo.Title}, isCompleted: {todo.IsComplete}");
}

まず、天気を調べるプランを立てるようなことを話しかけて TODO を列挙しています。そして、次に execute モードにして、実行をお願いしてから TODO を列挙しています。実行すると以下のような結果になります。

現在のモード: plan
以下の計画で進めるにゃん。

1. 東京・広島・京都の天気を取得するにゃん。
2. 天気・気温などを比較しやすいテーブルに整理するにゃん。
3. 取得時点を添えて表示するにゃん。

この計画を実行するため、実行モードへ切り替えてよいかにゃん?
## TODO リスト
- 3都市の天気を取得, isCompleted: False
- 天気情報を表形式で整理, isCompleted: False
--------
現在のモード: execute
| 都市 | 今日の天気 |
|---|---|
| 東京 | 晴れ ?? |
| 広島 | 晴れ ?? |
| 京都 | 晴れ ?? |

3都市とも今日は晴れにゃん。
## TODO リスト
- 3都市の天気を取得, isCompleted: True
- 天気情報を表形式で整理, isCompleted: True

TODO リストが作られてから処理が実行されていることがわかると思います。こういうの作りたいけど、地味にメンドイので用意されているのはありがたいですね。
ちなみに AgentMode はデフォルトで planexecute の 2 つが用意されていますが、これはカスタマイズ可能です。次の記事くらいで書けたら書こうと思います。

参考までに今回の最終版のコードの全体は以下のようになります。
実は、最初は luna でやっていたのですが plan モードなのに実行してしまったりとちょっと不安定だったので sol にしてみました。
使い方側で工夫すれば、多分 luna でも行けると思いますが、今回はモデルで解決しちゃいました。(次の記事luna でも行ける方法を紹介しています。)

using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using System.ComponentModel;

// 接続先情報
const string ProjectEndpoint = "https://XXXXX.services.ai.azure.com/api/projects/XXXXXX";
const string ModelDeploymentName = "gpt-5.6-sol"; // ちゃんとプランを立てて実行するために sol にしてみた

// Microsoft Foundry のプロジェクトへつなぐためのクライアント
var projectClient = new AIProjectClient(
    new Uri(ProjectEndpoint),
    new AzureCliCredential());

// ChatCompletion 用のクライアントを取得して IChatClient に変換する
var chatClient = projectClient.GetProjectOpenAIClient()
    .GetChatClient(ModelDeploymentName)
    .AsIChatClient();

// HarnessAgent を作成する
var harnessAgent = chatClient.AsHarnessAgent(new()
    {
        Name = "CatAgent",
        HarnessInstructions = """
            あなたは猫型エージェントです。
            猫らしく振舞うために語尾はかならず「にゃん」にしてください。
            """,
        DisableWebSearch = true,
        ChatOptions = new()
        {
            // 天気情報取得ツールを設定する
            Tools = [AIFunctionFactory.Create(GetWeather)],
        },
    });

// プロバイダーは Agent の GetService で取得出来る
var agentModeProvider = harnessAgent.GetService<AgentModeProvider>() ?? throw new InvalidOperationException();
var todoProvider = harnessAgent.GetService<TodoProvider>() ?? throw new InvalidOperationException();

// AIContextProvider は AgentSession にステータスを保持するため AgentSession が必須
var session = await harnessAgent.CreateSessionAsync();

// 初期状態の plan モードで実施プランを作成する
Console.WriteLine($"現在のモード: {await agentModeProvider.GetModeAsync(session)}");
var response = await harnessAgent.RunAsync("東京と広島と京都の天気をテーブル形式に整えて表示して", session);
Console.WriteLine(response.Text);
Console.WriteLine("## TODO リスト");
foreach (var todo in await todoProvider.GetAllTodosAsync(session))
{
    Console.WriteLine($"- {todo.Title}, isCompleted: {todo.IsComplete}");
}

// 実行モードに切り替えて実際に処理を行う
await agentModeProvider.SetModeAsync(session, "execute");
Console.WriteLine("--------");
Console.WriteLine($"現在のモード: {await agentModeProvider.GetModeAsync(session)}");
response = await harnessAgent.RunAsync("実行して", session);
Console.WriteLine(response.Text);
Console.WriteLine("## TODO リスト");
foreach (var todo in await todoProvider.GetAllTodosAsync(session))
{
    Console.WriteLine($"- {todo.Title}, isCompleted: {todo.IsComplete}");
}

[Description("指定した場所の天気を返す")]
string GetWeather(
    [Description("天気を取得する都市名")]
    string city)
{
    // ダミーの天気情報を返す
    return $"今日の{city}の天気は晴れです。";
}

まとめ

ということで Microsoft Agent Framework で GA された HarnessAgent を使ってみました。
前にプレビューの時に使ったときとは API が変わっていたりするのですが、おおむねできることは同じだと思います。
今回は、一番わかりやすい Plan モードと TODO 管理機能を使いましたが、最初に説明した通りその他の機能も持っていたりします。

ですが、いったん今回はここらへんまでで力つきたので他の機能は今後の記事に譲りたいと思います!

Microsoft (有志)

Discussion