GitHub Copilot SDK を使用した AI チャット形式のデバッガー アプリの実装

Last Update:
このエントリーをはてなブックマークに追加

こんにちは、Japan Developer Support Core チームの松井です。GitHub Copilot はエージェント モードの登場により、コード補完や質問への回答といった領域のタスクだけでなく、IDE やエディター上での AI による高度な作業の自動化が可能になっています。一方で、「AI の能力を IDE 上で使用するだけでなく自分のアプリケーションに組み込みたい」「高度な自動化・並列化を伴うエージェント ワークフローを構築したい」「権限やデータの取り扱いを自前で制御したい」といったニーズに応えることは難しい場合があります。GitHub Copilot SDK を使用するとこういったニーズに対応することが可能になります。本記事では、WinDbgX のデバッガー エンジンを "ツール" として実装し、自然言語で対話的にデバッグ情報を取得できるコンソール アプリケーションの実装を例として紹介します。なお、本記事の執筆時点で GitHub Copilot SDK はテクニカル プレビューの段階であり、GA に至るまでに変更される可能性があります。また、Microsoft のサポート チームでは GitHub Copilot SDK に関するサポートは提供していないため、あくまでも参考として本記事をご覧いただけると幸いです。

GitHub Copilot SDK について

GitHub Copilot SDK は、GitHub Copilot の機能を自分のアプリケーションやサービスから利用できるようにするための SDK です。GitHub Copilot の機能は既に GitHub Copilot CLI としてコマンド ライン ツールで提供されていますが、Copilot SDK は CLI と同じエンジンをアプリケーションからの呼び出しのために公開しています。例えば、Copilot SDK を使用することで、アプリケーション開発者は AI モデルへのメッセージの送信とレスポンスの受信といった基本的な操作の他、ツール呼び出しの管理、フックといった様々な操作を行うことができます。最新の情報やサンプル プログラムについては GitHub Copilot SDK のリポジトリや GitHub のブログ記事などを参照してください。

参考情報

前提条件

本記事の手順を実施するには、以下の準備が必要です。

  • Visual Studio 2026 以降 + .NET デスクトップ開発のワークロード
  • GitHub Copilot CLI のインストールとセットアップ
  • GitHub Copilot のサブスクリプション
  • インターネット接続

ステップ 1: GitHub Copilot SDK を使用したチャット部分の実装

まずは、GitHub Copilot SDK を使用して、ユーザーからの自然言語のリクエストを受け取って AI との対話を行うチャット部分の実装を行います。

  1. Visual Studio 2026 を起動し、新しいコンソール アプリケーションの作成を進めます。本記事では名前を "dbgx-copilot" としておきますが、任意の名前で構いません。 プロジェクトの作成

  2. プロジェクトの追加情報としてターゲット フレームワークを ".NET 10.0" に設定し、"最上位レベルのステートメントを使用しない" オプションを有効にしてプロジェクトを作成します。 プロジェクトの構成

  3. プロジェクトが作成されたら、NuGet パッケージ マネージャーから GitHub.Copilot.SDK パッケージをインストールします。これにより、GitHub Copilot SDK をプロジェクト内で使用できるようになります。 GitHub Copilot SDK のインストール

  4. 同様に、Nuget パッケージ マネージャーから Microsoft.Extensions.Logging.Debug パッケージもインストールします。このパッケージのインストールは必須ではありませんが、GitHub Copilot SDK のログをデバッグ出力するために使用します。動作を理解したり、問題が発生した際のトラブルシューティングに役立ちます。

  5. Program.cs を開き、GitHub Copilot SDK を使用してユーザーからの入力を受け取り、AI との対話を行う基本的なコードを実装します。

コード (Program.cs)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
using GitHub.Copilot.SDK;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Logging;
using System.ComponentModel;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Text.RegularExpressions;

namespace DbgXCopilot;

internal class Program
{

// システム プロンプト
// AI の役割や振る舞いを定義するための指示で、ユーザーには見えない形でモデルに渡されます
// すべての会話ターンに先立って適用され、応答のトーンや範囲の制御などを行うために使用できます。
static readonly string _systemMessage = """
あなたは WinDbgX のデバッガー エンジンを使用してアプリケーションの問題を診断するエージェントです。
ダンプ ファイルや Time Travel Debugging トレースを分析し、原因の特定、解決策の提案を行います。
また、トラブルシュートやデバッグに関する一般的な質問や相談にも答えます。
あなたの応答はコンソールに表示されますので、表示されない可能性がある文字は使用しません。
""";

static async Task Main(string[] args)
{
using var loggerFactory = LoggerFactory.Create(builder => builder
.AddDebug()
.SetMinimumLevel(LogLevel.Trace));

try
{
await RunAsync(loggerFactory);
}
finally
{
Console.ResetColor();
}
}

private static async Task RunAsync(ILoggerFactory loggerFactory)
{
// (1) GitHub Copilot SDK を使用するための初期化
CopilotClientOptions options = new()
{
Logger = loggerFactory.CreateLogger<CopilotClient>()
};
await using CopilotClient client = new(options);
await client.StartAsync();

// (2) 認証 (GitHub CLI でログイン済みの場合は不要)
if (!await LoginIfNeeded(client))
{
Console.WriteLine("GitHub アカウントでのログインに失敗しました。");
return;
}

// (3) セッションの開始
SessionConfig sessionConfig = new()
{
SystemMessage = new() { Content = _systemMessage },
Model = await SelectModelAsync(client),
Tools =
[
AIFunctionFactory.Create(EndConversation), // AI が会話の終了を判断したときに呼び出すツール
],
OnPermissionRequest = PromptPermission, // 許可要求のハンドラー
};
await using var session = await client.CreateSessionAsync(sessionConfig);

// (4) イベント ハンドラーの追加
TaskCompletionSource done = new();
session.On(evt =>
{
switch (evt)
{
// AI からのメッセージ受信イベント
case AssistantMessageEvent msg:
if (!string.IsNullOrEmpty(msg.Data.Content))
{
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine($"\n[アシスタント]\n{msg.Data.Content}");
Console.ResetColor();
}
break;

// ユーザーのプロンプトに対する AI の応答が完了しアイドル状態になったイベント
case SessionIdleEvent:
done.TrySetResult();
break;
}
});

// (5) 会話ループ

// エージェントから説明させるため初期プロンプト
await session.SendAsync(new MessageOptions { Prompt = "挨拶して、あなたができることを簡潔に説明してください。" });
await done.Task;

// 終了フラグが立つまでプロンプト送信と応答を繰り返す
while (!exitRequested)
{
done = new TaskCompletionSource();
Console.ForegroundColor = ConsoleColor.White;
Console.WriteLine("\n[プロンプト]");
Console.ResetColor();
string? userPrompt = Console.ReadLine();
await session.SendAsync(new MessageOptions { Prompt = userPrompt! });
await done.Task;
}
}

// 認証されていない場合は GitHub ログインを実行します。
private static async Task<bool> LoginIfNeeded(CopilotClient client)
{
var authStatus = await client.GetAuthStatusAsync();
if (!authStatus.IsAuthenticated)
{
ProcessStartInfo psi = new()
{
FileName = GetBundledCliPath(),
Arguments = "login",
UseShellExecute = true,
};
Console.WriteLine(psi.FileName + " " + psi.Arguments);
using var p = Process.Start(psi);
p?.WaitForExit();

authStatus = await client.GetAuthStatusAsync();
if (!authStatus.IsAuthenticated)
{
return false;
}
}
return true;
}

// NuGet パッケージに同梱されている Copilot CLI のパスを取得します。
static string GetBundledCliPath()
{
var portableRid = RuntimeInformation.OSArchitecture switch
{
Architecture.X64 => "win-x64",
Architecture.Arm64 => "win-arm64",
_ => throw new PlatformNotSupportedException()
};
var executablePath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
return Path.Combine(executablePath!, "runtimes", portableRid, @"native\copilot.exe");
}

// 利用可能なモデルを表示してユーザーに選択させた結果を返します。
static async Task<string> SelectModelAsync(CopilotClient client)
{
var models = await client.ListModelsAsync();
foreach (var (model, i) in models.Select((model, i) => (model, i)))
{
Console.WriteLine($"[{i:d2}] {model.Id,-26} {model.Billing?.Multiplier}x");
}

Console.Write("モデルの番号を選択してください (既定値:0): ");
int.TryParse(Console.ReadLine(), out var selectedIndex);
return models[Math.Clamp(selectedIndex, 0, models.Count - 1)].Id;
}

// AI から会話を終了するときに AI が呼び出すツールと終了フラグ
static bool exitRequested = false;

[DisplayName(nameof(EndConversation))]
[Description("""
ユーザーが会話の終了を意図していると判断した場合に呼び出します。
(例: 'さようなら', '終了', 'bye', 'quit' など)。
""")]
static void EndConversation()
{
exitRequested = true;
}

// 許可の要求イベントのハンドラー
static async Task<PermissionRequestResult> PromptPermission(PermissionRequest request, PermissionInvocation invocation)
{
switch (request)
{
case PermissionRequestCustomTool requestForCustomTool:
// 会話終了ツールの場合は常に承認します。
if (requestForCustomTool.ToolName == nameof(EndConversation))
{
return new() { Kind = PermissionRequestResultKind.Approved };
}
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine("\n[確認]");
Console.WriteLine("AI が以下の操作を実行しようとしています。");
Console.WriteLine($"- ツール名: {requestForCustomTool.ToolName}");
Console.WriteLine($"- 引数: {requestForCustomTool.Args}");
Console.WriteLine("許可しますか? ([Y]es / [N]o)");

string? input = Console.ReadLine()?.Trim().ToUpperInvariant();
Console.ResetColor();
if (input is "Y" or "YES")
{
return new() { Kind = PermissionRequestResultKind.Approved };
}
else
{
return new() { Kind = PermissionRequestResultKind.DeniedInteractivelyByUser };
}

default:
// MCP ツールなど他の承認要求の処理は実装を省略しています。
return new() { Kind = PermissionRequestResultKind.Approved };
}
}

}

コードの解説

コードは少々長くなっていますが、大きな流れは (1) GitHub Copilot SDK を使用するための初期化、(2) 認証、(3) セッションの開始、(4) イベント ハンドラーの定義と追加、(5) 会話ループというシンプルなものです。認証は GitHub CLI でログイン済みの場合は省略可能です。

このコードを実行すると、コンソール上で AI と対話的に会話を行うことができます。Visual Studio でデバッグを開始してステップ実行してみると理解が深まると思います。特に、イベントや許可要求のハンドラー、EndConversation メソッドなどにブレークポイントを設定して、AI からのメッセージ受信や会話終了のタイミングを確認してみるのもおすすめです。

ツールについて

セッションの開始時にAIFunctionFactory.Create(EndConversation) で EndConversation メソッドをツールとして登録しています。ツールは、AI が会話の中で特定の操作を実行するための手段となります。例えば、EndConversation のようなツールを定義して登録しておくと、AI はユーザーの入力に基づいて会話を終了する必要があると判断したときに EndConversation ツールを呼び出すことができます。ツールは AI が実行できる操作を定義するもので、AI は会話の中で適切なタイミングでこれらのツールを呼び出すことができます。また、GitHub Copilot では、powershell (PowerShell コマンドレットの実行) や view (ファイルの読み取り) といった組み込みのツールも提供されています。

本記事ではこの後のステップで WinDbgX のデバッガー エンジンを操作するためのツールを実装しますが、その他にも AI に実行させたい操作がある場合はツールとして実装して登録しておくと、AI がそれらの操作を会話の中で使用できるようになります。トラブルシュートやデバッグのシナリオにおいては、例えば「イベント ログを読み込む」、「Windows Error Reporting でダンプ出力を構成する」といった操作をツールとして実装しておくと、AI がユーザーのリクエストに応じてこれらの操作を実行できるようになるため便利です。

許可要求のハンドラー (SessionConfig.OnPermissionRequest) について

ツールは AI が会話の中で呼び出すことができる操作を定義するものでしたが、許可要求のハンドラーは、AI がツールを呼び出そうとしたときに、その呼び出しを許可するかどうかを判断するためのフック ポイントになります。GitHub Copilot SDK では、SessionConfig.OnPermissionRequest で許可要求のハンドラーを指定することができます。AI がツールを呼び出そうとするとこのハンドラーが呼び出され、ユーザーはその呼び出しを許可するかどうかを選択できます。

例えばデバッガー エンジンを操作するツールは強力な操作になる可能性があるため、意図しない操作が実行されないようにツールの呼び出しに対して許可要求のハンドラーで判断を行うようにすることができます。ユーザーが許可を与えた場合にのみツールの呼び出しを許可するようにすることで、AI の操作に対してユーザーが厳密なコントロールを持つことができます。システム プロンプトなどでツールの呼び出しに対するポリシーを指示することも可能ですが、プロンプトに確実な強制力はなく AI が誤った判断をする可能性もあるため、アプリケーション側の実装で決定的にコントロールできるようにしておくことも重要です。

ステップ 2: WinDbgX のデバッガー エンジンを操作するツールの実装

次に、AI が WinDbgX のデバッガー エンジンを操作するためのツールを実装します。ツールを実装するためには、ツールとして呼び出されるメソッドを定義し、そのメソッドを AIFunctionFactory.Create メソッドでツールとしてラップして SessionConfig.Tools に追加する必要があります。ツールとして呼び出されるメソッドは、AI からのリクエストを受け取って適切な操作を実行し、その結果を AI に返す役割を持ちます。

  1. NuGet パッケージ マネージャーから Microsoft.Debugging.Platform.DbgX パッケージと Microsoft.Debugging.Platform.SymSrv パッケージをインストールします。これにより、デバッガー エンジンを操作するための API をプロジェクト内で使用できるようになります。

  2. プロジェクト ファイルをテキストとして開き、TargetFramework を net10.0-windows10.0.17763.0 に変更します。DbgX API を使用するためには、ターゲット フレームワークを Windows 10 バージョン 1809 (ビルド 17763) 以降にする必要があります。また、Microsoft.Debugging.Platform.SymSrv パッケージのシンボル サーバーの機能を使用するために、同パッケージに同梱されているファイルを出力ディレクトリにコピーする設定も追加します。PackageReference タグに GeneratePathProperty="true" を追加してパッケージのインストール先パスを MSBuild プロパティとして利用できるようにし、ItemGroup タグで同梱ファイルを出力ディレクトリにコピーする設定を追加します。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    <Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0-windows10.0.17763.0</TargetFramework>
    <RootNamespace>dbgx_copilot</RootNamespace>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    </PropertyGroup>

    <ItemGroup>
    <PackageReference Include="GitHub.Copilot.SDK" Version="0.2.0" />
    <PackageReference Include="Microsoft.Debugging.Platform.DbgX" Version="20260112.1.0" />
    <PackageReference Include="Microsoft.Debugging.Platform.SymSrv" Version="20260109.1235.0" GeneratePathProperty="true" />
    <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="10.0.5" />
    </ItemGroup>

    <ItemGroup>
    <None Include="$(PkgMicrosoft_Debugging_Platform_SymSrv)\content\**\*" CopyToOutputDirectory="PreserveNewest" Visible="False" Link="%(RecursiveDir)%(FileName)%(Extension)" />
    </ItemGroup>

    </Project>
  3. プロジェクトに新しいクラス ファイル "DebuggerTools.cs" を追加します。

  4. 追加した "DebuggerTools.cs" に WinDbgX のデバッガー エンジンを操作するためのツールとして呼び出されるメソッドを実装します。今回は必要最小限の実装として、ダンプ ファイルを開く OpenDumpFile メソッドと、デバッガー エンジンでコマンドを実行する ExecuteDebuggerCommand メソッドを実装します。

  5. 最後に、Program.cs のセッションの開始部分で、AIFunctionFactory.Create を使用してこれらのメソッドをツールとして登録します。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // (3) セッションの開始
    await using var debuggerTools = await DebuggerTools.CreateAsync(loggerFactory);
    SessionConfig sessionConfig = new()
    {
    SystemMessage = new() { Content = _systemMessage },
    Model = await SelectModelAsync(client),
    Tools =
    [
    AIFunctionFactory.Create(EndConversation), // AI が会話の終了を判断したときに呼び出すツール
    AIFunctionFactory.Create(debuggerTools.OpenDumpFile), // ダンプ ファイルを開くツール
    AIFunctionFactory.Create(debuggerTools.ExecuteDebuggerCommand), // デバッガー コマンドを実行するツール
    ],
    OnPermissionRequest = PromptPermission
    };
    await using var session = await client.CreateSessionAsync(sessionConfig);
コード (DebuggerTools.cs)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
using DbgX;
using DbgX.Interfaces.Enums;
using DbgX.Interfaces.Listeners;
using DbgX.Interfaces.Services.Internal;
using DbgX.Requests;
using DbgX.Requests.Initialization;
using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
using System.ComponentModel;
using System.Diagnostics;
using System.Reflection;

namespace DbgXCopilot;

internal class DebuggerTools : IAsyncDisposable
{
// DbgEng requires all interactions to be on the same thread, so we create a dedicated synchronization context and thread for it.
private sealed class DebuggerSynchronizationContext : SynchronizationContext, IDisposable
{
private readonly BlockingCollection<(SendOrPostCallback Callback, object? State)> _queue = [];
private readonly Thread _thread;

public DebuggerSynchronizationContext()
{
_thread = new Thread(Run) { IsBackground = true, Name = nameof(DebuggerSynchronizationContext) };
_thread.Start();
}

public override void Post(SendOrPostCallback d, object? state) => _queue.Add((d, state));

private void Run()
{
SetSynchronizationContext(this);
foreach (var (callback, state) in _queue.GetConsumingEnumerable())
callback(state);
}

public void Dispose()
{
_queue.CompleteAdding();
_thread.Join();
_queue.Dispose();
}
}

// Simple IDbgReporter implementation that logs to the provided ILogger.
private class Reporter(ILogger logger) : IDbgReporter
{
private readonly ILogger _logger = logger;
public void Error(bool _, string message) => _logger.LogError(message);
public void Error(bool _, Exception exception, string? message = null) => _logger.LogError(exception, message);
public void Info(string message) => _logger.LogInformation(message);
public void Warning(string message) => _logger.LogWarning(message);
}

// Customization of engine paths to use the ones bundled with this application.
// Note that current SymSvr NuGet package does not copy the binary.
// Therefore, in this project, copying them to the output directory by MSbuild.
// See the .csproj file for the implementation to copy the binaries.
private class EnginePathCustomization : IDbgEnginePathCustomization
{
public string HomeDirectory => Environment.ExpandEnvironmentVariables("%TEMP%");
public string GetEngHostPath(string arch) => Path.Combine(GetEnginePath(arch), "EngHost.exe");
public string GetEnginePath(string arch)
{
var assemblyDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
return Path.Combine(assemblyDir!, arch);
}
}

// Override the default process creation to ensure the engine host process is created with the bundled binaries.
private Process? CreateEngHostProcess(CreateOutOfProcessArgs args)
{
var psi = new ProcessStartInfo
{
FileName = args.EngHostPath,
Arguments = args.Arguments,
CreateNoWindow = true,
UseShellExecute = false
};
return Process.Start(psi);
}

private readonly DebuggerSynchronizationContext _syncContext;
private readonly DebugEngine _engine;

internal static async Task<DebuggerTools> CreateAsync(ILoggerFactory loggerFactory)
{
DebuggerSynchronizationContext syncContext = new();
return await InvokeAsync(syncContext, async () => new DebuggerTools(loggerFactory, syncContext));
}

private DebuggerTools(ILoggerFactory loggerFactory, DebuggerSynchronizationContext syncContext)
{
EnginePathCustomization customization = new();
var logger = loggerFactory.CreateLogger<DebugEngine>();
Reporter reporter = new(logger);
_engine = new (customization, reporter, null, null, false, CreateEngHostProcess, syncContext, null, null);
_syncContext = syncContext;
_ensureNoShell = new(() => InvokeAsync(async () => await _engine.SendRequestAsync(new ExecuteRequest(".noshell"))));
}

public async ValueTask DisposeAsync()
{
try
{
await InvokeAsync(async () =>
{
_engine.Dispose();
return true;
});
}
catch (OperationCanceledException)
{
// Engine may cancel pending I/O during shutdown.
}
_syncContext.Dispose();
}

// Perform .noshell command to prevent accidentally running shell commands by AI.
private readonly Lazy<Task> _ensureNoShell;
private Task EnsureNoShellAsync() => _ensureNoShell.Value;

[Description("指定されたパスのダンプ ファイルを開きます。既に開かれている場合は停止した後に開きます。")]
public Task<bool> OpenDumpFile([Description("ダンプ ファイルのパス")] string filePath)
{
return InvokeAsync(async () =>
{
if (_engine.DebuggingState.TargetType != TargetType.NoTarget)
{
await _engine.StopDebuggingAsync();
}
return await _engine.SendRequestAsync(new OpenDumpFileRequest(filePath, new()));
});
}

[Description("指定されたデバッガー コマンドを実行して結果を返します。")]
public async Task<string> ExecuteDebuggerCommand([Description("デバッガー コマンド")] string command)
{
await EnsureNoShellAsync();
return await InvokeAsync(async () => await _engine.SendRequestAsync(new ExecuteToStringRequest(command)));
}

#region Helper methods to marshal calls to the debugger synchronization context
private Task<TResult> InvokeAsync<TResult>(Func<Task<TResult>> asyncAction) =>
InvokeAsync(_syncContext, asyncAction);

private static Task<TResult> InvokeAsync<TResult>(SynchronizationContext syncContext, Func<Task<TResult>> asyncAction)
{
TaskCompletionSource<TResult> tcs = new();
syncContext.Post(_ =>
{
try
{
asyncAction().ContinueWith(task =>
{
if (task.IsCanceled) tcs.TrySetCanceled();
else if (task.IsFaulted) tcs.TrySetException(task.Exception!.InnerExceptions);
else tcs.TrySetResult(task.Result);
}, TaskScheduler.FromCurrentSynchronizationContext());
}
catch (Exception ex)
{
tcs.TrySetException(ex);
}
}, null);
return tcs.Task;
}
#endregion
}

コードの解説

今回実装したツールは、ダンプ ファイルをデバッガーで開く OpenDumpFile メソッド、およびデバッガー コマンドを実行する ExecuteDebuggerCommand メソッドの 2 つです。

ツールを実装する際のポイントとして、Description 属性を使用してツールの説明を付与しておくと AI がツールの目的や使用方法を理解しやすくなります。また、ツールの引数に対しても Description 属性を使用して説明を付与することができます。AI はこれらの説明を参考にして、適切なタイミングや方法でツールを呼び出すことができます。同様に、エラーが発生する可能性のある操作をツールとして実装する場合は、エラーの内容や対処方法を説明することも重要です。AI がエラーの内容を理解し、ユーザーに適切なフィードバックを提供できるようになります。

なお、Microsoft.Debugging.Platform.DbgX パッケージや Microsoft.Debugging.Platform.SymSrv パッケージの利用方法の詳細については本記事では割愛させていただきます。

セキュリティの考慮事項

デバッガー コマンドの中には .shell コマンドのように外部プログラムを呼び出すことができるものもあります。AI がこれらのコマンドを使用して任意のコードを実行することがないように、今回の実装ではツールの初回呼び出し時に .noshell コマンドを実行してシェル コマンドの使用を無効化しています。システム プロンプトなどで危険な操作を控えるように指示することもできますが、AI が誤った判断をする可能性もあるため、ツール側の実装でコントロールできるようにしておくことも重要です。また、ツールの拡張としてライブ デバッグの対応を追加することも考えられますが、この場合はメモリやレジスタの操作も危険な操作となる可能性があるため、より慎重に実装する必要があります。

動作確認

ここまでの実装が完了したら、実際にアプリケーションを実行して AI との対話やツールの呼び出しが期待通りに動作するかを確認してみましょう。

まずは、テストに使用するダンプ ファイルを生成するための設定を行います。今回は Windows Error Reporting (WER) を使用してクラッシュ ダンプ ファイルを取得します。以下のテキストをコピーして localdumps.reg のファイル名で保存し、インポートしてください。これにより、クラッシュ ダンプ ファイルが C:\Logs フォルダーに保存されるようになります。

1
2
3
4
5
6
Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps]
“DumpType”=dword:00000002
“DumpFolder”=hex(2):43,00,3a,00,5c,00,4c,00,6f,00,67,00,73,00,00,00
“DumpCount”=dword:0000000a

次に以下のような C++ コードをコンパイルして実行し、クラッシュ ダンプ ファイルを生成します。このコードは意図的にヒープのバッファ オーバーフローを引き起こすものです。今回は x64|Release の構成でビルドします。このとき、exe ファイルと一緒に出力されるシンボル ファイル (*.pdb ファイル) はデバッグで必要となりますので、削除しないでください。シンボル ファイルがないと、ダンプ ファイルの解析時にソース コードの行番号などの情報が得られません。(参考情報: シンボル ファイルと Visual Studio のシンボル設定を理解する)

1
2
3
4
5
6
7
8
9
10
11
#pragma optimize("", off)
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int main()
{
char* buffer = new char[8];
scanf("%s", buffer);
printf("%s\n", buffer);
delete[] buffer;
}

プロジェクトをビルドして実行すると入力の待機状態となりますので、8 バイトを超える適当な文字列を入力してください。アプリケーションがクラッシュし、C:\Logs フォルダーにクラッシュ ダンプ ファイルが生成されるはずです。

ダンプ ファイルが生成されたら、dbgx-copilot プロジェクトを実行して実際にダンプ ファイルを開いて解析してもらいます。AI がツールを呼び出すときに許可要求のハンドラーが呼び出されるので、ツールの呼び出しを許可してください。

AI の応答例を以下に示します。AI がダンプ ファイルを開いて解析し、ヒープのバッファー オーバーランが原因でクラッシュが発生していることを特定できていることがわかります。また、根本原因の説明と対処方法の提案も行われています。(使用するモデルやプロンプトの内容などによって、AI の応答は異なる可能性があります。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
[アシスタント]
こんにちは!WinDbgX デバッグ アシスタントです。

私ができることは以下の通りです:

- ダンプファイルや TTD トレースの読み込みと分析
- クラッシュ原因の特定 (例外、スタックトレース、メモリ破損など)
- デバッガーコマンドの実行と結果の解釈
- スレッド、モジュール、メモリ状態の調査
- トラブルシューティングやデバッグに関する一般的な質問への回答

ダンプファイルを開くか、デバッグに関する質問をどうぞ!

[プロンプト]
"C:\logs\ConsoleApplication1.exe.14596.dmp" を開いて調査結果を教えてください。原因となったデータがあれば言及してください。

[確認]
AI が以下の操作を実行しようとしています。
- ツール名: OpenDumpFile
- 引数: {"filePath":"C:\\logs\\ConsoleApplication1.exe.14596.dmp"}
許可しますか? ([Y]es / [N]o)
Y

[アシスタント]
ダンプファイルを開きました。まず基本情報と例外を確認します。

[確認]
AI が以下の操作を実行しようとしています。
- ツール名: ExecuteDebuggerCommand
- 引数: {"command":".ecxr"}
許可しますか? ([Y]es / [N]o)
Y

[確認]
AI が以下の操作を実行しようとしています。
- ツール名: ExecuteDebuggerCommand
- 引数: {"command":"!heap -s"}
許可しますか? ([Y]es / [N]o)
Y

[確認]
AI が以下の操作を実行しようとしています。
- ツール名: ExecuteDebuggerCommand
- 引数: {"command":"k"}
許可しますか? ([Y]es / [N]o)
Y

[アシスタント]
ソースコードとローカル変数を確認します。

[確認]
AI が以下の操作を実行しようとしています。
- ツール名: ExecuteDebuggerCommand
- 引数: {"command":".frame 8; dv /t /v"}
許可しますか? ([Y]es / [N]o)
Y

[アシスタント]
調査が完了しました。以下が結果です。

--- 調査結果 ---

例外: STATUS_HEAP_CORRUPTION (c0000374)
場所: ConsoleApplication1.cpp 10行目 (delete[] buffer の実行時)

原因: ヒープバッファオーバーフロー

main 関数の処理の流れ:
1. char* buffer = new char[8] -- 8バイトだけ確保
2. scanf("%s", buffer) -- 入力データをバッファに読み込み
3. printf("%s\n", buffer) -- 出力
4. delete[] buffer -- ここでクラッシュ

buffer の実際の内容:
"user1@contoso.com;user2@contoso.com;user3@contoso.com;user4@contoso.com"
(約70バイト)

8バイトのバッファに約70バイトのデータが scanf で書き込まれたため、ヒープ領域を大幅に超過して書き込みが発生しました。これによりヒープの管理メタデータが破壊され、delete[] でメモリを解放しようとした際にヒープ破損が検出されクラッシュしました。

修正案: バッファサイズを十分に確保するか、scanf の代わりに入力長を制限できる関数 (fgets, scanf_s 等) を使用してください。

ステップ 3: ツール呼び出しのフックの実装

ここまでの段階で AI からデバッガー エンジンをツールとして利用することができるようになりましたが、ツールの実行に対してさらに細かい制御を行いたい場合は、SessionConfig.Hooks を使用してツールの呼び出しをフックすることができます。例えば、AI によるツールの呼び出しを監査したい場合や、AI からツールに渡される入力のサニタイズ、ツールから AI に渡す出力に対して特定の情報をマスクしたい場合などに使用できます。このような処理はツール自体の実装に組み込むこともできますが、組み込みのツールや MCP ツールなどツールの実装に手を加えられない場合や、ツールの呼び出しに対して横断的に適用したい処理がある場合は、SessionConfig.Hooks を使用してツールの呼び出しをフックすることが有効です。

今回は、ログ出力の代わりにツール呼び出しの情報をコンソールに表示するハンドラーを SessionConfig.Hooks.OnPreToolUse に指定してみます。また、ダンプ ファイルはプロセスの状態のスナップショットとなり様々な情報が含まれますので、デバッガーの出力をそのまま AI に渡したくない場合もあるかと思います。例えばステップ 2 の例ではバッファー オーバーランしたメモリをダンプした結果、デバッガーの出力にメール アドレスが含まれていました。そこで、ツール呼び出し後のフックで出力に含まれるメール アドレスをマスクする処理を実装してみます。(RFC の準拠や様々なデバッガー コマンドの出力形式への対応などは考慮していない例示用の簡易実装となりますのでご注意ください。)

コード (SessionConfig.Hooks の例)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
sessionConfig.Hooks = new()
{
OnPreToolUse = async (input, invocation) => {
Console.ForegroundColor = ConsoleColor.DarkGray;
Console.WriteLine($"\n[ツール呼び出し] ツール名: {input.ToolName}, 引数: {input.ToolArgs}");
Console.ResetColor();
return new() { PermissionDecision = "allow" };
},
OnPostToolUse = async (input, invocation) => {
if (input.ToolName != "ExecuteDebuggerCommand")
{
return null;
}
var toolResultString = input.ToolResult?.ToString()
?? throw new Exception("ツールの実行結果が取得できませんでした。");
var toolResult = JsonSerializer.Deserialize<ToolResultObject>(toolResultString)
?? throw new InvalidOperationException("ツールの実行結果のデシリアライズに失敗しました。");

if (!toolResult.ResultType.Equals("success", StringComparison.OrdinalIgnoreCase))
{
return null;
}

bool masked = false;
var maskedToolResultString = Regex.Replace(
toolResult.TextResultForLlm,
@"[a-zA-Z0-9._%+-]+@([a-zA-Z0-9.-]+)(\.[a-zA-Z]{2,})",
m =>
{
masked = true;
return Regex.Replace(m.Value, @"[a-zA-Z0-9_%+\-]", "*");
});
if (masked)
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"\n[マスク処理] ツール出力に含まれるメール アドレスをマスクしました。");
Console.ResetColor();
toolResult.TextResultForLlm = maskedToolResultString;
return new() { ModifiedResult = toolResult };
}

return null;
},
};

その他のサポートされているフックの種類や主要なユースケースについてはドキュメント (GitHub Copilot SDK - Session Hooks) を参照してください。

動作確認

ツール呼び出しのフックの実装が完了したら、実際にアプリケーションを実行して AI との対話やツールの呼び出しが期待通りに動作するかを確認してみましょう。AI にダンプ ファイルの分析やデバッガー コマンドの実行をリクエストして、ツールが呼び出されるとコンソールにツール名や引数が表示されること、またツールの出力にメール アドレスが含まれている場合はマスク処理が呼び出されていることを確認してみてください。なお、マスク処理は簡易実装となっているため、デバッガーが実行したコマンドによっては正しくマスク処理が行われない可能性もありますので、その点はご了承ください。AI の応答例は以下のようになります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
[アシスタント]
こんにちは!WinDbgX デバッグ アシスタントです。

私ができることは以下の通りです:

- ダンプファイルや TTD トレースの分析
- クラッシュや例外の原因調査
- スタックトレースの解析と説明
- メモリやスレッドの状態確認
- デバッガー コマンドの実行と結果の解釈
- トラブルシューティングの一般的な相談

ダンプファイルを開くか、デバッグに関する質問をどうぞ!

[プロンプト]
"C:\logs\ConsoleApplication1.exe.14596.dmp" を開いて調査結果を教えてください。原因となったデータがあれば言及してください。

[ツール呼び出し] ツール名: report_intent, 引数: {"intent":"Analyzing dump file"}

[ツール呼び出し] ツール名: OpenDumpFile, 引数: {"filePath":"C:\\logs\\ConsoleApplication1.exe.14596.dmp"}

[確認]
AI が以下の操作を実行しようとしています。
- ツール名: OpenDumpFile
- 引数: {"filePath":"C:\\logs\\ConsoleApplication1.exe.14596.dmp"}
許可しますか? ([Y]es / [N]o)
Y

[アシスタント]
ダンプファイルを開きました。基本情報と例外の状態を確認します。

[ツール呼び出し] ツール名: ExecuteDebuggerCommand, 引数: {"command":".ecxr"}

[確認]
AI が以下の操作を実行しようとしています。
- ツール名: ExecuteDebuggerCommand
- 引数: {"command":".ecxr"}
許可しますか? ([Y]es / [N]o)
Y

[ツール呼び出し] ツール名: ExecuteDebuggerCommand, 引数: {"command":"k"}

[確認]
AI が以下の操作を実行しようとしています。
- ツール名: ExecuteDebuggerCommand
- 引数: {"command":"k"}
許可しますか? ([Y]es / [N]o)
Y

[アシスタント]
ソースコードとローカル変数を確認します。

[ツール呼び出し] ツール名: ExecuteDebuggerCommand, 引数: {"command":".frame 0n8;dv /t /v"}

[確認]
AI が以下の操作を実行しようとしています。
- ツール名: ExecuteDebuggerCommand
- 引数: {"command":".frame 0n8;dv /t /v"}
許可しますか? ([Y]es / [N]o)
Y

[マスク処理] ツール出力に含まれるメール アドレスをマスクしました。

[アシスタント]
調査が完了しました。以下が結果です。

------------------------------------------------------------
例外: STATUS_HEAP_CORRUPTION (0xc0000374)
場所: ConsoleApplication1!main+0x54 (ConsoleApplication1.cpp 10行目)
------------------------------------------------------------

原因: ヒープバッファオーバーフロー

コードの流れ:
1. 7行目: new char[8] で 8バイトのバッファを確保
2. 8行目: scanf でバッファにユーザー入力を読み込み
3. 9行目: printf でバッファの内容を出力
4. 10行目: delete[] でバッファを解放 → ここでクラッシュ

問題のデータ:
buffer の内容は "*****@*******.***;*****@*******.***;*****@*******.***;*****@*******.***" (71文字 + null終端 = 72バイト) でした。8バイトしか確保されていないバッファに 72バイト書き込まれたため、ヒープメモリが破壊されました。

delete[] 実行時にヒープの整合性チェックで破壊が検出され、STATUS_HEAP_CORRUPTION で異常終了しています。

対策:
- バッファサイズを十分に確保する
- scanf の代わりに scanf_s や fgets で読み込みサイズを制限する
- std::string の使用を検討する

おわりに

今回は GitHub Copilot SDK を使用したアプリケーションの例として、WinDbgX のデバッガー エンジンの操作をツールとして実装し、自然言語で対話的にダンプ ファイルの分析やデバッガー コマンドの実行を行うアプリケーションを実装してみました。本記事のソースコードはこちらからダウンロードできます。アプリケーションのデバッグやトラブルシュートのシナリオに AI を活用する方法の一例として、今回紹介した内容が皆さんの参考になれば幸いです。


本ブログの内容は弊社の公式見解として保証されるものではなく、開発・運用時の参考情報としてご活用いただくことを目的としています。もし公式な見解が必要な場合は、弊社ドキュメント (https://learn.microsoft.comhttps://support.microsoft.com) をご参照いただくか、もしくは私共サポートまでお問い合わせください。