Forge
csharp4405de34
1using System.Text.Json;
2
3namespace AgentClientProtocol;
4
5public sealed class ClientSideConnection : IDisposable, IAcpAgent
6{
7 readonly IAcpClient client;
8
9 readonly CancellationTokenSource cts = new();
10 readonly JsonRpcEndpoint endpoint;
11
12 public ClientSideConnection(Func<IAcpAgent, IAcpClient> toClient, TextReader reader, TextWriter writer)
13 {
14 client = toClient(this);
15
16 endpoint = new(
17 _ => new(reader.ReadLine()),
18 (s, _) =>
19 {
20 writer.WriteLine(s);
21 return default;
22 },
23 (s, _) => default
24 );
25
26 endpoint.SetRequestHandler(ClientMethods.FsReadTextFile, async (request, ct) =>
27 {
28 AcpException.ThrowIfParamIsNull(request.Params);
29
30 var response = await client.ReadTextFileAsync(JsonSerializer.Deserialize(
31 request.Params!.Value,
32 AcpJsonSerializerContext.Default.Options.GetTypeInfo<ReadTextFileRequest>())!, ct);
33
34 return new JsonRpcResponse
35 {
36 Id = request.Id,
37 Result = JsonSerializer.SerializeToElement(response, AcpJsonSerializerContext.Default.Options.GetTypeInfo<ReadTextFileResponse>())
38 };
39 });
40
41 endpoint.SetRequestHandler(ClientMethods.FsWriteTextFile, async (request, ct) =>
42 {
43 AcpException.ThrowIfParamIsNull(request.Params);
44
45 var response = await client.WriteTextFileAsync(JsonSerializer.Deserialize(
46 request.Params!.Value,
47 AcpJsonSerializerContext.Default.Options.GetTypeInfo<WriteTextFileRequest>())!, ct);
48
49 return new JsonRpcResponse
50 {
51 Id = request.Id,
52 Result = JsonSerializer.SerializeToElement(response, AcpJsonSerializerContext.Default.Options.GetTypeInfo<WriteTextFileResponse>())
53 };
54 });
55
56 endpoint.SetRequestHandler(ClientMethods.SessionRequestPermission, async (request, ct) =>
57 {
58 AcpException.ThrowIfParamIsNull(request.Params);
59
60 var response = await client.RequestPermissionAsync(JsonSerializer.Deserialize(
61 request.Params!.Value,
62 AcpJsonSerializerContext.Default.Options.GetTypeInfo<RequestPermissionRequest>())!, ct);
63
64 return new JsonRpcResponse
65 {
66 Id = request.Id,
67 Result = JsonSerializer.SerializeToElement(response, AcpJsonSerializerContext.Default.Options.GetTypeInfo<RequestPermissionResponse>())
68 };
69 });
70
71 endpoint.SetRequestHandler(ClientMethods.TerminalCreate, async (request, ct) =>
72 {
73 AcpException.ThrowIfParamIsNull(request.Params);
74
75 var response = await client.CreateTerminalAsync(JsonSerializer.Deserialize(
76 request.Params!.Value,
77 AcpJsonSerializerContext.Default.Options.GetTypeInfo<CreateTerminalRequest>())!, ct);
78
79 return new JsonRpcResponse
80 {
81 Id = request.Id,
82 Result = JsonSerializer.SerializeToElement(response, AcpJsonSerializerContext.Default.Options.GetTypeInfo<CreateTerminalResponse>())
83 };
84 });
85
86 endpoint.SetRequestHandler(ClientMethods.TerminalKill, async (request, ct) =>
87 {
88 AcpException.ThrowIfParamIsNull(request.Params);
89
90 var response = await client.KillTerminalCommandAsync(JsonSerializer.Deserialize(
91 request.Params!.Value,
92 AcpJsonSerializerContext.Default.Options.GetTypeInfo<KillTerminalCommandRequest>())!, ct);
93
94 return new JsonRpcResponse
95 {
96 Id = request.Id,
97 Result = JsonSerializer.SerializeToElement(response, AcpJsonSerializerContext.Default.Options.GetTypeInfo<KillTerminalCommandResponse>())
98 };
99 });
100
101 endpoint.SetRequestHandler(ClientMethods.TerminalOutput, async (request, ct) =>
102 {
103 AcpException.ThrowIfParamIsNull(request.Params);
104
105 var response = await client.TerminalOutputAsync(JsonSerializer.Deserialize(
106 request.Params!.Value,
107 AcpJsonSerializerContext.Default.Options.GetTypeInfo<TerminalOutputRequest>())!, ct);
108
109 return new JsonRpcResponse
110 {
111 Id = request.Id,
112 Result = JsonSerializer.SerializeToElement(response, AcpJsonSerializerContext.Default.Options.GetTypeInfo<TerminalOutputResponse>())
113 };
114 });
115
116 endpoint.SetRequestHandler(ClientMethods.TerminalRelease, async (request, ct) =>
117 {
118 AcpException.ThrowIfParamIsNull(request.Params);
119
120 var response = await client.ReleaseTerminalAsync(JsonSerializer.Deserialize(
121 request.Params!.Value,
122 AcpJsonSerializerContext.Default.Options.GetTypeInfo<ReleaseTerminalRequest>())!, ct);
123
124 return new JsonRpcResponse
125 {
126 Id = request.Id,
127 Result = JsonSerializer.SerializeToElement(response, AcpJsonSerializerContext.Default.Options.GetTypeInfo<ReleaseTerminalResponse>())
128 };
129 });
130
131 endpoint.SetRequestHandler(ClientMethods.TerminalWaitForExit, async (request, ct) =>
132 {
133 AcpException.ThrowIfParamIsNull(request.Params);
134
135 var response = await client.WaitForTerminalExitAsync(JsonSerializer.Deserialize(
136 request.Params!.Value,
137 AcpJsonSerializerContext.Default.Options.GetTypeInfo<WaitForTerminalExitRequest>())!, ct);
138
139 return new JsonRpcResponse
140 {
141 Id = request.Id,
142 Result = JsonSerializer.SerializeToElement(response, AcpJsonSerializerContext.Default.Options.GetTypeInfo<WaitForTerminalExitResponse>())
143 };
144 });
145
146 endpoint.SetNotificationHandler(ClientMethods.SessionUpdate, async (notification, ct) =>
147 {
148 AcpException.ThrowIfParamIsNull(notification.Params);
149
150 var sessionNotification = JsonSerializer.Deserialize(
151 notification.Params!.Value,
152 AcpJsonSerializerContext.Default.Options.GetTypeInfo<SessionNotification>())!;
153
154 await client.SessionNotificationAsync(sessionNotification, ct);
155 });
156
157 endpoint.SetDefaultRequestHandler(async (request, ct) =>
158 {
159 var response = await client.ExtMethodAsync(request.Method, request.Params ?? default, ct);
160
161 return new JsonRpcResponse
162 {
163 Id = request.Id,
164 Result = response
165 };
166 });
167
168 endpoint.SetDefaultNotificationHandler(async (notification, ct) =>
169 {
170 await client.ExtNotificationAsync(notification.Method, notification.Params ?? default, ct);
171 });
172 }
173
174 async ValueTask<TResponse> RequestAsync<TRequest, TResponse>(string method, TRequest request, CancellationToken cancellationToken)
175 {
176 var response = await endpoint.SendRequestAsync(new JsonRpcRequest
177 {
178 Method = method,
179 Id = default,
180 Params = JsonSerializer.SerializeToElement(request, AcpJsonSerializerContext.Default.Options.GetTypeInfo<TRequest>())
181 }, cancellationToken);
182
183 if (response.Error != null)
184 {
185 throw new AcpException($"{response.Error!.Message}", response.Error.Data, response.Error.Code);
186 }
187
188 // HACK:
189 // In a specific version of Gemini-CLI, the `authenticate` method returns a response (`result: null`)
190 // that differs from the expected schema. To accommodate this, we are ignoring the null check.
191 // Since `result` should not be null in any other case, this should generally not be a problem.
192 if (response.Result == null)
193 {
194 return default!;
195 }
196
197 return JsonSerializer.Deserialize(response.Result.Value, AcpJsonSerializerContext.Default.Options.GetTypeInfo<TResponse>())!;
198 }
199
200
201 async ValueTask NotificationAsync<TNotification>(string method, TNotification notification, CancellationToken cancellationToken)
202 {
203 await endpoint.SendMessageAsync(new JsonRpcNotification
204 {
205 Method = method,
206 Params = JsonSerializer.SerializeToElement(notification, AcpJsonSerializerContext.Default.Options.GetTypeInfo<TNotification>())
207 }, cancellationToken);
208 }
209
210 public ValueTask<InitializeResponse> InitializeAsync(InitializeRequest request, CancellationToken cancellationToken = default)
211 {
212 return RequestAsync<InitializeRequest, InitializeResponse>(AgentMethods.Initialize, request, cancellationToken);
213 }
214
215 public ValueTask<AuthenticateResponse> AuthenticateAsync(AuthenticateRequest request, CancellationToken cancellationToken = default)
216 {
217 return RequestAsync<AuthenticateRequest, AuthenticateResponse>(AgentMethods.Authenticate, request, cancellationToken);
218 }
219
220 public ValueTask<NewSessionResponse> NewSessionAsync(NewSessionRequest request, CancellationToken cancellationToken = default)
221 {
222 return RequestAsync<NewSessionRequest, NewSessionResponse>(AgentMethods.SessionNew, request, cancellationToken);
223 }
224
225 public ValueTask<PromptResponse> PromptAsync(PromptRequest request, CancellationToken cancellationToken = default)
226 {
227 return RequestAsync<PromptRequest, PromptResponse>(AgentMethods.SessionPrompt, request, cancellationToken);
228 }
229
230 public ValueTask CancelAsync(CancelNotification notification, CancellationToken cancellationToken = default)
231 {
232 return NotificationAsync(AgentMethods.SessionCancel, notification, cancellationToken);
233 }
234
235 public ValueTask<LoadSessionResponse> LoadSessionAsync(LoadSessionRequest request, CancellationToken cancellationToken = default)
236 {
237 return RequestAsync<LoadSessionRequest, LoadSessionResponse>(AgentMethods.SessionLoad, request, cancellationToken);
238 }
239
240 public ValueTask<SetSessionModeResponse> SetSessionModeAsync(SetSessionModeRequest request, CancellationToken cancellationToken = default)
241 {
242 return RequestAsync<SetSessionModeRequest, SetSessionModeResponse>(AgentMethods.SessionSetMode, request, cancellationToken);
243 }
244
245 public ValueTask<SetSessionModelResponse> SetSessionModelAsync(SetSessionModelRequest request, CancellationToken cancellationToken = default)
246 {
247 return RequestAsync<SetSessionModelRequest, SetSessionModelResponse>(AgentMethods.SessionSetModel, request, cancellationToken);
248 }
249
250 public async ValueTask<JsonElement> ExtMethodAsync(string method, JsonElement request, CancellationToken cancellationToken = default)
251 {
252 var response = await endpoint.SendRequestAsync(new JsonRpcRequest
253 {
254 Method = method,
255 Id = default,
256 Params = request,
257 }, cancellationToken);
258
259 if (response.Result == null)
260 {
261 throw new AcpException($"{response.Error!.Message}", response.Error.Data, response.Error.Code);
262 }
263
264 return response.Result.Value;
265 }
266
267 public ValueTask ExtNotificationAsync(string method, JsonElement notification, CancellationToken cancellationToken = default)
268 {
269 // writer.WriteLineAsync(notification.ToString());
270 return default;
271 }
272
273 public void Dispose()
274 {
275 cts.Cancel();
276 cts.Dispose();
277 }
278
279 public void Open()
280 {
281 Task.Run(async () => await endpoint.ReadMessagesAsync(cts.Token));
282 }
283}
View only · write via MCP/CIDE