Skip to content
Source
Sign inSign up

errors.ts

454 lines13.0 KB

1import * as Schema from "effect/Schema";
2import * as SchemaAST from "effect/SchemaAST";
3import type * as SchemaIssue from "effect/SchemaIssue";
4​
5import * as AcpSchema from "./_generated/schema.gen.ts";
6​
7const AcpRequestOperation = Schema.Literals([
8 "decode-extension-request-payload",
9 "encode-extension-response",
10 "handle-request",
11 "handle-extension-request",
12 "receive-response",
13 "receive-streaming-response",
14]);
15type AcpRequestOperation = typeof AcpRequestOperation.Type;
16​
17export const AcpRequestId = Schema.Union([Schema.String, Schema.Number]);
18export type AcpRequestId = typeof AcpRequestId.Type;
19​
20const AcpSchemaIssueKind = Schema.Literals([
21 "Filter",
22 "Encoding",
23 "Pointer",
24 "Composite",
25 "AnyOf",
26 "InvalidType",
27 "InvalidValue",
28 "MissingKey",
29 "UnexpectedKey",
30 "Forbidden",
31 "OneOf",
32]);
33type AcpSchemaIssueKind = typeof AcpSchemaIssueKind.Type;
34​
35interface AcpSchemaIssueDiagnostics {
36 readonly issueCount: number;
37 readonly issueKinds: ReadonlyArray<AcpSchemaIssueKind>;
38 readonly maximumPathDepth: number;
40​
41const MAX_SUMMARY_ISSUES = 3;
42const MAX_SUMMARY_PATH_SEGMENTS = 8;
43​
44const summaryPathSegment = (segment: PropertyKey): string =>
45 typeof segment === "number" || /^[A-Za-z_][A-Za-z0-9_]{0,63}$/.test(String(segment))
46 ? String(segment)
47 : "*";
48​
49const summaryPath = (path: ReadonlyArray<PropertyKey>): string => {
50 if (path.length === 0) return "payload";
51 const shown = path.slice(0, MAX_SUMMARY_PATH_SEGMENTS).map(summaryPathSegment).join(".");
52 return path.length > MAX_SUMMARY_PATH_SEGMENTS ? `${shown}…` : shown;
53};
54​
55const summaryReason = (issue: SchemaIssue.Issue): string => {
56 switch (issue._tag) {
57 case "MissingKey":
58 return "missing";
59 case "UnexpectedKey":
60 return "unexpected";
61 case "InvalidType":
62 return "wrong type";
63 default:
64 return "invalid";
65 }
66};
67​
68const isCatchAllMember = (issue: SchemaIssue.Issue) =>
69 "ast" in issue && SchemaAST.resolveTitle(issue.ast) === "other";
70​
71/**
72 * ACP unions end in a member titled "other" that admits custom and future
73 * variants; it fails only because the discriminator names a known variant, so
74 * the known variant's issues are the ones reported.
75 */
76const schemaIssueSummary = (root: SchemaIssue.Issue): string => {
77 const entries = new Set<string>();
78 const visit = (issue: SchemaIssue.Issue, path: ReadonlyArray<PropertyKey>): void => {
79 switch (issue._tag) {
80 case "Filter":
81 case "Encoding":
82 visit(issue.issue, path);
83 return;
84 case "Pointer":
85 visit(issue.issue, [...path, ...issue.path]);
86 return;
87 case "Composite":
88 case "AnyOf": {
89 const known = issue.issues.filter((child) => !isCatchAllMember(child));
90 const children = known.length > 0 ? known : issue.issues;
91 if (children.length === 0) entries.add(`${summaryPath(path)} ${summaryReason(issue)}`);
92 for (const child of children) visit(child, path);
93 return;
94 }
95 default:
96 entries.add(`${summaryPath(path)} ${summaryReason(issue)}`);
97 }
98 };
99 visit(root, []);
100 const shown = [...entries].slice(0, MAX_SUMMARY_ISSUES);
101 const hidden = entries.size - shown.length;
102 return hidden > 0 ? `${shown.join(", ")} (+${hidden} more)` : shown.join(", ");
103};
104​
105const schemaIssueDiagnostics = (root: SchemaIssue.Issue): AcpSchemaIssueDiagnostics => {
106 let issueCount = 0;
107 let maximumPathDepth = 0;
108 const issueKinds = new Set<AcpSchemaIssueKind>();
109​
110 const visit = (issue: SchemaIssue.Issue, pathDepth: number): void => {
111 issueCount += 1;
112 issueKinds.add(issue._tag);
113 maximumPathDepth = Math.max(maximumPathDepth, pathDepth);
114 switch (issue._tag) {
115 case "Filter":
116 case "Encoding":
117 visit(issue.issue, pathDepth);
118 break;
119 case "Pointer":
120 visit(issue.issue, pathDepth + issue.path.length);
121 break;
122 case "Composite":
123 case "AnyOf":
124 for (const child of issue.issues) visit(child, pathDepth);
125 break;
126 }
127 };
128​
129 visit(root, 0);
130 return {
131 issueCount,
132 issueKinds: [...issueKinds],
133 maximumPathDepth,
134 };
135};
136​
137export interface AcpRequestDiagnostics {
138 readonly method?: string;
139 readonly requestId?: AcpRequestId;
140 readonly operation?: AcpRequestOperation;
141 readonly cause?: unknown;
142 readonly issueCount?: number;
143 readonly issueKinds?: ReadonlyArray<AcpSchemaIssueKind>;
144 readonly maximumPathDepth?: number;
146​
147export class AcpSpawnError extends Schema.TaggedError<AcpSpawnError>()("AcpSpawnError", {
148 command: Schema.optional(Schema.String),
149 cause: Schema.Defect(),
150}) {
151 override get message() {
152 return this.command
153 ? `Failed to spawn ACP process for command: ${this.command}`
154 : "Failed to spawn ACP process";
155 }
157​
158export class AcpProcessExitedError extends Schema.TaggedError<AcpProcessExitedError>()(
159 "AcpProcessExitedError",
160 {
161 code: Schema.optional(Schema.Number),
162 signal: Schema.optionalKey(Schema.String),
163 pid: Schema.optionalKey(Schema.Int),
164 stderr: Schema.optionalKey(Schema.String),
165 cause: Schema.optional(Schema.Defect()),
166 },
167) {
168 override get message() {
169 const base =
170 this.signal !== undefined
171 ? `ACP process exited after signal ${this.signal}`
172 : this.code === undefined
173 ? "ACP process exited"
174 : `ACP process exited with code ${this.code}`;
175 const excerpt = this.stderr?.trim();
176 return excerpt && excerpt.length > 0 ? `${base}\n${excerpt}` : base;
177 }
179​
180export const AcpProtocolParseOperation = Schema.Literals([
181 "encode-message",
182 "decode-wire-message",
183 "decode-notification-payload",
184]);
185export type AcpProtocolParseOperation = typeof AcpProtocolParseOperation.Type;
186​
187export class AcpProtocolParseError extends Schema.TaggedError<AcpProtocolParseError>()(
188 "AcpProtocolParseError",
189 {
190 operation: AcpProtocolParseOperation,
191 method: Schema.optionalKey(Schema.String),
192 requestId: Schema.optionalKey(AcpRequestId),
193 issueCount: Schema.optionalKey(Schema.Number),
194 issueKinds: Schema.optionalKey(Schema.Array(AcpSchemaIssueKind)),
195 maximumPathDepth: Schema.optionalKey(Schema.Number),
196 issueSummary: Schema.optionalKey(Schema.String),
197 cause: Schema.Defect(),
198 },
199) {
200 override get message() {
201 const method = this.method === undefined ? "" : ` for method '${this.method}'`;
202 const summary = this.issueSummary === undefined ? "" : `: ${this.issueSummary}`;
203 return `ACP protocol operation '${this.operation}' failed${method}${summary}.`;
204 }
205​
206 static fromSchemaError(
207 operation: AcpProtocolParseOperation,
208 method: string,
209 cause: Schema.SchemaError,
210 ) {
211 return new AcpProtocolParseError({
212 operation,
213 method,
214 ...schemaIssueDiagnostics(cause.issue),
215 issueSummary: schemaIssueSummary(cause.issue),
216 cause,
217 });
218 }
219​
220 static fromEncodingError(
221 method: string | undefined,
222 requestId: AcpRequestId | undefined,
223 cause: unknown,
224 ) {
225 return new AcpProtocolParseError({
226 operation: "encode-message",
227 ...(method === undefined ? {} : { method }),
228 ...(requestId === undefined ? {} : { requestId }),
229 cause,
230 });
231 }
233​
234export class AcpTransportError extends Schema.TaggedError<AcpTransportError>()(
235 "AcpTransportError",
236 {
237 operation: Schema.optional(
238 Schema.Literals(["call-rpc", "read-input-stream", "read-process-exit-status"]),
239 ),
240 method: Schema.optional(Schema.String),
241 detail: Schema.optional(Schema.String),
242 pid: Schema.optionalKey(Schema.Int),
243 cause: Schema.Defect(),
244 },
245) {
246 override get message() {
247 const operation = this.operation ? ` ${this.operation}` : "";
248 const method = this.method ? ` for method ${this.method}` : "";
249 // Callers often pass a sentence; strip its period so the message ends with exactly one.
250 const detail = this.detail ? `: ${this.detail.replace(/\.$/, "")}` : "";
251 return `ACP transport operation${operation} failed${method}${detail}.`;
252 }
254​
255export class AcpInputStreamEndedError extends Schema.TaggedError<AcpInputStreamEndedError>()(
256 "AcpInputStreamEndedError",
257 {},
258) {
259 override get message() {
260 return "ACP input stream ended.";
261 }
263​
264export class AcpRequestError extends Schema.TaggedError<AcpRequestError>()("AcpRequestError", {
265 code: AcpSchema.ErrorCode,
266 errorMessage: Schema.String,
267 data: Schema.optional(Schema.Unknown),
268 method: Schema.optionalKey(Schema.String),
269 requestId: Schema.optionalKey(AcpRequestId),
270 operation: Schema.optionalKey(AcpRequestOperation),
271 issueCount: Schema.optionalKey(Schema.Number),
272 issueKinds: Schema.optionalKey(Schema.Array(AcpSchemaIssueKind)),
273 maximumPathDepth: Schema.optionalKey(Schema.Number),
274 cause: Schema.optionalKey(Schema.Defect()),
275}) {
276 override get message() {
277 return this.errorMessage;
278 }
279​
280 static fromProtocolError(
281 error: AcpSchema.Error,
282 context: {
283 readonly method: string;
284 readonly requestId?: AcpRequestId;
285 readonly cause?: unknown;
286 },
287 ) {
288 return new AcpRequestError({
289 code: error.code,
290 errorMessage: error.message,
291 ...(error.data !== undefined ? { data: error.data } : {}),
292 method: context.method,
293 ...(context.requestId === undefined ? {} : { requestId: context.requestId }),
294 operation: "receive-response",
295 cause: context.cause ?? error,
296 });
297 }
298​
299 static fromExtensionResponseFailure(method: string, requestId: AcpRequestId, cause: unknown) {
300 return AcpRequestError.internalError("Extension request failed", undefined, {
301 method,
302 requestId,
303 operation: "receive-response",
304 cause,
305 });
306 }
307​
308 static fromExtensionResponseEncodingError(
309 method: string,
310 requestId: AcpRequestId,
311 cause: AcpProtocolParseError,
312 ) {
313 return AcpRequestError.internalError("Internal error", undefined, {
314 method,
315 requestId,
316 operation: "encode-extension-response",
317 cause,
318 });
319 }
320​
321 static unsupportedStreamingResponse(method: string, requestId: AcpRequestId) {
322 return AcpRequestError.internalError(
323 "Streaming extension responses are not supported",
324 undefined,
325 {
326 method,
327 requestId,
328 operation: "receive-streaming-response",
329 },
330 );
331 }
332​
333 static fromCoreHandlerError(error: AcpError, method: string) {
334 if (error._tag === "AcpRequestError") {
335 return error;
336 }
337 return AcpRequestError.internalError(
338 `ACP request handler failed for method '${method}'`,
339 undefined,
340 {
341 method,
342 operation: "handle-request",
343 cause: error,
344 },
345 );
346 }
347​
348 static fromExtensionHandlerError(error: AcpError, method: string) {
349 if (error._tag === "AcpRequestError") {
350 return error;
351 }
352 return AcpRequestError.internalError(
353 `ACP extension request handler failed for method '${method}'`,
354 undefined,
355 {
356 method,
357 operation: "handle-extension-request",
358 cause: error,
359 },
360 );
361 }
362​
363 static parseError(message = "Parse error", data?: unknown) {
364 return new AcpRequestError({
365 code: -32700,
366 errorMessage: message,
367 ...(data !== undefined ? { data } : {}),
368 });
369 }
370​
371 static invalidRequest(message = "Invalid request", data?: unknown) {
372 return new AcpRequestError({
373 code: -32600,
374 errorMessage: message,
375 ...(data !== undefined ? { data } : {}),
376 });
377 }
378​
379 static methodNotFound(method: string) {
380 return new AcpRequestError({
381 code: -32601,
382 errorMessage: `Method not found: ${method}`,
383 });
384 }
385​
386 static invalidParams(message = "Invalid params", data?: unknown) {
387 return new AcpRequestError({
388 code: -32602,
389 errorMessage: message,
390 ...(data !== undefined ? { data } : {}),
391 });
392 }
393​
394 static invalidExtensionPayload(method: string, cause: Schema.SchemaError) {
395 const diagnostics = schemaIssueDiagnostics(cause.issue);
396 return new AcpRequestError({
397 code: -32602,
398 errorMessage: `Invalid payload for ACP extension method '${method}'.`,
399 data: diagnostics,
400 method,
401 operation: "decode-extension-request-payload",
402 ...diagnostics,
403 cause,
404 });
405 }
406​
407 static internalError(
408 message = "Internal error",
409 data?: unknown,
410 diagnostics: AcpRequestDiagnostics = {},
411 ) {
412 return new AcpRequestError({
413 code: -32603,
414 errorMessage: message,
415 ...(data !== undefined ? { data } : {}),
416 ...diagnostics,
417 });
418 }
419​
420 static authRequired(message = "Authentication required", data?: unknown) {
421 return new AcpRequestError({
422 code: -32000,
423 errorMessage: message,
424 ...(data !== undefined ? { data } : {}),
425 });
426 }
427​
428 static resourceNotFound(message = "Resource not found", data?: unknown) {
429 return new AcpRequestError({
430 code: -32002,
431 errorMessage: message,
432 ...(data !== undefined ? { data } : {}),
433 });
434 }
435​
436 toProtocolError() {
437 return AcpSchema.Error.make({
438 code: this.code,
439 message: this.errorMessage,
440 ...(this.data !== undefined ? { data: this.data } : {}),
441 });
442 }
444​
445export const AcpError = Schema.Union([
446 AcpRequestError,
447 AcpSpawnError,
448 AcpProcessExitedError,
449 AcpProtocolParseError,
450 AcpTransportError,
451 AcpInputStreamEndedError,
452]);
453​
454export type AcpError = typeof AcpError.Type;