1import * as Context from "effect/Context";
2import * as Deferred from "effect/Deferred";
3import * as Effect from "effect/Effect";
4import * as ErrorReporter from "effect/ErrorReporter";
5import * as Layer from "effect/Layer";
6import * as Option from "effect/Option";
7import * as Schema from "effect/Schema";
8import * as Predicate from "effect/Predicate";
9import * as Scope from "effect/Scope";
10import * as Stream from "effect/Stream";
11import * as RpcClient from "effect/rpc/RpcClient";
12import * as RpcMessage from "effect/rpc/RpcMessage";
13import * as RpcServer from "effect/rpc/RpcServer";
14import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
15
16import * as AcpError from "./errors.ts";
17import * as AcpProtocol from "./protocol.ts";
18import * as AcpRpcs from "./rpc.ts";
19import * as AcpSchema from "./compat.ts";
20import * as AcpSchemaV1 from "./_generated/schema-v1.gen.ts";
21import * as AcpSchemaV2 from "./_generated/schema.gen.ts";
22import { AGENT_METHODS, CLIENT_METHODS } from "./_generated/meta.gen.ts";
23import {
24 callRpc,
25 decodeExtNotificationRegistration,
26 decodeExtRequestRegistration,
27 isolateNotificationHandler,
28 runHandler,
29} from "./_internal/shared.ts";
30import { makeChildStdio, makeTerminationError } from "./_internal/stdio.ts";
31
32const decodeElicitationRequest = Schema.decodeUnknownEffect(
33 Schema.Union([AcpSchemaV2.CreateElicitationRequest, AcpSchemaV1.CreateElicitationRequest]),
34);
35// ACP session notices postdate the pinned v1.21 and v2 alpha.3 schemas, so they arrive as unknown updates.
36const decodeNoticeUpdate = Schema.decodeUnknownOption(
37 Schema.Struct({
38 sessionUpdate: Schema.Literal("notice"),
39 severity: Schema.String,
40 title: Schema.NonEmptyString,
41 description: Schema.optionalKey(Schema.NullOr(Schema.String)),
42 _meta: Schema.optionalKey(Schema.NullOr(Schema.Record(Schema.String, Schema.Json))),
43 }),
44);
45
46export interface AcpClientOptions {
47 readonly logIncoming?: boolean;
48 readonly logOutgoing?: boolean;
49 readonly logger?: (event: AcpProtocol.AcpProtocolLogEvent) => Effect.Effect<void, never>;
50 readonly onIncomingRequest?: AcpProtocol.AcpPatchedProtocolOptions["onIncomingRequest"];
51 readonly onTermination?: AcpProtocol.AcpPatchedProtocolOptions["onTermination"];
52 readonly onOutgoingResponseFailure?: AcpProtocol.AcpPatchedProtocolOptions["onOutgoingResponseFailure"];
53 readonly onOutgoingResponse?: AcpProtocol.AcpPatchedProtocolOptions["onOutgoingResponse"];
54 readonly transformStdout?: (
55 stdout: ChildProcessSpawner.ChildProcessHandle["stdout"],
56 ) => AcpProtocol.AcpStdio["stdin"];
57 readonly transformSessionUpdate?: (
58 notification: AcpSchema.SessionNotification,
59 ) => AcpSchema.SessionNotification;
60}
61
62type AcpClientRaw = {
63 /**
64 * Inbound notifications. Unread ones are kept best-effort: the newest 32 whose
65 * JSON lines fit in 16Mi characters, always including the latest. Ends with the
66 * connection.
67 */
68 readonly notifications: Stream.Stream<AcpProtocol.AcpIncomingNotification>;
69 readonly request: (method: string, payload: unknown) => Effect.Effect<unknown, AcpError.AcpError>;
70 readonly notify: (method: string, payload: unknown) => Effect.Effect<void, AcpError.AcpError>;
71};
72
73export type AcpRequestHandler<Request, Response> = AcpProtocol.AcpRequestHandler<Request, Response>;
74
75export class AcpClient extends Context.Service<
76 AcpClient,
77 {
78 readonly raw: AcpClientRaw;
79 readonly agent: {
80 readonly initialize: (
81 payload: AcpSchema.InitializeRequest,
82 ) => Effect.Effect<AcpSchema.InitializeResponse, AcpError.AcpError>;
83 readonly authenticate: (
84 payload: AcpSchema.AuthenticateRequest,
85 ) => Effect.Effect<AcpSchema.AuthenticateResponse, AcpError.AcpError>;
86 readonly logout: (
87 payload: AcpSchema.LogoutRequest,
88 ) => Effect.Effect<AcpSchema.LogoutResponse, AcpError.AcpError>;
89 readonly createSession: (
90 payload: AcpSchema.NewSessionRequest,
91 ) => Effect.Effect<AcpSchema.NewSessionResponse, AcpError.AcpError>;
92 /**
93 * This compatibility name maps to ACP v2's `session/resume`; v2 removed
94 * `session/load` and expresses replay with `replayFrom`.
95 */
96 readonly loadSession: (
97 payload: AcpSchema.LoadSessionRequest,
98 ) => Effect.Effect<AcpSchema.LoadSessionResponse, AcpError.AcpError>;
99 readonly listSessions: (
100 payload: AcpSchema.ListSessionsRequest,
101 ) => Effect.Effect<AcpSchema.ListSessionsResponse, AcpError.AcpError>;
102 readonly forkSession: (
103 payload: AcpSchema.ForkSessionRequest,
104 ) => Effect.Effect<AcpSchema.ForkSessionResponse, AcpError.AcpError>;
105 readonly resumeSession: (
106 payload: AcpSchema.ResumeSessionRequest,
107 ) => Effect.Effect<AcpSchema.ResumeSessionResponse, AcpError.AcpError>;
108 readonly closeSession: (
109 payload: AcpSchema.CloseSessionRequest,
110 ) => Effect.Effect<AcpSchema.CloseSessionResponse, AcpError.AcpError>;
111 readonly deleteSession: (
112 payload: AcpSchema.DeleteSessionRequest,
113 ) => Effect.Effect<AcpSchema.DeleteSessionResponse, AcpError.AcpError>;
114 readonly listProviders: (
115 payload: AcpSchema.ListProvidersRequest,
116 ) => Effect.Effect<AcpSchema.ListProvidersResponse, AcpError.AcpError>;
117 readonly setProvider: (
118 payload: AcpSchema.SetProviderRequest,
119 ) => Effect.Effect<AcpSchema.SetProviderResponse, AcpError.AcpError>;
120 readonly disableProvider: (
121 payload: AcpSchema.DisableProviderRequest,
122 ) => Effect.Effect<AcpSchema.DisableProviderResponse, AcpError.AcpError>;
123 readonly setSessionModel: (
124 payload: AcpSchema.SetSessionModelRequest,
125 ) => Effect.Effect<AcpSchema.SetSessionModelResponse, AcpError.AcpError>;
126 /**
127 * Selects a session mode on ACP v1 agents, which predate mode config
128 * options. ACP v2 fails with method-not-found; use `setSessionConfigOption`.
129 */
130 readonly setSessionMode: (
131 payload: AcpSchemaV1.SetSessionModeRequest,
132 ) => Effect.Effect<AcpSchemaV1.SetSessionModeResponse, AcpError.AcpError>;
133 readonly setSessionConfigOption: (
134 payload: AcpSchema.SetSessionConfigOptionRequest,
135 ) => Effect.Effect<AcpSchema.SetSessionConfigOptionResponse, AcpError.AcpError>;
136 readonly prompt: (
137 payload: AcpSchema.PromptRequest,
138 ) => Effect.Effect<AcpSchema.PromptResponse, AcpError.AcpError>;
139 readonly cancel: (
140 payload: AcpSchema.CancelNotification,
141 ) => Effect.Effect<void, AcpError.AcpError>;
142 };
143 readonly handleRequestPermission: (
144 handler: AcpRequestHandler<
145 AcpSchema.RequestPermissionRequest,
146 AcpSchema.RequestPermissionResponse
147 >,
148 ) => Effect.Effect<void>;
149 readonly handleElicitation: (
150 handler: AcpRequestHandler<
151 AcpSchema.CreateElicitationRequest,
152 AcpSchema.CreateElicitationResponse
153 >,
154 ) => Effect.Effect<void>;
155 readonly handleMcpConnect: (
156 handler: AcpRequestHandler<AcpSchema.ConnectMcpRequest, AcpSchema.ConnectMcpResponse>,
157 ) => Effect.Effect<void>;
158 readonly handleMcpMessage: (
159 handler: AcpRequestHandler<AcpSchema.MessageMcpRequest, AcpSchema.MessageMcpResponse>,
160 ) => Effect.Effect<void>;
161 readonly handleMcpDisconnect: (
162 handler: AcpRequestHandler<AcpSchema.DisconnectMcpRequest, AcpSchema.DisconnectMcpResponse>,
163 ) => Effect.Effect<void>;
164 readonly handleMcpNotification: (
165 handler: (
166 notification: AcpSchema.MessageMcpNotification,
167 ) => Effect.Effect<void, AcpError.AcpError>,
168 ) => Effect.Effect<void>;
169 readonly handleReadTextFile: (
170 handler: AcpRequestHandler<AcpSchema.ReadTextFileRequest, AcpSchema.ReadTextFileResponse>,
171 ) => Effect.Effect<void>;
172 readonly handleWriteTextFile: (
173 handler: AcpRequestHandler<
174 AcpSchema.WriteTextFileRequest,
175 AcpSchema.WriteTextFileResponse | void
176 >,
177 ) => Effect.Effect<void>;
178 readonly handleCreateTerminal: (
179 handler: AcpRequestHandler<AcpSchema.CreateTerminalRequest, AcpSchema.CreateTerminalResponse>,
180 ) => Effect.Effect<void>;
181 readonly handleTerminalOutput: (
182 handler: AcpRequestHandler<AcpSchema.TerminalOutputRequest, AcpSchema.TerminalOutputResponse>,
183 ) => Effect.Effect<void>;
184 readonly handleTerminalWaitForExit: (
185 handler: AcpRequestHandler<
186 AcpSchema.WaitForTerminalExitRequest,
187 AcpSchema.WaitForTerminalExitResponse
188 >,
189 ) => Effect.Effect<void>;
190 readonly handleTerminalKill: (
191 handler: AcpRequestHandler<
192 AcpSchema.KillTerminalRequest,
193 AcpSchema.KillTerminalResponse | void
194 >,
195 ) => Effect.Effect<void>;
196 readonly handleTerminalRelease: (
197 handler: AcpRequestHandler<
198 AcpSchema.ReleaseTerminalRequest,
199 AcpSchema.ReleaseTerminalResponse | void
200 >,
201 ) => Effect.Effect<void>;
202 readonly handleSessionUpdate: (
203 handler: (
204 notification: AcpSchema.SessionNotification,
205 ) => Effect.Effect<void, AcpError.AcpError>,
206 ) => Effect.Effect<void>;
207 readonly handleElicitationComplete: (
208 handler: (
209 notification: AcpSchema.CompleteElicitationNotification,
210 ) => Effect.Effect<void, AcpError.AcpError>,
211 ) => Effect.Effect<void>;
212 readonly handleUnknownExtRequest: (
213 handler: (
214 method: string,
215 params: unknown,
216 context: AcpProtocol.AcpRequestContext,
217 ) => Effect.Effect<unknown, AcpError.AcpError>,
218 ) => Effect.Effect<void>;
219 readonly handleUnknownExtNotification: (
220 handler: (method: string, params: unknown) => Effect.Effect<void, AcpError.AcpError>,
221 ) => Effect.Effect<void>;
222 readonly handleExtRequest: <A, I>(
223 method: string,
224 payload: Schema.Codec<A, I>,
225 handler: AcpRequestHandler<A, unknown>,
226 ) => Effect.Effect<void>;
227 readonly handleExtNotification: <A, I>(
228 method: string,
229 payload: Schema.Codec<A, I>,
230 handler: (payload: A) => Effect.Effect<void, AcpError.AcpError>,
231 ) => Effect.Effect<void>;
232 }
233>()("effect-acp/client/AcpClient") {}
234
235interface AcpCoreRequestHandlers {
236 requestPermission?: AcpRequestHandler<
237 AcpSchema.RequestPermissionRequest,
238 AcpSchema.RequestPermissionResponse
239 >;
240 elicitation?: AcpRequestHandler<
241 AcpSchema.CreateElicitationRequest,
242 AcpSchema.CreateElicitationResponse
243 >;
244 mcpConnect?: AcpRequestHandler<AcpSchema.ConnectMcpRequest, AcpSchema.ConnectMcpResponse>;
245 mcpMessage?: AcpRequestHandler<AcpSchema.MessageMcpRequest, AcpSchema.MessageMcpResponse>;
246 mcpDisconnect?: AcpRequestHandler<
247 AcpSchema.DisconnectMcpRequest,
248 AcpSchema.DisconnectMcpResponse
249 >;
250 readTextFile?: AcpRequestHandler<AcpSchema.ReadTextFileRequest, AcpSchema.ReadTextFileResponse>;
251 writeTextFile?: AcpRequestHandler<
252 AcpSchema.WriteTextFileRequest,
253 AcpSchema.WriteTextFileResponse | void
254 >;
255 createTerminal?: AcpRequestHandler<
256 AcpSchema.CreateTerminalRequest,
257 AcpSchema.CreateTerminalResponse
258 >;
259 terminalOutput?: AcpRequestHandler<
260 AcpSchema.TerminalOutputRequest,
261 AcpSchema.TerminalOutputResponse
262 >;
263 terminalWaitForExit?: AcpRequestHandler<
264 AcpSchema.WaitForTerminalExitRequest,
265 AcpSchema.WaitForTerminalExitResponse
266 >;
267 terminalKill?: AcpRequestHandler<
268 AcpSchema.KillTerminalRequest,
269 AcpSchema.KillTerminalResponse | void
270 >;
271 terminalRelease?: AcpRequestHandler<
272 AcpSchema.ReleaseTerminalRequest,
273 AcpSchema.ReleaseTerminalResponse | void
274 >;
275}
276
277interface AcpNotificationHandlers {
278 readonly sessionUpdate: BufferedNotificationHandler<AcpSchema.SessionNotification>;
279 readonly elicitationComplete: BufferedNotificationHandler<AcpSchema.CompleteElicitationNotification>;
280}
281
282interface BufferedNotificationHandler<A> {
283 readonly handlers: Array<(notification: A) => Effect.Effect<void, AcpError.AcpError>>;
284 readonly pending: Array<A>;
285}
286
287function normalizeContentBlock(content: AcpSchemaV2.ContentBlock): AcpSchema.ContentBlock {
288 switch (content.type) {
289 case "text":
290 case "image":
291 case "audio":
292 case "resource_link":
293 case "resource":
294 return content as AcpSchema.ContentBlock;
295 default:
296 return { type: "_t3_unknown", originalType: content.type, raw: content };
297 }
298}
299
300function normalizeToolCallContent(content: AcpSchemaV2.ToolCallContent): AcpSchema.ToolCallContent {
301 if (content.type === "content") {
302 const known = content as Extract<AcpSchemaV2.ToolCallContent, { readonly type: "content" }>;
303 return { ...known, content: normalizeContentBlock(known.content) };
304 }
305 if (content.type === "terminal") {
306 return content as Extract<AcpSchemaV2.ToolCallContent, { readonly type: "terminal" }>;
307 }
308 if (content.type !== "diff") {
309 return { type: "_t3_unknown", originalType: content.type, raw: content };
310 }
311
312 return content as Extract<AcpSchemaV2.ToolCallContent, { readonly type: "diff" }>;
313}
314
315function normalizeToolCallUpdate(update: AcpSchemaV2.ToolCallUpdate): AcpSchema.ToolCallUpdate {
316 return {
317 toolCallId: update.toolCallId,
318 ...(update.name === undefined ? {} : { name: update.name }),
319 ...(update.title === undefined ? {} : { title: update.title }),
320 ...(update.kind === undefined ? {} : { kind: update.kind }),
321 ...(update.status === undefined ? {} : { status: update.status }),
322 ...(update.content === undefined
323 ? {}
324 : {
325 content: update.content === null ? null : update.content.map(normalizeToolCallContent),
326 }),
327 ...(update.locations === undefined ? {} : { locations: update.locations }),
328 ...(update.rawInput === undefined ? {} : { rawInput: update.rawInput }),
329 ...(update.rawOutput === undefined ? {} : { rawOutput: update.rawOutput }),
330 ...(update._meta === undefined ? {} : { _meta: update._meta }),
331 };
332}
333
334function normalizeConfigOption(
335 option: AcpSchemaV2.SessionConfigOption,
336): AcpSchema.SessionConfigOption | undefined {
337 if (option.type === "boolean") {
338 const known = option as Extract<AcpSchemaV2.SessionConfigOption, { readonly type: "boolean" }>;
339 return {
340 type: "boolean",
341 id: known.configId,
342 name: known.name,
343 currentValue: known.currentValue,
344 ...(known.description === undefined ? {} : { description: known.description }),
345 ...(known.category === undefined ? {} : { category: known.category }),
346 ...(known._meta === undefined ? {} : { _meta: known._meta }),
347 };
348 }
349 if (option.type === "select") {
350 const known = option as Extract<AcpSchemaV2.SessionConfigOption, { readonly type: "select" }>;
351 return {
352 type: "select",
353 id: known.configId,
354 name: known.name,
355 currentValue: known.currentValue,
356 options: known.options,
357 ...(known.description === undefined ? {} : { description: known.description }),
358 ...(known.category === undefined ? {} : { category: known.category }),
359 ...(known._meta === undefined ? {} : { _meta: known._meta }),
360 };
361 }
362 return undefined;
363}
364
365function normalizeConfigOptions(
366 options: ReadonlyArray<AcpSchemaV2.SessionConfigOption> | undefined,
367): ReadonlyArray<AcpSchema.SessionConfigOption> | undefined {
368 return options?.flatMap((option) => {
369 const normalized = normalizeConfigOption(option);
370 return normalized === undefined ? [] : [normalized];
371 });
372}
373
374function normalizeSessionUpdate(
375 notification: AcpSchemaV2.UpdateSessionNotification,
376): AcpSchema.SessionNotification {
377 const update = notification.update;
378 const base = {
379 sessionId: notification.sessionId,
380 ...(notification._meta === undefined ? {} : { _meta: notification._meta }),
381 };
382
383 switch (update.sessionUpdate) {
384 case "user_message_chunk":
385 case "agent_message_chunk":
386 case "agent_thought_chunk": {
387 const known = update as Extract<
388 AcpSchemaV2.SessionUpdate,
389 {
390 readonly sessionUpdate:
391 | "user_message_chunk"
392 | "agent_message_chunk"
393 | "agent_thought_chunk";
394 }
395 >;
396 const content = normalizeContentBlock(known.content);
397 return { ...base, update: { ...known, content } };
398 }
399 case "user_message":
400 case "agent_message":
401 case "agent_thought": {
402 const known = update as Extract<
403 AcpSchemaV2.SessionUpdate,
404 { readonly sessionUpdate: "user_message" | "agent_message" | "agent_thought" }
405 >;
406 const content = known.content;
407 return {
408 ...base,
409 update: {
410 ...known,
411 ...(content === undefined
412 ? {}
413 : {
414 content:
415 content === null
416 ? null
417 : (content as ReadonlyArray<AcpSchemaV2.ContentBlock>).map(
418 normalizeContentBlock,
419 ),
420 }),
421 },
422 } as AcpSchema.SessionNotification;
423 }
424 case "tool_call_update": {
425 const known = update as Extract<
426 AcpSchemaV2.SessionUpdate,
427 { readonly sessionUpdate: "tool_call_update" }
428 >;
429 return {
430 ...base,
431 update: { sessionUpdate: "tool_call_update", ...normalizeToolCallUpdate(known) },
432 };
433 }
434 case "tool_call_content_chunk": {
435 const known = update as Extract<
436 AcpSchemaV2.SessionUpdate,
437 { readonly sessionUpdate: "tool_call_content_chunk" }
438 >;
439 return {
440 ...base,
441 update: {
442 sessionUpdate: "tool_call_content_chunk",
443 toolCallId: known.toolCallId,
444 content: normalizeToolCallContent(known.content),
445 ...(known._meta === undefined ? {} : { _meta: known._meta }),
446 },
447 };
448 }
449 case "config_option_update": {
450 const known = update as Extract<
451 AcpSchemaV2.SessionUpdate,
452 { readonly sessionUpdate: "config_option_update" }
453 >;
454 return {
455 ...base,
456 update: {
457 sessionUpdate: "config_option_update",
458 configOptions: known.configOptions.flatMap((option) => {
459 const normalized = normalizeConfigOption(option);
460 return normalized === undefined ? [] : [normalized];
461 }),
462 ...(known._meta === undefined ? {} : { _meta: known._meta }),
463 },
464 };
465 }
466 case "state_update":
467 case "terminal_update":
468 case "terminal_output_chunk":
469 case "plan_update":
470 case "plan_removed":
471 case "session_info_update":
472 case "usage_update":
473 case "compaction_update":
474 case "compaction_summary_chunk":
475 return { ...base, update } as AcpSchema.SessionNotification;
476 case "available_commands_update": {
477 const known = update as Extract<
478 AcpSchemaV2.SessionUpdate,
479 { readonly sessionUpdate: "available_commands_update" }
480 >;
481 return {
482 ...base,
483 update: {
484 ...known,
485 availableCommands: known.availableCommands.map((command) => ({
486 ...command,
487 ...(command.input?.type === "text"
488 ? {
489 input: command.input as Extract<
490 AcpSchemaV2.AvailableCommandInput,
491 { readonly type: "text" }
492 >,
493 }
494 : { input: null }),
495 })),
496 },
497 };
498 }
499 default: {
500 const notice = decodeNoticeUpdate(update);
501 if (Option.isSome(notice)) return { ...base, update: notice.value };
502 return {
503 ...base,
504 update: {
505 sessionUpdate: "_t3_unknown",
506 originalSessionUpdate: update.sessionUpdate,
507 raw: update,
508 },
509 };
510 }
511 }
512}
513
514function normalizeV1SessionUpdate(
515 notification: AcpSchemaV1.SessionNotification,
516): AcpSchema.SessionNotification {
517 // v1 passes unknown updates through raw, so a malformed notice must not reach consumers as one.
518 if ((notification.update.sessionUpdate as string) === "notice") {
519 const notice = decodeNoticeUpdate(notification.update);
520 return {
521 ...notification,
522 update: Option.isSome(notice)
523 ? notice.value
524 : {
525 sessionUpdate: "_t3_unknown",
526 originalSessionUpdate: "notice",
527 raw: notification.update,
528 },
529 };
530 }
531 if (notification.update.sessionUpdate !== "available_commands_update") {
532 return notification as unknown as AcpSchema.SessionNotification;
533 }
534 return {
535 ...notification,
536 update: {
537 ...notification.update,
538 availableCommands: notification.update.availableCommands.map((command) => {
539 const { input, ...rest } = command;
540 return {
541 ...rest,
542 ...(input == null ? {} : { input: { type: "text" as const, hint: input.hint } }),
543 };
544 }),
545 },
546 };
547}
548
549function toV2McpServer(server: AcpSchema.McpServer): AcpSchemaV2.McpServer {
550 if ("type" in server) return server;
551 return {
552 type: "stdio",
553 name: server.name,
554 command: server.command,
555 ...(server.args === undefined ? {} : { args: server.args }),
556 ...(server.env === undefined ? {} : { env: server.env }),
557 ...(server._meta === undefined ? {} : { _meta: server._meta }),
558 };
559}
560
561function toV2ResumeRequest(
562 request: AcpSchema.ResumeSessionRequest,
563 replayFrom?: AcpSchemaV2.ReplayFrom,
564): AcpSchemaV2.ResumeSessionRequest {
565 const { mcpServers, ...rest } = request;
566 return {
567 ...rest,
568 ...(mcpServers === undefined ? {} : { mcpServers: mcpServers.map(toV2McpServer) }),
569 ...(replayFrom === undefined ? {} : { replayFrom }),
570 };
571}
572
573function toV2ForkRequest(request: AcpSchema.ForkSessionRequest): AcpSchemaV2.ForkSessionRequest {
574 const { mcpServers, ...rest } = request;
575 return {
576 ...rest,
577 ...(mcpServers === undefined ? {} : { mcpServers: mcpServers.map(toV2McpServer) }),
578 };
579}
580
581function normalizeInitializeResponse(
582 response: AcpSchemaV2.InitializeResponse,
583): AcpSchema.InitializeResponse {
584 const session = response.capabilities?.session;
585 const prompt = session?.prompt;
586 const mcp = session?.mcp;
587 return {
588 protocolVersion: response.protocolVersion,
589 agentInfo: response.info,
590 agentCapabilities: {
591 loadSession: session !== undefined && session !== null,
592 promptCapabilities: {
593 image: prompt?.image != null,
594 audio: prompt?.audio != null,
595 embeddedContext: prompt?.embeddedContext != null,
596 },
597 mcpCapabilities: {
598 stdio: mcp?.stdio != null,
599 http: mcp?.http != null,
600 acp: mcp?.acp != null,
601 },
602 ...(session == null
603 ? {}
604 : {
605 sessionCapabilities: {
606 list: {},
607 resume: {},
608 close: {},
609 ...(session.delete == null ? {} : { delete: {} }),
610 ...(session.fork == null ? {} : { fork: {} }),
611 ...(session.additionalDirectories == null ? {} : { additionalDirectories: {} }),
612 },
613 }),
614 ...(response.capabilities?.providers == null ? {} : { providers: {} }),
615 ...(response.authMethods === undefined || response.authMethods.length === 0
616 ? {}
617 : { auth: { logout: {} } }),
618 },
619 ...(response.authMethods === undefined
620 ? {}
621 : {
622 authMethods: response.authMethods
623 .map((method) => {
624 const record = method as AcpSchemaV2.AuthMethod;
625 const base = {
626 id: record.methodId,
627 name: record.name,
628 ...(record.description === undefined ? {} : { description: record.description }),
629 ...(record._meta === undefined ? {} : { _meta: record._meta }),
630 };
631 if (
632 record.type === "env_var" &&
633 Array.isArray(record.vars) &&
634 record.vars.every(
635 (variable) =>
636 typeof variable === "object" &&
637 variable !== null &&
638 typeof (variable as { readonly name?: unknown }).name === "string",
639 )
640 ) {
641 return {
642 ...base,
643 type: "env_var" as const,
644 vars: record.vars.map((variable) => {
645 const value = variable as { readonly name: string; readonly label?: unknown };
646 return {
647 name: value.name,
648 ...(typeof value.label === "string" ? { label: value.label } : {}),
649 };
650 }),
651 ...(typeof record.link === "string" ? { link: record.link } : {}),
652 };
653 }
654 if (record.type === "agent") return { ...base, type: "agent" as const };
655 if (record.type !== "terminal") return undefined;
656 return {
657 ...base,
658 type: "terminal" as const,
659 ...(Array.isArray(record.args)
660 ? {
661 args: record.args.filter(
662 (argument): argument is string => typeof argument === "string",
663 ),
664 }
665 : {}),
666 ...(Array.isArray(record.env)
667 ? {
668 env: Object.fromEntries(
669 (record.env as ReadonlyArray<AcpSchemaV2.EnvVariable>).map((variable) => [
670 variable.name,
671 variable.value,
672 ]),
673 ),
674 }
675 : {}),
676 };
677 })
678 .filter(Predicate.isNotUndefined),
679 }),
680 ...(response._meta === undefined ? {} : { _meta: response._meta }),
681 };
682}
683
684function toV2InitializeRequest(
685 request: AcpSchema.InitializeRequest,
686): AcpSchemaV2.InitializeRequest {
687 const capabilities = request.clientCapabilities;
688 return {
689 protocolVersion: 2,
690 info: request.clientInfo ?? { name: "t3-code", version: "unknown" },
691 capabilities: {
692 ...(capabilities?.auth?.terminal === true ? { auth: { terminal: {} } } : {}),
693 ...(capabilities?.elicitation == null ? {} : { elicitation: capabilities.elicitation }),
694 ...(capabilities?._meta === undefined ? {} : { _meta: capabilities._meta }),
695 },
696 ...(request._meta === undefined ? {} : { _meta: request._meta }),
697 };
698}
699
700function toNegotiatingInitializeRequest(
701 request: AcpSchema.InitializeRequest,
702): AcpSchemaV1.InitializeRequest | AcpSchemaV2.InitializeRequest {
703 return {
704 ...toV2InitializeRequest(request),
705 clientCapabilities: request.clientCapabilities ?? {},
706 ...(request.clientInfo === undefined ? {} : { clientInfo: request.clientInfo }),
707 } as unknown as AcpSchemaV1.InitializeRequest | AcpSchemaV2.InitializeRequest;
708}
709
710function isV2InitializeResponse(
711 response: AcpSchemaV1.InitializeResponse | AcpSchemaV2.InitializeResponse,
712): response is AcpSchemaV2.InitializeResponse {
713 return "info" in response;
714}
715
716function normalizeV2SessionSetupResponse(
717 response:
718 | AcpSchemaV2.NewSessionResponse
719 | AcpSchemaV2.ResumeSessionResponse
720 | AcpSchemaV2.ForkSessionResponse,
721) {
722 const configOptions = normalizeConfigOptions(response.configOptions);
723 return {
724 ...("sessionId" in response ? { sessionId: response.sessionId } : {}),
725 ...(configOptions === undefined ? {} : { configOptions }),
726 ...(response._meta === undefined ? {} : { _meta: response._meta }),
727 };
728}
729
730function normalizePermissionRequest(
731 request: AcpSchemaV2.RequestPermissionRequest,
732 context: AcpProtocol.AcpRequestContext,
733): AcpSchema.RequestPermissionRequest {
734 const subject = request.subject;
735 const toolCall =
736 subject?.type === "tool_call" && "toolCall" in subject
737 ? normalizeToolCallUpdate(subject.toolCall as AcpSchemaV2.ToolCallUpdate)
738 : {
739 toolCallId:
740 subject?.type === "command" &&
741 "toolCallId" in subject &&
742 typeof subject.toolCallId === "string"
743 ? subject.toolCallId
744 : context.requestId,
745 title: request.title,
746 kind: subject?.type === "command" ? ("execute" as const) : ("other" as const),
747 };
748 return {
749 sessionId: request.sessionId,
750 title: request.title,
751 ...(request.description === undefined ? {} : { description: request.description }),
752 ...(subject === undefined ? {} : { subject }),
753 toolCall,
754 options: request.options,
755 ...(request._meta === undefined ? {} : { _meta: request._meta }),
756 };
757}
758const decodeMessageMcpNotification = Schema.decodeUnknownEffect(AcpSchemaV2.MessageMcpNotification);
759
760export const make = Effect.fn("effect-acp/AcpClient.make")(function* (
761 stdio: AcpProtocol.AcpStdio,
762 options: AcpClientOptions = {},
763 terminationError?: Effect.Effect<AcpError.AcpError>,
764): Effect.fn.Return<AcpClient["Service"], never, Scope.Scope> {
765 const coreHandlers: AcpCoreRequestHandlers = {};
766 const notificationHandlers: AcpNotificationHandlers = {
767 sessionUpdate: { handlers: [], pending: [] },
768 elicitationComplete: { handlers: [], pending: [] },
769 };
770 const promptCompletions = new Map<
771 string,
772 Deferred.Deferred<AcpSchema.PromptResponse, AcpError.AcpError>
773 >();
774 const extRequestHandlers = new Map<string, AcpRequestHandler<unknown, unknown>>();
775 const extNotificationHandlers = new Map<
776 string,
777 (params: unknown) => Effect.Effect<void, AcpError.AcpError>
778 >();
779 let unknownExtRequestHandler:
780 | ((
781 method: string,
782 params: unknown,
783 context: AcpProtocol.AcpRequestContext,
784 ) => Effect.Effect<unknown, AcpError.AcpError>)
785 | undefined;
786 let unknownExtNotificationHandler:
787 | ((method: string, params: unknown) => Effect.Effect<void, AcpError.AcpError>)
788 | undefined;
789 let negotiatedProtocolGeneration: 1 | 2 | undefined;
790 let sessionDeleteSupported = false;
791
792 const runNotificationHandlers = <A>(
793 registration: BufferedNotificationHandler<A>,
794 notification: A,
795 ) =>
796 Effect.forEach(
797 registration.handlers,
798 (handler) => isolateNotificationHandler(handler(notification)),
799 { discard: true },
800 );
801
802 const flushBufferedNotifications = <A>(registration: BufferedNotificationHandler<A>) =>
803 Effect.suspend(() => {
804 if (registration.handlers.length === 0 || registration.pending.length === 0) {
805 return Effect.void;
806 }
807 const pending = registration.pending.splice(0, registration.pending.length);
808 return Effect.forEach(
809 pending,
810 (notification) => runNotificationHandlers(registration, notification),
811 {
812 discard: true,
813 },
814 );
815 });
816
817 const dispatchNotification = (notification: AcpProtocol.AcpIncomingNotification) => {
818 switch (notification._tag) {
819 case "SessionUpdate": {
820 const normalized = notification.params as AcpSchema.SessionNotification;
821 const state = normalized.update;
822 const completePrompt =
823 state.sessionUpdate === "state_update" && state.state === "idle"
824 ? Effect.suspend(() => {
825 const idle = state as Extract<
826 AcpSchema.SessionUpdate,
827 { readonly sessionUpdate: "state_update"; readonly state: "idle" }
828 >;
829 const completion = promptCompletions.get(normalized.sessionId);
830 if (completion === undefined) return Effect.void;
831 promptCompletions.delete(normalized.sessionId);
832 return Deferred.succeed(completion, {
833 stopReason: idle.stopReason ?? "end_turn",
834 ...(idle.usage === undefined ? {} : { usage: idle.usage }),
835 ...(idle.error === undefined ? {} : { error: idle.error }),
836 ...(idle._meta === undefined ? {} : { _meta: idle._meta }),
837 }).pipe(Effect.asVoid);
838 })
839 : Effect.void;
840 if (notificationHandlers.sessionUpdate.handlers.length === 0) {
841 notificationHandlers.sessionUpdate.pending.push(normalized);
842 return completePrompt;
843 }
844 return Effect.all(
845 [completePrompt, runNotificationHandlers(notificationHandlers.sessionUpdate, normalized)],
846 { discard: true },
847 );
848 }
849 case "ElicitationComplete": {
850 if (notificationHandlers.elicitationComplete.handlers.length === 0) {
851 notificationHandlers.elicitationComplete.pending.push(notification.params);
852 return Effect.void;
853 }
854 return runNotificationHandlers(
855 notificationHandlers.elicitationComplete,
856 notification.params,
857 );
858 }
859 case "ExtNotification": {
860 const handler = extNotificationHandlers.get(notification.method);
861 if (handler) {
862 return handler(notification.params);
863 }
864 return unknownExtNotificationHandler
865 ? unknownExtNotificationHandler(notification.method, notification.params)
866 : Effect.void;
867 }
868 }
869 };
870
871 const dispatchExtRequest = (
872 method: string,
873 params: unknown,
874 context: AcpProtocol.AcpRequestContext,
875 ) => {
876 const handler = extRequestHandlers.get(method);
877 if (handler) {
878 return handler(params, context);
879 }
880 return unknownExtRequestHandler
881 ? unknownExtRequestHandler(method, params, context)
882 : Effect.fail(AcpError.AcpRequestError.methodNotFound(method));
883 };
884
885 const transport = yield* AcpProtocol.makeAcpPatchedProtocol({
886 stdio: stdio,
887 ...(terminationError ? { terminationError } : {}),
888 serverRequestMethods: new Set(AcpRpcs.CompatClientRpcs.requests.keys()),
889 ...(options.logIncoming !== undefined ? { logIncoming: options.logIncoming } : {}),
890 ...(options.logOutgoing !== undefined ? { logOutgoing: options.logOutgoing } : {}),
891 ...(options.logger ? { logger: options.logger } : {}),
892 ...(options.onIncomingRequest ? { onIncomingRequest: options.onIncomingRequest } : {}),
893 onTermination: (error) =>
894 Effect.all(
895 [
896 ...Array.from(promptCompletions.values(), (completion) =>
897 Deferred.fail(completion, error).pipe(Effect.asVoid),
898 ),
899 options.onTermination?.(error) ?? Effect.void,
900 ],
901 { discard: true },
902 ).pipe(
903 Effect.tap(() =>
904 Effect.sync(() => {
905 promptCompletions.clear();
906 }),
907 ),
908 ),
909 ...(options.onOutgoingResponseFailure
910 ? { onOutgoingResponseFailure: options.onOutgoingResponseFailure }
911 : {}),
912 ...(options.onOutgoingResponse ? { onOutgoingResponse: options.onOutgoingResponse } : {}),
913 transformSessionUpdate: (notification) => {
914 const normalized =
915 negotiatedProtocolGeneration === 1
916 ? normalizeV1SessionUpdate(notification as AcpSchemaV1.SessionNotification)
917 : normalizeSessionUpdate(notification as AcpSchemaV2.UpdateSessionNotification);
918 return options.transformSessionUpdate?.(normalized) ?? normalized;
919 },
920 onNotification: dispatchNotification,
921 onExtRequest: dispatchExtRequest,
922 });
923
924 const requestContext = (
925 requestId: RpcMessage.RequestId,
926 method: string,
927 ): AcpProtocol.AcpRequestContext => ({
928 requestId: AcpProtocol.acpRequestIdentity(requestId),
929 method,
930 });
931
932 const layerClientHandler = AcpRpcs.CompatClientRpcs.toLayer(
933 AcpRpcs.CompatClientRpcs.of({
934 [CLIENT_METHODS.session_request_permission]: (payload, { requestId }) =>
935 runHandler(
936 coreHandlers.requestPermission,
937 "toolCall" in payload
938 ? (payload as AcpSchema.RequestPermissionRequest)
939 : normalizePermissionRequest(
940 payload,
941 requestContext(requestId, CLIENT_METHODS.session_request_permission),
942 ),
943 CLIENT_METHODS.session_request_permission,
944 requestContext(requestId, CLIENT_METHODS.session_request_permission),
945 ),
946 [AcpRpcs.V1_CLIENT_METHODS.session_elicitation]: (payload, { requestId }) =>
947 runHandler(
948 coreHandlers.elicitation,
949 payload,
950 AcpRpcs.V1_CLIENT_METHODS.session_elicitation,
951 requestContext(requestId, AcpRpcs.V1_CLIENT_METHODS.session_elicitation),
952 ).pipe(
953 Effect.map(({ _meta, ...action }) => ({
954 action,
955 ...(_meta === undefined ? {} : { _meta }),
956 })),
957 ),
958 [AcpRpcs.V1_CLIENT_METHODS.unstable_session_elicitation]: (payload, { requestId }) =>
959 runHandler(
960 coreHandlers.elicitation,
961 payload,
962 AcpRpcs.V1_CLIENT_METHODS.unstable_session_elicitation,
963 requestContext(requestId, AcpRpcs.V1_CLIENT_METHODS.unstable_session_elicitation),
964 ).pipe(
965 Effect.map(({ _meta, ...action }) => ({
966 action,
967 ...(_meta === undefined ? {} : { _meta }),
968 })),
969 ),
970 [CLIENT_METHODS.elicitation_create]: (payload, { requestId }) =>
971 decodeElicitationRequest(payload).pipe(
972 Effect.mapError((cause) =>
973 AcpError.AcpRequestError.invalidExtensionPayload(
974 CLIENT_METHODS.elicitation_create,
975 cause,
976 ).toProtocolError(),
977 ),
978 Effect.flatMap((request) =>
979 runHandler(
980 coreHandlers.elicitation,
981 request,
982 CLIENT_METHODS.elicitation_create,
983 requestContext(requestId, CLIENT_METHODS.elicitation_create),
984 ),
985 ),
986 ),
987 [CLIENT_METHODS.mcp_connect]: (payload, { requestId }) =>
988 runHandler(
989 coreHandlers.mcpConnect,
990 payload,
991 CLIENT_METHODS.mcp_connect,
992 requestContext(requestId, CLIENT_METHODS.mcp_connect),
993 ),
994 [CLIENT_METHODS.mcp_message]: (payload, { requestId }) =>
995 runHandler(
996 coreHandlers.mcpMessage,
997 payload,
998 CLIENT_METHODS.mcp_message,
999 requestContext(requestId, CLIENT_METHODS.mcp_message),
1000 ),
1001 [CLIENT_METHODS.mcp_disconnect]: (payload, { requestId }) =>
1002 runHandler(
1003 coreHandlers.mcpDisconnect,
1004 payload,
1005 CLIENT_METHODS.mcp_disconnect,
1006 requestContext(requestId, CLIENT_METHODS.mcp_disconnect),
1007 ),
1008 [AcpRpcs.V1_CLIENT_METHODS.fs_read_text_file]: (payload, { requestId }) =>
1009 runHandler(
1010 coreHandlers.readTextFile,
1011 payload,
1012 AcpRpcs.V1_CLIENT_METHODS.fs_read_text_file,
1013 requestContext(requestId, AcpRpcs.V1_CLIENT_METHODS.fs_read_text_file),
1014 ),
1015 [AcpRpcs.V1_CLIENT_METHODS.fs_write_text_file]: (payload, { requestId }) =>
1016 runHandler(
1017 coreHandlers.writeTextFile,
1018 payload,
1019 AcpRpcs.V1_CLIENT_METHODS.fs_write_text_file,
1020 requestContext(requestId, AcpRpcs.V1_CLIENT_METHODS.fs_write_text_file),
1021 ).pipe(Effect.map((result) => result ?? {})),
1022 [AcpRpcs.V1_CLIENT_METHODS.terminal_create]: (payload, { requestId }) =>
1023 runHandler(
1024 coreHandlers.createTerminal,
1025 payload,
1026 AcpRpcs.V1_CLIENT_METHODS.terminal_create,
1027 requestContext(requestId, AcpRpcs.V1_CLIENT_METHODS.terminal_create),
1028 ),
1029 [AcpRpcs.V1_CLIENT_METHODS.terminal_output]: (payload, { requestId }) =>
1030 runHandler(
1031 coreHandlers.terminalOutput,
1032 payload,
1033 AcpRpcs.V1_CLIENT_METHODS.terminal_output,
1034 requestContext(requestId, AcpRpcs.V1_CLIENT_METHODS.terminal_output),
1035 ),
1036 [AcpRpcs.V1_CLIENT_METHODS.terminal_wait_for_exit]: (payload, { requestId }) =>
1037 runHandler(
1038 coreHandlers.terminalWaitForExit,
1039 payload,
1040 AcpRpcs.V1_CLIENT_METHODS.terminal_wait_for_exit,
1041 requestContext(requestId, AcpRpcs.V1_CLIENT_METHODS.terminal_wait_for_exit),
1042 ),
1043 [AcpRpcs.V1_CLIENT_METHODS.terminal_kill]: (payload, { requestId }) =>
1044 runHandler(
1045 coreHandlers.terminalKill,
1046 payload,
1047 AcpRpcs.V1_CLIENT_METHODS.terminal_kill,
1048 requestContext(requestId, AcpRpcs.V1_CLIENT_METHODS.terminal_kill),
1049 ).pipe(Effect.map((result) => result ?? {})),
1050 [AcpRpcs.V1_CLIENT_METHODS.terminal_release]: (payload, { requestId }) =>
1051 runHandler(
1052 coreHandlers.terminalRelease,
1053 payload,
1054 AcpRpcs.V1_CLIENT_METHODS.terminal_release,
1055 requestContext(requestId, AcpRpcs.V1_CLIENT_METHODS.terminal_release),
1056 ).pipe(Effect.map((result) => result ?? {})),
1057 }),
1058 );
1059
1060 yield* RpcServer.make(AcpRpcs.CompatClientRpcs, { disableFatalDefects: true }).pipe(
1061 // runHandler logs handler defects with their method. A reporter inherited
1062 // from the caller (a WebSocket request, say) would log them again.
1063 Effect.provideService(ErrorReporter.CurrentErrorReporters, new Set()),
1064 Effect.provideService(RpcServer.Protocol, transport.serverProtocol),
1065 Effect.provide(layerClientHandler),
1066 Effect.forkScoped,
1067 );
1068
1069 let nextRpcRequestId = AcpProtocol.RPC_REQUEST_ID_START;
1070 const rpc = yield* RpcClient.make(AcpRpcs.CompatAgentRpcs, {
1071 generateRequestId: () => RpcMessage.RequestId(nextRpcRequestId++),
1072 }).pipe(Effect.provideService(RpcClient.Protocol, transport.clientProtocol));
1073
1074 return AcpClient.of({
1075 raw: {
1076 notifications: transport.incoming,
1077 request: transport.request,
1078 notify: transport.notify,
1079 },
1080 agent: {
1081 initialize: (payload) =>
1082 callRpc(
1083 AGENT_METHODS.initialize,
1084 rpc[AGENT_METHODS.initialize](toNegotiatingInitializeRequest(payload)),
1085 ).pipe(
1086 Effect.map((response) => {
1087 // ACP v1 protocolVersion values were not generation identifiers in
1088 // every shipped agent. Antigravity, for example, reports version 2
1089 // with the v1 agentInfo/agentCapabilities response shape.
1090 const isV2 = isV2InitializeResponse(response);
1091 negotiatedProtocolGeneration = isV2 ? 2 : 1;
1092 const normalized = isV2
1093 ? normalizeInitializeResponse(response)
1094 : (response as AcpSchema.InitializeResponse);
1095 sessionDeleteSupported =
1096 normalized.agentCapabilities?.sessionCapabilities?.delete != null;
1097 return normalized;
1098 }),
1099 ),
1100 authenticate: (payload) =>
1101 negotiatedProtocolGeneration === 1
1102 ? callRpc(
1103 AcpRpcs.V1_AGENT_METHODS.authenticate,
1104 rpc[AcpRpcs.V1_AGENT_METHODS.authenticate](payload),
1105 )
1106 : callRpc(AGENT_METHODS.auth_login, rpc[AGENT_METHODS.auth_login](payload)),
1107 logout: (payload) =>
1108 negotiatedProtocolGeneration === 1
1109 ? callRpc(AcpRpcs.V1_AGENT_METHODS.logout, rpc[AcpRpcs.V1_AGENT_METHODS.logout](payload))
1110 : callRpc(AGENT_METHODS.auth_logout, rpc[AGENT_METHODS.auth_logout](payload)),
1111 createSession: (payload) =>
1112 negotiatedProtocolGeneration === 1
1113 ? callRpc(
1114 AGENT_METHODS.session_new,
1115 rpc[AGENT_METHODS.session_new](payload as AcpSchemaV1.NewSessionRequest),
1116 ).pipe(Effect.map((response) => response as AcpSchema.NewSessionResponse))
1117 : callRpc(
1118 AGENT_METHODS.session_new,
1119 rpc[AGENT_METHODS.session_new]({
1120 ...payload,
1121 mcpServers: payload.mcpServers.map(toV2McpServer),
1122 }),
1123 ).pipe(
1124 Effect.map(
1125 (response) =>
1126 normalizeV2SessionSetupResponse(
1127 response as AcpSchemaV2.NewSessionResponse,
1128 ) as AcpSchema.NewSessionResponse,
1129 ),
1130 ),
1131 loadSession: (payload) =>
1132 negotiatedProtocolGeneration === 1
1133 ? callRpc(
1134 AcpRpcs.V1_AGENT_METHODS.session_load,
1135 rpc[AcpRpcs.V1_AGENT_METHODS.session_load](payload as AcpSchemaV1.LoadSessionRequest),
1136 ).pipe(Effect.map((response) => response as AcpSchema.LoadSessionResponse))
1137 : callRpc(
1138 AGENT_METHODS.session_resume,
1139 rpc[AGENT_METHODS.session_resume](toV2ResumeRequest(payload, { type: "start" })),
1140 ).pipe(
1141 Effect.map(
1142 (response) =>
1143 normalizeV2SessionSetupResponse(
1144 response as AcpSchemaV2.ResumeSessionResponse,
1145 ) as AcpSchema.LoadSessionResponse,
1146 ),
1147 ),
1148 listSessions: (payload) =>
1149 callRpc(AGENT_METHODS.session_list, rpc[AGENT_METHODS.session_list](payload)).pipe(
1150 Effect.map((response) => response as AcpSchema.ListSessionsResponse),
1151 ),
1152 forkSession: (payload) =>
1153 negotiatedProtocolGeneration === 1
1154 ? callRpc(
1155 AGENT_METHODS.session_fork,
1156 rpc[AGENT_METHODS.session_fork](payload as AcpSchemaV1.ForkSessionRequest),
1157 ).pipe(Effect.map((response) => response as AcpSchema.ForkSessionResponse))
1158 : callRpc(
1159 AGENT_METHODS.session_fork,
1160 rpc[AGENT_METHODS.session_fork](toV2ForkRequest(payload)),
1161 ).pipe(
1162 Effect.map(
1163 (response) =>
1164 normalizeV2SessionSetupResponse(
1165 response as AcpSchemaV2.ForkSessionResponse,
1166 ) as AcpSchema.ForkSessionResponse,
1167 ),
1168 ),
1169 resumeSession: (payload) =>
1170 negotiatedProtocolGeneration === 1
1171 ? callRpc(
1172 AGENT_METHODS.session_resume,
1173 rpc[AGENT_METHODS.session_resume](payload as AcpSchemaV1.ResumeSessionRequest),
1174 ).pipe(Effect.map((response) => response as AcpSchema.ResumeSessionResponse))
1175 : callRpc(
1176 AGENT_METHODS.session_resume,
1177 rpc[AGENT_METHODS.session_resume](
1178 toV2ResumeRequest(payload, payload.replayFrom ?? undefined),
1179 ),
1180 ).pipe(
1181 Effect.map(
1182 (response) =>
1183 normalizeV2SessionSetupResponse(
1184 response as AcpSchemaV2.ResumeSessionResponse,
1185 ) as AcpSchema.ResumeSessionResponse,
1186 ),
1187 ),
1188 closeSession: (payload) =>
1189 callRpc(AGENT_METHODS.session_close, rpc[AGENT_METHODS.session_close](payload)).pipe(
1190 Effect.map((response) => response as AcpSchema.CloseSessionResponse),
1191 ),
1192 deleteSession: (payload) =>
1193 Effect.suspend(() =>
1194 sessionDeleteSupported
1195 ? callRpc(AGENT_METHODS.session_delete, rpc[AGENT_METHODS.session_delete](payload))
1196 : Effect.fail(AcpError.AcpRequestError.methodNotFound(AGENT_METHODS.session_delete)),
1197 ),
1198 listProviders: (payload) =>
1199 negotiatedProtocolGeneration === 1
1200 ? Effect.fail(AcpError.AcpRequestError.methodNotFound(AGENT_METHODS.providers_list))
1201 : callRpc(AGENT_METHODS.providers_list, rpc[AGENT_METHODS.providers_list](payload)),
1202 setProvider: (payload) =>
1203 negotiatedProtocolGeneration === 1
1204 ? Effect.fail(AcpError.AcpRequestError.methodNotFound(AGENT_METHODS.providers_set))
1205 : callRpc(AGENT_METHODS.providers_set, rpc[AGENT_METHODS.providers_set](payload)),
1206 disableProvider: (payload) =>
1207 negotiatedProtocolGeneration === 1
1208 ? Effect.fail(AcpError.AcpRequestError.methodNotFound(AGENT_METHODS.providers_disable))
1209 : callRpc(AGENT_METHODS.providers_disable, rpc[AGENT_METHODS.providers_disable](payload)),
1210 setSessionModel: (payload) =>
1211 negotiatedProtocolGeneration === 1
1212 ? callRpc(
1213 AcpRpcs.V1_AGENT_METHODS.session_set_model,
1214 rpc[AcpRpcs.V1_AGENT_METHODS.session_set_model](payload),
1215 )
1216 : Effect.fail(
1217 AcpError.AcpRequestError.methodNotFound(AcpRpcs.V1_AGENT_METHODS.session_set_model),
1218 ),
1219 setSessionMode: (payload) =>
1220 negotiatedProtocolGeneration === 1
1221 ? callRpc(
1222 AcpRpcs.V1_AGENT_METHODS.session_set_mode,
1223 rpc[AcpRpcs.V1_AGENT_METHODS.session_set_mode](payload),
1224 )
1225 : Effect.fail(
1226 AcpError.AcpRequestError.methodNotFound(AcpRpcs.V1_AGENT_METHODS.session_set_mode),
1227 ),
1228 setSessionConfigOption: (payload) =>
1229 negotiatedProtocolGeneration === 1
1230 ? callRpc(
1231 AGENT_METHODS.session_set_config_option,
1232 rpc[AGENT_METHODS.session_set_config_option](
1233 payload as AcpSchemaV1.SetSessionConfigOptionRequest,
1234 ),
1235 ).pipe(Effect.map((response) => response as AcpSchema.SetSessionConfigOptionResponse))
1236 : callRpc(
1237 AGENT_METHODS.session_set_config_option,
1238 rpc[AGENT_METHODS.session_set_config_option]({
1239 ...payload,
1240 type: "type" in payload && payload.type === "boolean" ? "boolean" : "id",
1241 }),
1242 ).pipe(
1243 Effect.map((response) => ({
1244 configOptions:
1245 normalizeConfigOptions(
1246 (response as AcpSchemaV2.SetSessionConfigOptionResponse).configOptions,
1247 ) ?? [],
1248 })),
1249 ),
1250 prompt: (payload) =>
1251 negotiatedProtocolGeneration === 1
1252 ? callRpc(
1253 AGENT_METHODS.session_prompt,
1254 rpc[AGENT_METHODS.session_prompt](payload as AcpSchemaV1.PromptRequest),
1255 ).pipe(Effect.map((response) => response as AcpSchema.PromptResponse))
1256 : Effect.gen(function* () {
1257 const existing = promptCompletions.get(payload.sessionId);
1258 if (existing !== undefined) {
1259 return yield* AcpError.AcpRequestError.internalError(
1260 `ACP session '${payload.sessionId}' already has an active prompt.`,
1261 );
1262 }
1263 const completion = yield* Deferred.make<
1264 AcpSchema.PromptResponse,
1265 AcpError.AcpError
1266 >();
1267 promptCompletions.set(payload.sessionId, completion);
1268 return yield* callRpc(
1269 AGENT_METHODS.session_prompt,
1270 rpc[AGENT_METHODS.session_prompt](payload as AcpSchemaV2.PromptRequest),
1271 ).pipe(
1272 Effect.andThen(Deferred.await(completion)),
1273 Effect.ensuring(
1274 Effect.sync(() => {
1275 if (promptCompletions.get(payload.sessionId) === completion) {
1276 promptCompletions.delete(payload.sessionId);
1277 }
1278 }),
1279 ),
1280 );
1281 }),
1282 cancel: (payload) => transport.notify(AGENT_METHODS.session_cancel, payload),
1283 },
1284 handleRequestPermission: Effect.fn("AcpClient.handleRequestPermission")(function (handler) {
1285 return Effect.suspend(() => {
1286 coreHandlers.requestPermission = handler;
1287 return Effect.void;
1288 });
1289 }),
1290 handleElicitation: Effect.fn("AcpClient.handleElicitation")(function (handler) {
1291 return Effect.suspend(() => {
1292 coreHandlers.elicitation = handler;
1293 return Effect.void;
1294 });
1295 }),
1296 handleMcpConnect: Effect.fn("AcpClient.handleMcpConnect")(function (handler) {
1297 return Effect.sync(() => {
1298 coreHandlers.mcpConnect = handler;
1299 });
1300 }),
1301 handleMcpMessage: Effect.fn("AcpClient.handleMcpMessage")(function (handler) {
1302 return Effect.sync(() => {
1303 coreHandlers.mcpMessage = handler;
1304 });
1305 }),
1306 handleMcpDisconnect: Effect.fn("AcpClient.handleMcpDisconnect")(function (handler) {
1307 return Effect.sync(() => {
1308 coreHandlers.mcpDisconnect = handler;
1309 });
1310 }),
1311 handleMcpNotification: Effect.fn("AcpClient.handleMcpNotification")(function (handler) {
1312 return Effect.sync(() => {
1313 extNotificationHandlers.set(CLIENT_METHODS.mcp_message, (params) =>
1314 decodeMessageMcpNotification(params).pipe(
1315 Effect.mapError(() => AcpError.AcpRequestError.invalidParams()),
1316 Effect.flatMap(handler),
1317 ),
1318 );
1319 });
1320 }),
1321 handleReadTextFile: Effect.fn("AcpClient.handleReadTextFile")(function (handler) {
1322 return Effect.sync(() => {
1323 coreHandlers.readTextFile = handler;
1324 });
1325 }),
1326 handleWriteTextFile: Effect.fn("AcpClient.handleWriteTextFile")(function (handler) {
1327 return Effect.sync(() => {
1328 coreHandlers.writeTextFile = handler;
1329 });
1330 }),
1331 handleCreateTerminal: Effect.fn("AcpClient.handleCreateTerminal")(function (handler) {
1332 return Effect.sync(() => {
1333 coreHandlers.createTerminal = handler;
1334 });
1335 }),
1336 handleTerminalOutput: Effect.fn("AcpClient.handleTerminalOutput")(function (handler) {
1337 return Effect.sync(() => {
1338 coreHandlers.terminalOutput = handler;
1339 });
1340 }),
1341 handleTerminalWaitForExit: Effect.fn("AcpClient.handleTerminalWaitForExit")(function (handler) {
1342 return Effect.sync(() => {
1343 coreHandlers.terminalWaitForExit = handler;
1344 });
1345 }),
1346 handleTerminalKill: Effect.fn("AcpClient.handleTerminalKill")(function (handler) {
1347 return Effect.sync(() => {
1348 coreHandlers.terminalKill = handler;
1349 });
1350 }),
1351 handleTerminalRelease: Effect.fn("AcpClient.handleTerminalRelease")(function (handler) {
1352 return Effect.sync(() => {
1353 coreHandlers.terminalRelease = handler;
1354 });
1355 }),
1356 handleSessionUpdate: Effect.fn("AcpClient.handleSessionUpdate")(function (handler) {
1357 return Effect.suspend(() => {
1358 notificationHandlers.sessionUpdate.handlers.push(handler);
1359 return flushBufferedNotifications(notificationHandlers.sessionUpdate);
1360 });
1361 }),
1362 handleElicitationComplete: Effect.fn("AcpClient.handleElicitationComplete")(function (handler) {
1363 return Effect.suspend(() => {
1364 notificationHandlers.elicitationComplete.handlers.push(handler);
1365 return flushBufferedNotifications(notificationHandlers.elicitationComplete);
1366 });
1367 }),
1368 handleUnknownExtRequest: Effect.fn("AcpClient.handleUnknownExtRequest")(function (handler) {
1369 return Effect.suspend(() => {
1370 unknownExtRequestHandler = handler;
1371 return Effect.void;
1372 });
1373 }),
1374 handleUnknownExtNotification: Effect.fn("AcpClient.handleUnknownExtNotification")(
1375 function (handler) {
1376 return Effect.suspend(() => {
1377 unknownExtNotificationHandler = handler;
1378 return Effect.void;
1379 });
1380 },
1381 ),
1382 handleExtRequest: Effect.fn("AcpClient.handleExtRequest")(function (method, payload, handler) {
1383 return Effect.suspend(() => {
1384 extRequestHandlers.set(method, decodeExtRequestRegistration(method, payload, handler));
1385 return Effect.void;
1386 });
1387 }),
1388 handleExtNotification: Effect.fn("AcpClient.handleExtNotification")(
1389 function (method, payload, handler) {
1390 return Effect.suspend(() => {
1391 extNotificationHandlers.set(
1392 method,
1393 decodeExtNotificationRegistration(method, payload, handler),
1394 );
1395 return Effect.void;
1396 });
1397 },
1398 ),
1399 });
1400});
1401
1402export const layerChildProcess = (
1403 handle: ChildProcessSpawner.ChildProcessHandle,
1404 options: AcpClientOptions = {},
1405): Layer.Layer<AcpClient> => {
1406 const stdio = {
1407 ...makeChildStdio(handle),
1408 stdin: options.transformStdout?.(handle.stdout) ?? handle.stdout,
1409 };
1410 const terminationError = makeTerminationError(handle);
1411 return Layer.effect(AcpClient, make(stdio, options, terminationError));
1412};