Cline で作った Devin 風ワークフローを Microsoft Agent Framework で再構築する③ - Interrupt / Resume 実装編

前回は Microsoft Agent Framework (以下 MAF と表記します) の Workflow 機能を使用してグラフ構造の作成方法を確認しました。

今回は Request / Response という機能を使用して、LangGraph の Interrupt / Resume に相当する機能を実装していきます。

この記事で使用している実行環境は以下のようになっています。

  • VS Code (Windows 版)・・・バージョン 1.141.2
  • C# Dev Kit (Windows 版)・・・バージョン 3.40.210
  • .NET SDK (Windows 版)・・・バージョン 10.0.402

Request / Response 機能のサンプル

こちら の公式ページにあるサンプルを見て仕組みを学習します。この機能は、エージェントループに人の介在 (Human-in-the-Loop) を行えるようにする仕組みですが、LangGraph の場合と同じように Kanban タスクの終了と同期させる用途にも使用可能です。

実際の処理はもう少し複雑ですが、概念的には以下のようになります。

  • RequestPort を使用して Request / Response の情報をやり取りする
  • RequestPort に Request を送信すると RequestInfoEvent が発生し、Workflow は Response 待ちになる
  • SendResponseAsync で Response を返すと Workflow が再開し、後続の処理が実行される

動作確認のため、検証用のサンプルを作成してみます。

まずは何か適当なフォルダ (RequestResponseTest 等) を作成して、VS Code で開きます。VS Code のターミナル画面で以下のように入力して、サンプルプロジェクトのひな型を作成します。

dotnet new console

続けて以下のコマンドを実行して、必要なパッケージをインストールします。

dotnet add package Microsoft.Agents.AI.Workflows --prerelease

まずは送受信するデータ構造を考えます。「Program.cs」ファイルを開いて以下のデータ構造を追加します。(実際に実行する場合は、Program.cs ファイルの「一番最後」にこの定義を追加しないとコンパイルエラーになります。)

// ----------------------------------------
// Request / Response のデータ型
// ----------------------------------------

public sealed record KanbanRequest(string TaskId);

public sealed record KanbanResult(
    string TaskId,
    bool Success,
    string Message
);

次に RequestPort と、結果を出力する Executor を含む Workflow を作成します。

RequestPort に Response を返すと Workflow 自体は再開しますが、このサンプルでは返された結果を確認しやすくするため、WorkflowOutputEvent を発生させる出力用 Executor を追加しています。

// ----------------------------------------
// Workflow を作成
// ----------------------------------------

// KanbanRequest を送り、KanbanResult を受け取る RequestPort
var kanbanPort =
    RequestPort.Create<KanbanRequest, KanbanResult>("Kanban");

// WorkflowOutputEvent イベント発生用 Executor
var outputExecutor =
    new FunctionExecutor<KanbanResult, KanbanResult>(
        "KanbanResultOutput",
        (result, _, _) => result
    );

// 実際には Planner や Executor から
// この RequestPort に Request を送る Workflow を構築する想定
Workflow workflow = new WorkflowBuilder(kanbanPort)
    .AddEdge(kanbanPort, outputExecutor)
    .WithOutputFrom(outputExecutor)
    .Build();

最初のグラフで表現すると、以下のようになります。

作成した Workflow を実行するコマンドを追加します。同時に KanbanRequest データを引数で渡します。

// ----------------------------------------
// Workflow 開始
// ----------------------------------------

await using StreamingRun run =
    await InProcessExecution.RunStreamingAsync(
        workflow,
        new KanbanRequest("T0001")
    );

Request 発生時と Workflow 完了時の処理を行うイベントハンドラを追加します。

送信イベント (RequestInfoEvent) の場合はダミーのタスク監視メソッド「WaitForKanbanAsync」を呼び出しています。Workflow 完了時 (WorkflowOutputEvent) は「Workflow 完了: KanbanResult {...」と表示します。

// ----------------------------------------
// Workflow のイベントを監視
// ----------------------------------------

await foreach (WorkflowEvent evt in run.WatchStreamAsync())
{
    switch (evt)
    {
        // Workflow が外部応答待ちになった
        case RequestInfoEvent requestEvent:
        {
            Console.WriteLine("Kanban の完了待ちに入りました。");

            // --------------------------------
            // KanbanRequest データを取得
            // --------------------------------
            if (!requestEvent.Request.TryGetDataAs<KanbanRequest>(
                    out KanbanRequest? kanbanRequest) ||
                kanbanRequest is null)
            {
                throw new InvalidOperationException(
                    "KanbanRequest データを取得できませんでした。"
                );
            }

            // --------------------------------
            // ここで Kanban を監視
            // --------------------------------
            KanbanResult result =
                await WaitForKanbanAsync(kanbanRequest.TaskId);

            Console.WriteLine("Kanban が完了しました。");

            // --------------------------------
            // Workflow に Response を返す
            // --------------------------------
            var response =
                requestEvent.Request.CreateResponse(result);

            await run.SendResponseAsync(response);

            break;
        }

        // Workflow 全体が完了
        case WorkflowOutputEvent outputEvent:
        {
            Console.WriteLine(
                $"Workflow 完了: {outputEvent.Data}"
            );

            return;
        }
    }
}

ダミーの監視関数を追加します。今回は動作確認のため、3秒待機したあと強制的に完了扱いにしています。実際の Kanban 連携では、この部分をタスクのステータスを確認する処理に置き換えます。

// ----------------------------------------
// Kanban 監視処理
// ----------------------------------------

static async Task<KanbanResult> WaitForKanbanAsync(string taskId)
{
    while (true)
    {
        Console.WriteLine($"{taskId} を監視中...");

        // 本番では
        // var status = ReadKanbanStatus(taskId);
        // のような処理になる。

        await Task.Delay(TimeSpan.FromSeconds(3));

        // サンプルなので Done になったことにする
        bool done = true;

        if (done)
        {
            return new KanbanResult(
                taskId,
                true,
                "Kanban task completed"
            );
        }
    }
}

以上でサンプルの作成は終了です。

実際に実行してみると、RequestInfoEvent の発生後に Kanban の監視処理へ入り、SendResponseAsync で結果を返すと Workflow が再開して WorkflowOutputEvent が発生することを確認できました。

ここまでのサンプルコードをまとめたものは、以下のようになっています。

クリックすると展開します。(Program.cs)
using Microsoft.Agents.AI.Workflows;

// ----------------------------------------
// Workflow を作成
// ----------------------------------------

// KanbanRequest を送り、KanbanResult を受け取る RequestPort
var kanbanPort =
    RequestPort.Create<KanbanRequest, KanbanResult>("Kanban");

// WorkflowOutputEvent イベント発生用 Executor
var outputExecutor =
    new FunctionExecutor<KanbanResult, KanbanResult>(
        "KanbanResultOutput",
        (result, _, _) => result
    );

// 実際には Planner や Executor から
// この RequestPort に Request を送る Workflow を構築する想定
Workflow workflow = new WorkflowBuilder(kanbanPort)
    .AddEdge(kanbanPort, outputExecutor)
    .WithOutputFrom(outputExecutor)
    .Build();

// ----------------------------------------
// Workflow 開始
// ----------------------------------------

await using StreamingRun run =
    await InProcessExecution.RunStreamingAsync(
        workflow,
        new KanbanRequest("T0001")
    );

// ----------------------------------------
// Workflow のイベントを監視
// ----------------------------------------

await foreach (WorkflowEvent evt in run.WatchStreamAsync())
{
    switch (evt)
    {
        // Workflow が外部応答待ちになった
        case RequestInfoEvent requestEvent:
        {
            Console.WriteLine("Kanban の完了待ちに入りました。");

            // --------------------------------
            // KanbanRequest データを取得
            // --------------------------------
            if (!requestEvent.Request.TryGetDataAs<KanbanRequest>(
                    out KanbanRequest? kanbanRequest) ||
                kanbanRequest is null)
            {
                throw new InvalidOperationException(
                    "KanbanRequest データを取得できませんでした。"
                );
            }

            // --------------------------------
            // ここで Kanban を監視
            // --------------------------------
            KanbanResult result =
                await WaitForKanbanAsync(kanbanRequest.TaskId);

            Console.WriteLine("Kanban が完了しました。");

            // --------------------------------
            // Workflow に Response を返す
            // --------------------------------
            var response =
                requestEvent.Request.CreateResponse(result);

            await run.SendResponseAsync(response);

            break;
        }

        // Workflow 全体が完了
        case WorkflowOutputEvent outputEvent:
        {
            Console.WriteLine(
                $"Workflow 完了: {outputEvent.Data}"
            );

            return;
        }
    }
}


// ----------------------------------------
// Kanban 監視処理
// ----------------------------------------

static async Task<KanbanResult> WaitForKanbanAsync(string taskId)
{
    while (true)
    {
        Console.WriteLine($"{taskId} を監視中...");

        // 本番では
        // var status = ReadKanbanStatus(taskId);
        // のような処理になる。

        await Task.Delay(TimeSpan.FromSeconds(3));

        // サンプルなので Done になったことにする
        bool done = true;

        if (done)
        {
            return new KanbanResult(
                taskId,
                true,
                "Kanban task completed"
            );
        }
    }
}

// ----------------------------------------
// Request / Response のデータ型
// ----------------------------------------

public sealed record KanbanRequest(string TaskId);

public sealed record KanbanResult(
    string TaskId,
    bool Success,
    string Message
);

短いサンプルでしたが、前回と今回の機能を合わせると以下に示した Kanban ループの青い点線で囲んだ部分を置き換えることができます。

MAF には Workflow の状態を保存して後から再開する Checkpoint 機能もあります。ただし今回の用途では、Kanban タスクの実行中もアプリケーション自体は起動したままとするため、Checkpoint は使用せず Request / Response による待機だけで実装します。

アプリを終了せずに待機できるため、デバッグもしやすくなります。
(※ LangGraph でも、アプリを終了せずに async / await などを使用して同様の構成にすることが可能です。ただし、その場合でも DB やメモリを使用してチェックポイントを保存する必要がある点が MAF と異なっています。)

まとめ

今回は Microsoft Agent Framework の Request / Response 機能を使用して、LangGraph の Interrupt / Resume に相当する処理を実装しました。

RequestPort から Request を送信すると Workflow を一時的に Response 待ちの状態にでき、外部処理の完了後に SendResponseAsync で Response を返すことで、停止していた Workflow を再開できることを確認しました。

今回作成したサンプルでは、Kanban タスクの完了待ちをダミー処理で実装しましたが、実際の開発ではこの部分を Kanban のステータス監視処理に置き換え、タスク生成処理と組み合わせることで、MAF から Kanban タスクを起動し、処理完了後に Workflow を再開する仕組みを構築できます。

また、MAF には Checkpoint を使用して Workflow の状態を保存・復元する機能もありますが、今回のようにアプリケーションを起動したまま外部処理の完了を待つ用途では、Request / Response のみでも比較的シンプルに実装できます。

次回は、今回作成した仕組みを実際の Kanban ループに組み込み、MAF からタスクを起動して処理結果を受け取りながら Workflow を進める部分を実装していきます。

今回は以上です。