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