1import * as Schema from "effect/Schema";
2import type * as SchemaIssue from "effect/SchemaIssue";
3
4import * as AcpSchema from "./_generated/schema.gen.ts";
5
6const AcpRequestOperation = Schema.Literals([
7 "decode-extension-request-payload",
8 "encode-extension-response",
9 "handle-request",
10 "handle-extension-request",
11 "receive-response",
12 "receive-streaming-response",
13]);
14type AcpRequestOperation = typeof AcpRequestOperation.Type;
15
16export const AcpRequestId = Schema.Union([Schema.String, Schema.Number]);
17export type AcpRequestId = typeof AcpRequestId.Type;
18
19const AcpSchemaIssueKind = Schema.Literals([
20 "Filter",
21 "Encoding",
22 "Pointer",
23 "Composite",
24 "AnyOf",
25 "InvalidType",
26 "InvalidValue",
27 "MissingKey",
28 "UnexpectedKey",
29 "Forbidden",
30 "OneOf",
31]);
32type AcpSchemaIssueKind = typeof AcpSchemaIssueKind.Type;
33
34interface AcpSchemaIssueDiagnostics {
35 readonly issueCount: number;
36 readonly issueKinds: ReadonlyArray<AcpSchemaIssueKind>;
37 readonly maximumPathDepth: number;
38}
39
40const schemaIssueDiagnostics = (root: SchemaIssue.Issue): AcpSchemaIssueDiagnostics => {
41 let issueCount = 0;
42 let maximumPathDepth = 0;
43 const issueKinds = new Set<AcpSchemaIssueKind>();
44
45 const visit = (issue: SchemaIssue.Issue, pathDepth: number): void => {
46 issueCount += 1;
47 issueKinds.add(issue._tag);
48 maximumPathDepth = Math.max(maximumPathDepth, pathDepth);
49 switch (issue._tag) {
50 case "Filter":
51 case "Encoding":
52 visit(issue.issue, pathDepth);
53 break;
54 case "Pointer":
55 visit(issue.issue, pathDepth + issue.path.length);
56 break;
57 case "Composite":
58 case "AnyOf":
59 for (const child of issue.issues) visit(child, pathDepth);
60 break;
61 }
62 };
63
64 visit(root, 0);
65 return {
66 issueCount,
67 issueKinds: [...issueKinds],
68 maximumPathDepth,
69 };
70};
71
72export interface AcpRequestDiagnostics {
73 readonly method?: string;
74 readonly requestId?: AcpRequestId;
75 readonly operation?: AcpRequestOperation;
76 readonly cause?: unknown;
77 readonly issueCount?: number;
78 readonly issueKinds?: ReadonlyArray<AcpSchemaIssueKind>;
79 readonly maximumPathDepth?: number;
80}
81
82export class AcpSpawnError extends Schema.TaggedError<AcpSpawnError>()("AcpSpawnError", {
83 command: Schema.optional(Schema.String),
84 cause: Schema.Defect(),
85}) {
86 override get message() {
87 return this.command
88 ? `Failed to spawn ACP process for command: ${this.command}`
89 : "Failed to spawn ACP process";
90 }
91}
92
93export class AcpProcessExitedError extends Schema.TaggedError<AcpProcessExitedError>()(
94 "AcpProcessExitedError",
95 {
96 code: Schema.optional(Schema.Number),
97 pid: Schema.optionalKey(Schema.Int),
98 stderr: Schema.optionalKey(Schema.String),
99 cause: Schema.optional(Schema.Defect()),
100 },
101) {
102 override get message() {
103 const base =
104 this.code === undefined ? "ACP process exited" : `ACP process exited with code ${this.code}`;
105 const excerpt = this.stderr?.trim();
106 return excerpt && excerpt.length > 0 ? `${base}\n${excerpt}` : base;
107 }
108}
109
110export const AcpProtocolParseOperation = Schema.Literals([
111 "encode-message",
112 "decode-wire-message",
113 "decode-notification-payload",
114]);
115export type AcpProtocolParseOperation = typeof AcpProtocolParseOperation.Type;
116
117export class AcpProtocolParseError extends Schema.TaggedError<AcpProtocolParseError>()(
118 "AcpProtocolParseError",
119 {
120 operation: AcpProtocolParseOperation,
121 method: Schema.optionalKey(Schema.String),
122 requestId: Schema.optionalKey(AcpRequestId),
123 issueCount: Schema.optionalKey(Schema.Number),
124 issueKinds: Schema.optionalKey(Schema.Array(AcpSchemaIssueKind)),
125 maximumPathDepth: Schema.optionalKey(Schema.Number),
126 cause: Schema.Defect(),
127 },
128) {
129 override get message() {
130 const method = this.method === undefined ? "" : ` for method '${this.method}'`;
131 return `ACP protocol operation '${this.operation}' failed${method}.`;
132 }
133
134 static fromSchemaError(
135 operation: AcpProtocolParseOperation,
136 method: string,
137 cause: Schema.SchemaError,
138 ) {
139 return new AcpProtocolParseError({
140 operation,
141 method,
142 ...schemaIssueDiagnostics(cause.issue),
143 cause,
144 });
145 }
146
147 static fromEncodingError(
148 method: string | undefined,
149 requestId: AcpRequestId | undefined,
150 cause: unknown,
151 ) {
152 return new AcpProtocolParseError({
153 operation: "encode-message",
154 ...(method === undefined ? {} : { method }),
155 ...(requestId === undefined ? {} : { requestId }),
156 cause,
157 });
158 }
159}
160
161export class AcpTransportError extends Schema.TaggedError<AcpTransportError>()(
162 "AcpTransportError",
163 {
164 operation: Schema.optional(
165 Schema.Literals(["call-rpc", "read-input-stream", "read-process-exit-status"]),
166 ),
167 method: Schema.optional(Schema.String),
168 detail: Schema.optional(Schema.String),
169 pid: Schema.optionalKey(Schema.Int),
170 cause: Schema.Defect(),
171 },
172) {
173 override get message() {
174 const method = this.method ? ` for method ${this.method}` : "";
175 return this.operation
176 ? `ACP transport operation ${this.operation} failed${method}.`
177 : "ACP transport operation failed.";
178 }
179}
180
181export class AcpInputStreamEndedError extends Schema.TaggedError<AcpInputStreamEndedError>()(
182 "AcpInputStreamEndedError",
183 {},
184) {
185 override get message() {
186 return "ACP input stream ended.";
187 }
188}
189
190export class AcpRequestError extends Schema.TaggedError<AcpRequestError>()("AcpRequestError", {
191 code: AcpSchema.ErrorCode,
192 errorMessage: Schema.String,
193 data: Schema.optional(Schema.Unknown),
194 method: Schema.optionalKey(Schema.String),
195 requestId: Schema.optionalKey(AcpRequestId),
196 operation: Schema.optionalKey(AcpRequestOperation),
197 issueCount: Schema.optionalKey(Schema.Number),
198 issueKinds: Schema.optionalKey(Schema.Array(AcpSchemaIssueKind)),
199 maximumPathDepth: Schema.optionalKey(Schema.Number),
200 cause: Schema.optionalKey(Schema.Defect()),
201}) {
202 override get message() {
203 return this.errorMessage;
204 }
205
206 static fromProtocolError(
207 error: AcpSchema.Error,
208 context: {
209 readonly method: string;
210 readonly requestId?: AcpRequestId;
211 readonly cause?: unknown;
212 },
213 ) {
214 return new AcpRequestError({
215 code: error.code,
216 errorMessage: error.message,
217 ...(error.data !== undefined ? { data: error.data } : {}),
218 method: context.method,
219 ...(context.requestId === undefined ? {} : { requestId: context.requestId }),
220 operation: "receive-response",
221 cause: context.cause ?? error,
222 });
223 }
224
225 static fromExtensionResponseFailure(method: string, requestId: AcpRequestId, cause: unknown) {
226 return AcpRequestError.internalError("Extension request failed", undefined, {
227 method,
228 requestId,
229 operation: "receive-response",
230 cause,
231 });
232 }
233
234 static fromExtensionResponseEncodingError(
235 method: string,
236 requestId: AcpRequestId,
237 cause: AcpProtocolParseError,
238 ) {
239 return AcpRequestError.internalError("Internal error", undefined, {
240 method,
241 requestId,
242 operation: "encode-extension-response",
243 cause,
244 });
245 }
246
247 static unsupportedStreamingResponse(method: string, requestId: AcpRequestId) {
248 return AcpRequestError.internalError(
249 "Streaming extension responses are not supported",
250 undefined,
251 {
252 method,
253 requestId,
254 operation: "receive-streaming-response",
255 },
256 );
257 }
258
259 static fromCoreHandlerError(error: AcpError, method: string) {
260 if (error._tag === "AcpRequestError") {
261 return error;
262 }
263 return AcpRequestError.internalError(
264 `ACP request handler failed for method '${method}'`,
265 undefined,
266 {
267 method,
268 operation: "handle-request",
269 cause: error,
270 },
271 );
272 }
273
274 static fromExtensionHandlerError(error: AcpError, method: string) {
275 if (error._tag === "AcpRequestError") {
276 return error;
277 }
278 return AcpRequestError.internalError(
279 `ACP extension request handler failed for method '${method}'`,
280 undefined,
281 {
282 method,
283 operation: "handle-extension-request",
284 cause: error,
285 },
286 );
287 }
288
289 static parseError(message = "Parse error", data?: unknown) {
290 return new AcpRequestError({
291 code: -32700,
292 errorMessage: message,
293 ...(data !== undefined ? { data } : {}),
294 });
295 }
296
297 static invalidRequest(message = "Invalid request", data?: unknown) {
298 return new AcpRequestError({
299 code: -32600,
300 errorMessage: message,
301 ...(data !== undefined ? { data } : {}),
302 });
303 }
304
305 static methodNotFound(method: string) {
306 return new AcpRequestError({
307 code: -32601,
308 errorMessage: `Method not found: ${method}`,
309 });
310 }
311
312 static invalidParams(message = "Invalid params", data?: unknown) {
313 return new AcpRequestError({
314 code: -32602,
315 errorMessage: message,
316 ...(data !== undefined ? { data } : {}),
317 });
318 }
319
320 static invalidExtensionPayload(method: string, cause: Schema.SchemaError) {
321 const diagnostics = schemaIssueDiagnostics(cause.issue);
322 return new AcpRequestError({
323 code: -32602,
324 errorMessage: `Invalid payload for ACP extension method '${method}'.`,
325 data: diagnostics,
326 method,
327 operation: "decode-extension-request-payload",
328 ...diagnostics,
329 cause,
330 });
331 }
332
333 static internalError(
334 message = "Internal error",
335 data?: unknown,
336 diagnostics: AcpRequestDiagnostics = {},
337 ) {
338 return new AcpRequestError({
339 code: -32603,
340 errorMessage: message,
341 ...(data !== undefined ? { data } : {}),
342 ...diagnostics,
343 });
344 }
345
346 static authRequired(message = "Authentication required", data?: unknown) {
347 return new AcpRequestError({
348 code: -32000,
349 errorMessage: message,
350 ...(data !== undefined ? { data } : {}),
351 });
352 }
353
354 static resourceNotFound(message = "Resource not found", data?: unknown) {
355 return new AcpRequestError({
356 code: -32002,
357 errorMessage: message,
358 ...(data !== undefined ? { data } : {}),
359 });
360 }
361
362 toProtocolError() {
363 return AcpSchema.Error.make({
364 code: this.code,
365 message: this.errorMessage,
366 ...(this.data !== undefined ? { data: this.data } : {}),
367 });
368 }
369}
370
371export const AcpError = Schema.Union([
372 AcpRequestError,
373 AcpSpawnError,
374 AcpProcessExitedError,
375 AcpProtocolParseError,
376 AcpTransportError,
377 AcpInputStreamEndedError,
378]);
379
380export type AcpError = typeof AcpError.Type;