Forge
csharpdeeb25a2
1using System.Diagnostics;
2using System.Text.Json;
3using AgentClientProtocol;
4
5// Локальный smoke: .NET-клиент ACP поднимает тот же Python echo_agent, что и samples/AcpSmoke.
6// Зависимость: Python + pip install agent-client-protocol (для echo_agent.py).
7
8var echoAgentPath = ResolveEchoAgentPath(args);
9var python = Environment.GetEnvironmentVariable("ACP_PYTHON") ?? "python";
10
11using var process = new Process
12{
13 StartInfo = new ProcessStartInfo
14 {
15 FileName = python,
16 Arguments = $"\"{echoAgentPath}\"",
17 UseShellExecute = false,
18 CreateNoWindow = true,
19 RedirectStandardOutput = true,
20 RedirectStandardInput = true,
21 RedirectStandardError = true,
22 WorkingDirectory = Path.GetDirectoryName(echoAgentPath)!,
23 },
24};
25
26process.ErrorDataReceived += (_, e) =>
27{
28 if (!string.IsNullOrEmpty(e.Data))
29 Console.Error.WriteLine($"[agent stderr] {e.Data}");
30};
31
32try
33{
34 if (!process.Start())
35 throw new InvalidOperationException("Не удалось запустить процесс агента.");
36
37 process.BeginErrorReadLine();
38
39 var client = new SmokeClient();
40 using var connection = new ClientSideConnection(_ => client, process.StandardOutput, process.StandardInput);
41 connection.Open();
42
43 var initResult = await connection.InitializeAsync(new InitializeRequest
44 {
45 ProtocolVersion = 1,
46 ClientInfo = new Implementation { Name = "cascade-acp-smoke-dotnet", Version = "0.1.0" },
47 ClientCapabilities = new ClientCapabilities
48 {
49 Fs = new FileSystemCapability { ReadTextFile = true, WriteTextFile = true },
50 },
51 });
52
53 Console.WriteLine($"initialize: protocol v{initResult.ProtocolVersion}");
54
55 var sessionRoot = Path.GetDirectoryName(echoAgentPath)!;
56 var sessionResult = await connection.NewSessionAsync(new NewSessionRequest
57 {
58 Cwd = sessionRoot,
59 McpServers = [],
60 });
61
62 Console.WriteLine($"session: {sessionResult.SessionId}");
63
64 var promptResult = await connection.PromptAsync(new PromptRequest
65 {
66 SessionId = sessionResult.SessionId,
67 Prompt = [new TextContentBlock { Text = "Hello ACP from .NET AcpSmokeDotnet" }],
68 });
69
70 Console.WriteLine($"prompt stop_reason: {promptResult.StopReason}");
71 Console.WriteLine("ACP smoke (.NET) OK");
72}
73catch (Exception ex)
74{
75 Console.Error.WriteLine($"[Client] {ex}");
76 Environment.ExitCode = 1;
77}
78finally
79{
80 try
81 {
82 if (!process.HasExited)
83 {
84 process.Kill(entireProcessTree: true);
85 process.WaitForExit(milliseconds: 5000);
86 }
87 }
88 catch
89 {
90 // ignore
91 }
92}
93
94static string ResolveEchoAgentPath(string[] args)
95{
96 if (args.Length > 0 && File.Exists(args[0]))
97 return Path.GetFullPath(args[0]);
98
99 var env = Environment.GetEnvironmentVariable("ACP_ECHO_AGENT_PATH");
100 if (!string.IsNullOrEmpty(env) && File.Exists(env))
101 return Path.GetFullPath(env);
102
103 foreach (var root in new[] { Directory.GetCurrentDirectory(), AppContext.BaseDirectory })
104 {
105 var dir = new DirectoryInfo(root);
106 while (dir != null)
107 {
108 var p = Path.Combine(dir.FullName, "samples", "AcpSmoke", "echo_agent.py");
109 if (File.Exists(p))
110 return Path.GetFullPath(p);
111 dir = dir.Parent;
112 }
113 }
114
115 throw new FileNotFoundException(
116 "Не найден echo_agent.py. Укажи путь первым аргументом или установи ACP_ECHO_AGENT_PATH.");
117}
118
119/// <summary>Минимальная реализация клиента: эхо в консоль, права — отмена (как в Python smoke).</summary>
120internal sealed class SmokeClient : IAcpClient
121{
122 public ValueTask<RequestPermissionResponse> RequestPermissionAsync(
123 RequestPermissionRequest request,
124 CancellationToken cancellationToken = default)
125 {
126 return ValueTask.FromResult(new RequestPermissionResponse
127 {
128 Outcome = new CancelledRequestPermissionOutcome(),
129 });
130 }
131
132 public ValueTask SessionNotificationAsync(SessionNotification notification, CancellationToken cancellationToken = default)
133 {
134 var update = notification.Update;
135 if (update is AgentMessageChunkSessionUpdate chunk && chunk.Content is TextContentBlock text)
136 Console.WriteLine($"session_update: {text.Text}");
137 return ValueTask.CompletedTask;
138 }
139
140 public ValueTask<WriteTextFileResponse> WriteTextFileAsync(WriteTextFileRequest request, CancellationToken cancellationToken = default)
141 => ValueTask.FromResult(new WriteTextFileResponse());
142
143 public ValueTask<ReadTextFileResponse> ReadTextFileAsync(ReadTextFileRequest request, CancellationToken cancellationToken = default)
144 => ValueTask.FromResult(new ReadTextFileResponse { Content = "" });
145
146 public ValueTask<CreateTerminalResponse> CreateTerminalAsync(CreateTerminalRequest request, CancellationToken cancellationToken = default)
147 => throw new NotSupportedException();
148
149 public ValueTask<TerminalOutputResponse> TerminalOutputAsync(TerminalOutputRequest request, CancellationToken cancellationToken = default)
150 => throw new NotSupportedException();
151
152 public ValueTask<ReleaseTerminalResponse> ReleaseTerminalAsync(ReleaseTerminalRequest request, CancellationToken cancellationToken = default)
153 => throw new NotSupportedException();
154
155 public ValueTask<WaitForTerminalExitResponse> WaitForTerminalExitAsync(WaitForTerminalExitRequest request, CancellationToken cancellationToken = default)
156 => throw new NotSupportedException();
157
158 public ValueTask<KillTerminalCommandResponse> KillTerminalCommandAsync(KillTerminalCommandRequest request, CancellationToken cancellationToken = default)
159 => throw new NotSupportedException();
160
161 public ValueTask<JsonElement> ExtMethodAsync(string method, JsonElement request, CancellationToken cancellationToken = default)
162 => throw new NotSupportedException();
163
164 public ValueTask ExtNotificationAsync(string method, JsonElement notification, CancellationToken cancellationToken = default)
165 => ValueTask.CompletedTask;
166}
167
View only · write via MCP/CIDE