Skip to content
Source
Sign inSign up

client.ts

1,421 lines54.7 KB

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;
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 >;
276​
277interface AcpNotificationHandlers {
278 readonly sessionUpdate: BufferedNotificationHandler<AcpSchema.SessionNotification>;
279 readonly elicitationComplete: BufferedNotificationHandler<AcpSchema.CompleteElicitationNotification>;
281​
282interface BufferedNotificationHandler<A> {
283 readonly handlers: Array<(notification: A) => Effect.Effect<void, AcpError.AcpError>>;
284 readonly pending: Array<A>;
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 }
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" }>;
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 };
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;
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 });
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 }
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 const update = notification.update;
532 // v1 defines a null tool name as unchanged; v2 consumers read null as a clear.
533 if (
534 (update.sessionUpdate === "tool_call" || update.sessionUpdate === "tool_call_update") &&
535 update.name === null
536 ) {
537 const { name: _name, ...rest } = update;
538 return { ...notification, update: rest } as unknown as AcpSchema.SessionNotification;
539 }
540 if (notification.update.sessionUpdate !== "available_commands_update") {
541 return notification as unknown as AcpSchema.SessionNotification;
542 }
543 return {
544 ...notification,
545 update: {
546 ...notification.update,
547 availableCommands: notification.update.availableCommands.map((command) => {
548 const { input, ...rest } = command;
549 return {
550 ...rest,
551 ...(input == null ? {} : { input: { type: "text" as const, hint: input.hint } }),
552 };
553 }),
554 },
555 };
557​
558function toV2McpServer(server: AcpSchema.McpServer): AcpSchemaV2.McpServer {
559 if ("type" in server) return server;
560 return {
561 type: "stdio",
562 name: server.name,
563 command: server.command,
564 ...(server.args === undefined ? {} : { args: server.args }),
565 ...(server.env === undefined ? {} : { env: server.env }),
566 ...(server._meta === undefined ? {} : { _meta: server._meta }),
567 };
569​
570function toV2ResumeRequest(
571 request: AcpSchema.ResumeSessionRequest,
572 replayFrom?: AcpSchemaV2.ReplayFrom,
573): AcpSchemaV2.ResumeSessionRequest {
574 const { mcpServers, ...rest } = request;
575 return {
576 ...rest,
577 ...(mcpServers === undefined ? {} : { mcpServers: mcpServers.map(toV2McpServer) }),
578 ...(replayFrom === undefined ? {} : { replayFrom }),
579 };
581​
582function toV2ForkRequest(request: AcpSchema.ForkSessionRequest): AcpSchemaV2.ForkSessionRequest {
583 const { mcpServers, ...rest } = request;
584 return {
585 ...rest,
586 ...(mcpServers === undefined ? {} : { mcpServers: mcpServers.map(toV2McpServer) }),
587 };
589​
590function normalizeInitializeResponse(
591 response: AcpSchemaV2.InitializeResponse,
592): AcpSchema.InitializeResponse {
593 const session = response.capabilities?.session;
594 const prompt = session?.prompt;
595 const mcp = session?.mcp;
596 return {
597 protocolVersion: response.protocolVersion,
598 agentInfo: response.info,
599 agentCapabilities: {
600 loadSession: session !== undefined && session !== null,
601 promptCapabilities: {
602 image: prompt?.image != null,
603 audio: prompt?.audio != null,
604 embeddedContext: prompt?.embeddedContext != null,
605 },
606 mcpCapabilities: {
607 stdio: mcp?.stdio != null,
608 http: mcp?.http != null,
609 acp: mcp?.acp != null,
610 },
611 ...(session == null
612 ? {}
613 : {
614 sessionCapabilities: {
615 list: {},
616 resume: {},
617 close: {},
618 ...(session.delete == null ? {} : { delete: {} }),
619 ...(session.fork == null ? {} : { fork: {} }),
620 ...(session.additionalDirectories == null ? {} : { additionalDirectories: {} }),
621 },
622 }),
623 ...(response.capabilities?.providers == null ? {} : { providers: {} }),
624 ...(response.authMethods === undefined || response.authMethods.length === 0
625 ? {}
626 : { auth: { logout: {} } }),
627 },
628 ...(response.authMethods === undefined
629 ? {}
630 : {
631 authMethods: response.authMethods
632 .map((method) => {
633 const record = method as AcpSchemaV2.AuthMethod;
634 const base = {
635 id: record.methodId,
636 name: record.name,
637 ...(record.description === undefined ? {} : { description: record.description }),
638 ...(record._meta === undefined ? {} : { _meta: record._meta }),
639 };
640 if (
641 record.type === "env_var" &&
642 Array.isArray(record.vars) &&
643 record.vars.every(
644 (variable) =>
645 typeof variable === "object" &&
646 variable !== null &&
647 typeof (variable as { readonly name?: unknown }).name === "string",
648 )
649 ) {
650 return {
651 ...base,
652 type: "env_var" as const,
653 vars: record.vars.map((variable) => {
654 const value = variable as { readonly name: string; readonly label?: unknown };
655 return {
656 name: value.name,
657 ...(typeof value.label === "string" ? { label: value.label } : {}),
658 };
659 }),
660 ...(typeof record.link === "string" ? { link: record.link } : {}),
661 };
662 }
663 if (record.type === "agent") return { ...base, type: "agent" as const };
664 if (record.type !== "terminal") return undefined;
665 return {
666 ...base,
667 type: "terminal" as const,
668 ...(Array.isArray(record.args)
669 ? {
670 args: record.args.filter(
671 (argument): argument is string => typeof argument === "string",
672 ),
673 }
674 : {}),
675 ...(Array.isArray(record.env)
676 ? {
677 env: Object.fromEntries(
678 (record.env as ReadonlyArray<AcpSchemaV2.EnvVariable>).map((variable) => [
679 variable.name,
680 variable.value,
681 ]),
682 ),
683 }
684 : {}),
685 };
686 })
687 .filter(Predicate.isNotUndefined),
688 }),
689 ...(response._meta === undefined ? {} : { _meta: response._meta }),
690 };
692​
693function toV2InitializeRequest(
694 request: AcpSchema.InitializeRequest,
695): AcpSchemaV2.InitializeRequest {
696 const capabilities = request.clientCapabilities;
697 return {
698 protocolVersion: 2,
699 info: request.clientInfo ?? { name: "t3-code", version: "unknown" },
700 capabilities: {
701 ...(capabilities?.auth?.terminal === true ? { auth: { terminal: {} } } : {}),
702 ...(capabilities?.elicitation == null ? {} : { elicitation: capabilities.elicitation }),
703 ...(capabilities?._meta === undefined ? {} : { _meta: capabilities._meta }),
704 },
705 ...(request._meta === undefined ? {} : { _meta: request._meta }),
706 };
708​
709function toNegotiatingInitializeRequest(
710 request: AcpSchema.InitializeRequest,
711): AcpSchemaV1.InitializeRequest | AcpSchemaV2.InitializeRequest {
712 return {
713 ...toV2InitializeRequest(request),
714 clientCapabilities: request.clientCapabilities ?? {},
715 ...(request.clientInfo === undefined ? {} : { clientInfo: request.clientInfo }),
716 } as unknown as AcpSchemaV1.InitializeRequest | AcpSchemaV2.InitializeRequest;
718​
719function isV2InitializeResponse(
720 response: AcpSchemaV1.InitializeResponse | AcpSchemaV2.InitializeResponse,
721): response is AcpSchemaV2.InitializeResponse {
722 return "info" in response;
724​
725function normalizeV2SessionSetupResponse(
726 response:
727 | AcpSchemaV2.NewSessionResponse
728 | AcpSchemaV2.ResumeSessionResponse
729 | AcpSchemaV2.ForkSessionResponse,
730) {
731 const configOptions = normalizeConfigOptions(response.configOptions);
732 return {
733 ...("sessionId" in response ? { sessionId: response.sessionId } : {}),
734 ...(configOptions === undefined ? {} : { configOptions }),
735 ...(response._meta === undefined ? {} : { _meta: response._meta }),
736 };
738​
739function normalizePermissionRequest(
740 request: AcpSchemaV2.RequestPermissionRequest,
741 context: AcpProtocol.AcpRequestContext,
742): AcpSchema.RequestPermissionRequest {
743 const subject = request.subject;
744 const toolCall =
745 subject?.type === "tool_call" && "toolCall" in subject
746 ? normalizeToolCallUpdate(subject.toolCall as AcpSchemaV2.ToolCallUpdate)
747 : {
748 toolCallId:
749 subject?.type === "command" &&
750 "toolCallId" in subject &&
751 typeof subject.toolCallId === "string"
752 ? subject.toolCallId
753 : context.requestId,
754 title: request.title,
755 kind: subject?.type === "command" ? ("execute" as const) : ("other" as const),
756 };
757 return {
758 sessionId: request.sessionId,
759 title: request.title,
760 ...(request.description === undefined ? {} : { description: request.description }),
761 ...(subject === undefined ? {} : { subject }),
762 toolCall,
763 options: request.options,
764 ...(request._meta === undefined ? {} : { _meta: request._meta }),
765 };
767const decodeMessageMcpNotification = Schema.decodeUnknownEffect(AcpSchemaV2.MessageMcpNotification);
768​
769export const make = Effect.fn("effect-acp/AcpClient.make")(function* (
770 stdio: AcpProtocol.AcpStdio,
771 options: AcpClientOptions = {},
772 terminationError?: Effect.Effect<AcpError.AcpError>,
773): Effect.fn.Return<AcpClient["Service"], never, Scope.Scope> {
774 const coreHandlers: AcpCoreRequestHandlers = {};
775 const notificationHandlers: AcpNotificationHandlers = {
776 sessionUpdate: { handlers: [], pending: [] },
777 elicitationComplete: { handlers: [], pending: [] },
778 };
779 const promptCompletions = new Map<
780 string,
781 Deferred.Deferred<AcpSchema.PromptResponse, AcpError.AcpError>
782 >();
783 const extRequestHandlers = new Map<string, AcpRequestHandler<unknown, unknown>>();
784 const extNotificationHandlers = new Map<
785 string,
786 (params: unknown) => Effect.Effect<void, AcpError.AcpError>
787 >();
788 let unknownExtRequestHandler:
789 | ((
790 method: string,
791 params: unknown,
792 context: AcpProtocol.AcpRequestContext,
793 ) => Effect.Effect<unknown, AcpError.AcpError>)
794 | undefined;
795 let unknownExtNotificationHandler:
796 | ((method: string, params: unknown) => Effect.Effect<void, AcpError.AcpError>)
797 | undefined;
798 let negotiatedProtocolGeneration: 1 | 2 | undefined;
799 let sessionDeleteSupported = false;
800​
801 const runNotificationHandlers = <A>(
802 registration: BufferedNotificationHandler<A>,
803 notification: A,
804 ) =>
805 Effect.forEach(
806 registration.handlers,
807 (handler) => isolateNotificationHandler(handler(notification)),
808 { discard: true },
809 );
810​
811 const flushBufferedNotifications = <A>(registration: BufferedNotificationHandler<A>) =>
812 Effect.suspend(() => {
813 if (registration.handlers.length === 0 || registration.pending.length === 0) {
814 return Effect.void;
815 }
816 const pending = registration.pending.splice(0, registration.pending.length);
817 return Effect.forEach(
818 pending,
819 (notification) => runNotificationHandlers(registration, notification),
820 {
821 discard: true,
822 },
823 );
824 });
825​
826 const dispatchNotification = (notification: AcpProtocol.AcpIncomingNotification) => {
827 switch (notification._tag) {
828 case "SessionUpdate": {
829 const normalized = notification.params as AcpSchema.SessionNotification;
830 const state = normalized.update;
831 const completePrompt =
832 state.sessionUpdate === "state_update" && state.state === "idle"
833 ? Effect.suspend(() => {
834 const idle = state as Extract<
835 AcpSchema.SessionUpdate,
836 { readonly sessionUpdate: "state_update"; readonly state: "idle" }
837 >;
838 const completion = promptCompletions.get(normalized.sessionId);
839 if (completion === undefined) return Effect.void;
840 promptCompletions.delete(normalized.sessionId);
841 return Deferred.succeed(completion, {
842 stopReason: idle.stopReason ?? "end_turn",
843 ...(idle.usage === undefined ? {} : { usage: idle.usage }),
844 ...(idle.error === undefined ? {} : { error: idle.error }),
845 ...(idle._meta === undefined ? {} : { _meta: idle._meta }),
846 }).pipe(Effect.asVoid);
847 })
848 : Effect.void;
849 if (notificationHandlers.sessionUpdate.handlers.length === 0) {
850 notificationHandlers.sessionUpdate.pending.push(normalized);
851 return completePrompt;
852 }
853 return Effect.all(
854 [completePrompt, runNotificationHandlers(notificationHandlers.sessionUpdate, normalized)],
855 { discard: true },
856 );
857 }
858 case "ElicitationComplete": {
859 if (notificationHandlers.elicitationComplete.handlers.length === 0) {
860 notificationHandlers.elicitationComplete.pending.push(notification.params);
861 return Effect.void;
862 }
863 return runNotificationHandlers(
864 notificationHandlers.elicitationComplete,
865 notification.params,
866 );
867 }
868 case "ExtNotification": {
869 const handler = extNotificationHandlers.get(notification.method);
870 if (handler) {
871 return handler(notification.params);
872 }
873 return unknownExtNotificationHandler
874 ? unknownExtNotificationHandler(notification.method, notification.params)
875 : Effect.void;
876 }
877 }
878 };
879​
880 const dispatchExtRequest = (
881 method: string,
882 params: unknown,
883 context: AcpProtocol.AcpRequestContext,
884 ) => {
885 const handler = extRequestHandlers.get(method);
886 if (handler) {
887 return handler(params, context);
888 }
889 return unknownExtRequestHandler
890 ? unknownExtRequestHandler(method, params, context)
891 : Effect.fail(AcpError.AcpRequestError.methodNotFound(method));
892 };
893​
894 const transport = yield* AcpProtocol.makeAcpPatchedProtocol({
895 stdio: stdio,
896 ...(terminationError ? { terminationError } : {}),
897 serverRequestMethods: new Set(AcpRpcs.CompatClientRpcs.requests.keys()),
898 ...(options.logIncoming !== undefined ? { logIncoming: options.logIncoming } : {}),
899 ...(options.logOutgoing !== undefined ? { logOutgoing: options.logOutgoing } : {}),
900 ...(options.logger ? { logger: options.logger } : {}),
901 ...(options.onIncomingRequest ? { onIncomingRequest: options.onIncomingRequest } : {}),
902 onTermination: (error) =>
903 Effect.all(
904 [
905 ...Array.from(promptCompletions.values(), (completion) =>
906 Deferred.fail(completion, error).pipe(Effect.asVoid),
907 ),
908 options.onTermination?.(error) ?? Effect.void,
909 ],
910 { discard: true },
911 ).pipe(
912 Effect.tap(() =>
913 Effect.sync(() => {
914 promptCompletions.clear();
915 }),
916 ),
917 ),
918 ...(options.onOutgoingResponseFailure
919 ? { onOutgoingResponseFailure: options.onOutgoingResponseFailure }
920 : {}),
921 ...(options.onOutgoingResponse ? { onOutgoingResponse: options.onOutgoingResponse } : {}),
922 transformSessionUpdate: (notification) => {
923 const normalized =
924 negotiatedProtocolGeneration === 1
925 ? normalizeV1SessionUpdate(notification as AcpSchemaV1.SessionNotification)
926 : normalizeSessionUpdate(notification as AcpSchemaV2.UpdateSessionNotification);
927 return options.transformSessionUpdate?.(normalized) ?? normalized;
928 },
929 onNotification: dispatchNotification,
930 onExtRequest: dispatchExtRequest,
931 });
932​
933 const requestContext = (
934 requestId: RpcMessage.RequestId,
935 method: string,
936 ): AcpProtocol.AcpRequestContext => ({
937 requestId: AcpProtocol.acpRequestIdentity(requestId),
938 method,
939 });
940​
941 const layerClientHandler = AcpRpcs.CompatClientRpcs.toLayer(
942 AcpRpcs.CompatClientRpcs.of({
943 [CLIENT_METHODS.session_request_permission]: (payload, { requestId }) =>
944 runHandler(
945 coreHandlers.requestPermission,
946 "toolCall" in payload
947 ? (payload as AcpSchema.RequestPermissionRequest)
948 : normalizePermissionRequest(
949 payload,
950 requestContext(requestId, CLIENT_METHODS.session_request_permission),
951 ),
952 CLIENT_METHODS.session_request_permission,
953 requestContext(requestId, CLIENT_METHODS.session_request_permission),
954 ),
955 [AcpRpcs.V1_CLIENT_METHODS.session_elicitation]: (payload, { requestId }) =>
956 runHandler(
957 coreHandlers.elicitation,
958 payload,
959 AcpRpcs.V1_CLIENT_METHODS.session_elicitation,
960 requestContext(requestId, AcpRpcs.V1_CLIENT_METHODS.session_elicitation),
961 ).pipe(
962 Effect.map(({ _meta, ...action }) => ({
963 action,
964 ...(_meta === undefined ? {} : { _meta }),
965 })),
966 ),
967 [AcpRpcs.V1_CLIENT_METHODS.unstable_session_elicitation]: (payload, { requestId }) =>
968 runHandler(
969 coreHandlers.elicitation,
970 payload,
971 AcpRpcs.V1_CLIENT_METHODS.unstable_session_elicitation,
972 requestContext(requestId, AcpRpcs.V1_CLIENT_METHODS.unstable_session_elicitation),
973 ).pipe(
974 Effect.map(({ _meta, ...action }) => ({
975 action,
976 ...(_meta === undefined ? {} : { _meta }),
977 })),
978 ),
979 [CLIENT_METHODS.elicitation_create]: (payload, { requestId }) =>
980 decodeElicitationRequest(payload).pipe(
981 Effect.mapError((cause) =>
982 AcpError.AcpRequestError.invalidExtensionPayload(
983 CLIENT_METHODS.elicitation_create,
984 cause,
985 ).toProtocolError(),
986 ),
987 Effect.flatMap((request) =>
988 runHandler(
989 coreHandlers.elicitation,
990 request,
991 CLIENT_METHODS.elicitation_create,
992 requestContext(requestId, CLIENT_METHODS.elicitation_create),
993 ),
994 ),
995 ),
996 [CLIENT_METHODS.mcp_connect]: (payload, { requestId }) =>
997 runHandler(
998 coreHandlers.mcpConnect,
999 payload,
1000 CLIENT_METHODS.mcp_connect,
1001 requestContext(requestId, CLIENT_METHODS.mcp_connect),
1002 ),
1003 [CLIENT_METHODS.mcp_message]: (payload, { requestId }) =>
1004 runHandler(
1005 coreHandlers.mcpMessage,
1006 payload,
1007 CLIENT_METHODS.mcp_message,
1008 requestContext(requestId, CLIENT_METHODS.mcp_message),
1009 ),
1010 [CLIENT_METHODS.mcp_disconnect]: (payload, { requestId }) =>
1011 runHandler(
1012 coreHandlers.mcpDisconnect,
1013 payload,
1014 CLIENT_METHODS.mcp_disconnect,
1015 requestContext(requestId, CLIENT_METHODS.mcp_disconnect),
1016 ),
1017 [AcpRpcs.V1_CLIENT_METHODS.fs_read_text_file]: (payload, { requestId }) =>
1018 runHandler(
1019 coreHandlers.readTextFile,
1020 payload,
1021 AcpRpcs.V1_CLIENT_METHODS.fs_read_text_file,
1022 requestContext(requestId, AcpRpcs.V1_CLIENT_METHODS.fs_read_text_file),
1023 ),
1024 [AcpRpcs.V1_CLIENT_METHODS.fs_write_text_file]: (payload, { requestId }) =>
1025 runHandler(
1026 coreHandlers.writeTextFile,
1027 payload,
1028 AcpRpcs.V1_CLIENT_METHODS.fs_write_text_file,
1029 requestContext(requestId, AcpRpcs.V1_CLIENT_METHODS.fs_write_text_file),
1030 ).pipe(Effect.map((result) => result ?? {})),
1031 [AcpRpcs.V1_CLIENT_METHODS.terminal_create]: (payload, { requestId }) =>
1032 runHandler(
1033 coreHandlers.createTerminal,
1034 payload,
1035 AcpRpcs.V1_CLIENT_METHODS.terminal_create,
1036 requestContext(requestId, AcpRpcs.V1_CLIENT_METHODS.terminal_create),
1037 ),
1038 [AcpRpcs.V1_CLIENT_METHODS.terminal_output]: (payload, { requestId }) =>
1039 runHandler(
1040 coreHandlers.terminalOutput,
1041 payload,
1042 AcpRpcs.V1_CLIENT_METHODS.terminal_output,
1043 requestContext(requestId, AcpRpcs.V1_CLIENT_METHODS.terminal_output),
1044 ),
1045 [AcpRpcs.V1_CLIENT_METHODS.terminal_wait_for_exit]: (payload, { requestId }) =>
1046 runHandler(
1047 coreHandlers.terminalWaitForExit,
1048 payload,
1049 AcpRpcs.V1_CLIENT_METHODS.terminal_wait_for_exit,
1050 requestContext(requestId, AcpRpcs.V1_CLIENT_METHODS.terminal_wait_for_exit),
1051 ),
1052 [AcpRpcs.V1_CLIENT_METHODS.terminal_kill]: (payload, { requestId }) =>
1053 runHandler(
1054 coreHandlers.terminalKill,
1055 payload,
1056 AcpRpcs.V1_CLIENT_METHODS.terminal_kill,
1057 requestContext(requestId, AcpRpcs.V1_CLIENT_METHODS.terminal_kill),
1058 ).pipe(Effect.map((result) => result ?? {})),
1059 [AcpRpcs.V1_CLIENT_METHODS.terminal_release]: (payload, { requestId }) =>
1060 runHandler(
1061 coreHandlers.terminalRelease,
1062 payload,
1063 AcpRpcs.V1_CLIENT_METHODS.terminal_release,
1064 requestContext(requestId, AcpRpcs.V1_CLIENT_METHODS.terminal_release),
1065 ).pipe(Effect.map((result) => result ?? {})),
1066 }),
1067 );
1068​
1069 yield* RpcServer.make(AcpRpcs.CompatClientRpcs, { disableFatalDefects: true }).pipe(
1070 // runHandler logs handler defects with their method. A reporter inherited
1071 // from the caller (a WebSocket request, say) would log them again.
1072 Effect.provideService(ErrorReporter.CurrentErrorReporters, new Set()),
1073 Effect.provideService(RpcServer.Protocol, transport.serverProtocol),
1074 Effect.provide(layerClientHandler),
1075 Effect.forkScoped,
1076 );
1077​
1078 let nextRpcRequestId = AcpProtocol.RPC_REQUEST_ID_START;
1079 const rpc = yield* RpcClient.make(AcpRpcs.CompatAgentRpcs, {
1080 generateRequestId: () => RpcMessage.RequestId(nextRpcRequestId++),
1081 }).pipe(Effect.provideService(RpcClient.Protocol, transport.clientProtocol));
1082​
1083 return AcpClient.of({
1084 raw: {
1085 notifications: transport.incoming,
1086 request: transport.request,
1087 notify: transport.notify,
1088 },
1089 agent: {
1090 initialize: (payload) =>
1091 callRpc(
1092 AGENT_METHODS.initialize,
1093 rpc[AGENT_METHODS.initialize](toNegotiatingInitializeRequest(payload)),
1094 ).pipe(
1095 Effect.map((response) => {
1096 // ACP v1 protocolVersion values were not generation identifiers in
1097 // every shipped agent. Antigravity, for example, reports version 2
1098 // with the v1 agentInfo/agentCapabilities response shape.
1099 const isV2 = isV2InitializeResponse(response);
1100 negotiatedProtocolGeneration = isV2 ? 2 : 1;
1101 const normalized = isV2
1102 ? normalizeInitializeResponse(response)
1103 : (response as AcpSchema.InitializeResponse);
1104 sessionDeleteSupported =
1105 normalized.agentCapabilities?.sessionCapabilities?.delete != null;
1106 return normalized;
1107 }),
1108 ),
1109 authenticate: (payload) =>
1110 negotiatedProtocolGeneration === 1
1111 ? callRpc(
1112 AcpRpcs.V1_AGENT_METHODS.authenticate,
1113 rpc[AcpRpcs.V1_AGENT_METHODS.authenticate](payload),
1115 : callRpc(AGENT_METHODS.auth_login, rpc[AGENT_METHODS.auth_login](payload)),
1116 logout: (payload) =>
1117 negotiatedProtocolGeneration === 1
1118 ? callRpc(AcpRpcs.V1_AGENT_METHODS.logout, rpc[AcpRpcs.V1_AGENT_METHODS.logout](payload))
1119 : callRpc(AGENT_METHODS.auth_logout, rpc[AGENT_METHODS.auth_logout](payload)),
1120 createSession: (payload) =>
1121 negotiatedProtocolGeneration === 1
1122 ? callRpc(
1123 AGENT_METHODS.session_new,
1124 rpc[AGENT_METHODS.session_new](payload as AcpSchemaV1.NewSessionRequest),
1125 ).pipe(Effect.map((response) => response as AcpSchema.NewSessionResponse))
1126 : callRpc(
1127 AGENT_METHODS.session_new,
1128 rpc[AGENT_METHODS.session_new]({
1129 ...payload,
1130 mcpServers: payload.mcpServers.map(toV2McpServer),
1131 }),
1132 ).pipe(
1133 Effect.map(
1134 (response) =>
1135 normalizeV2SessionSetupResponse(
1136 response as AcpSchemaV2.NewSessionResponse,
1137 ) as AcpSchema.NewSessionResponse,
1138 ),
1139 ),
1140 loadSession: (payload) =>
1141 negotiatedProtocolGeneration === 1
1142 ? callRpc(
1143 AcpRpcs.V1_AGENT_METHODS.session_load,
1144 rpc[AcpRpcs.V1_AGENT_METHODS.session_load](payload as AcpSchemaV1.LoadSessionRequest),
1145 ).pipe(Effect.map((response) => response as AcpSchema.LoadSessionResponse))
1146 : callRpc(
1147 AGENT_METHODS.session_resume,
1148 rpc[AGENT_METHODS.session_resume](toV2ResumeRequest(payload, { type: "start" })),
1149 ).pipe(
1150 Effect.map(
1151 (response) =>
1152 normalizeV2SessionSetupResponse(
1153 response as AcpSchemaV2.ResumeSessionResponse,
1154 ) as AcpSchema.LoadSessionResponse,
1155 ),
1156 ),
1157 listSessions: (payload) =>
1158 callRpc(AGENT_METHODS.session_list, rpc[AGENT_METHODS.session_list](payload)).pipe(
1159 Effect.map((response) => response as AcpSchema.ListSessionsResponse),
1160 ),
1161 forkSession: (payload) =>
1162 negotiatedProtocolGeneration === 1
1163 ? callRpc(
1164 AGENT_METHODS.session_fork,
1165 rpc[AGENT_METHODS.session_fork](payload as AcpSchemaV1.ForkSessionRequest),
1166 ).pipe(Effect.map((response) => response as AcpSchema.ForkSessionResponse))
1167 : callRpc(
1168 AGENT_METHODS.session_fork,
1169 rpc[AGENT_METHODS.session_fork](toV2ForkRequest(payload)),
1170 ).pipe(
1171 Effect.map(
1172 (response) =>
1173 normalizeV2SessionSetupResponse(
1174 response as AcpSchemaV2.ForkSessionResponse,
1175 ) as AcpSchema.ForkSessionResponse,
1176 ),
1177 ),
1178 resumeSession: (payload) =>
1179 negotiatedProtocolGeneration === 1
1180 ? callRpc(
1181 AGENT_METHODS.session_resume,
1182 rpc[AGENT_METHODS.session_resume](payload as AcpSchemaV1.ResumeSessionRequest),
1183 ).pipe(Effect.map((response) => response as AcpSchema.ResumeSessionResponse))
1184 : callRpc(
1185 AGENT_METHODS.session_resume,
1186 rpc[AGENT_METHODS.session_resume](
1187 toV2ResumeRequest(payload, payload.replayFrom ?? undefined),
1188 ),
1189 ).pipe(
1190 Effect.map(
1191 (response) =>
1192 normalizeV2SessionSetupResponse(
1193 response as AcpSchemaV2.ResumeSessionResponse,
1194 ) as AcpSchema.ResumeSessionResponse,
1195 ),
1196 ),
1197 closeSession: (payload) =>
1198 callRpc(AGENT_METHODS.session_close, rpc[AGENT_METHODS.session_close](payload)).pipe(
1199 Effect.map((response) => response as AcpSchema.CloseSessionResponse),
1200 ),
1201 deleteSession: (payload) =>
1202 Effect.suspend(() =>
1203 sessionDeleteSupported
1204 ? callRpc(AGENT_METHODS.session_delete, rpc[AGENT_METHODS.session_delete](payload))
1205 : Effect.fail(AcpError.AcpRequestError.methodNotFound(AGENT_METHODS.session_delete)),
1206 ),
1207 listProviders: (payload) =>
1208 negotiatedProtocolGeneration === 1
1209 ? Effect.fail(AcpError.AcpRequestError.methodNotFound(AGENT_METHODS.providers_list))
1210 : callRpc(AGENT_METHODS.providers_list, rpc[AGENT_METHODS.providers_list](payload)),
1211 setProvider: (payload) =>
1212 negotiatedProtocolGeneration === 1
1213 ? Effect.fail(AcpError.AcpRequestError.methodNotFound(AGENT_METHODS.providers_set))
1214 : callRpc(AGENT_METHODS.providers_set, rpc[AGENT_METHODS.providers_set](payload)),
1215 disableProvider: (payload) =>
1216 negotiatedProtocolGeneration === 1
1217 ? Effect.fail(AcpError.AcpRequestError.methodNotFound(AGENT_METHODS.providers_disable))
1218 : callRpc(AGENT_METHODS.providers_disable, rpc[AGENT_METHODS.providers_disable](payload)),
1219 setSessionModel: (payload) =>
1220 negotiatedProtocolGeneration === 1
1221 ? callRpc(
1222 AcpRpcs.V1_AGENT_METHODS.session_set_model,
1223 rpc[AcpRpcs.V1_AGENT_METHODS.session_set_model](payload),
1225 : Effect.fail(
1226 AcpError.AcpRequestError.methodNotFound(AcpRpcs.V1_AGENT_METHODS.session_set_model),
1227 ),
1228 setSessionMode: (payload) =>
1229 negotiatedProtocolGeneration === 1
1230 ? callRpc(
1231 AcpRpcs.V1_AGENT_METHODS.session_set_mode,
1232 rpc[AcpRpcs.V1_AGENT_METHODS.session_set_mode](payload),
1234 : Effect.fail(
1235 AcpError.AcpRequestError.methodNotFound(AcpRpcs.V1_AGENT_METHODS.session_set_mode),
1236 ),
1237 setSessionConfigOption: (payload) =>
1238 negotiatedProtocolGeneration === 1
1239 ? callRpc(
1240 AGENT_METHODS.session_set_config_option,
1241 rpc[AGENT_METHODS.session_set_config_option](
1242 payload as AcpSchemaV1.SetSessionConfigOptionRequest,
1243 ),
1244 ).pipe(Effect.map((response) => response as AcpSchema.SetSessionConfigOptionResponse))
1245 : callRpc(
1246 AGENT_METHODS.session_set_config_option,
1247 rpc[AGENT_METHODS.session_set_config_option]({
1248 ...payload,
1249 type: "type" in payload && payload.type === "boolean" ? "boolean" : "id",
1250 }),
1251 ).pipe(
1252 Effect.map((response) => ({
1253 configOptions:
1254 normalizeConfigOptions(
1255 (response as AcpSchemaV2.SetSessionConfigOptionResponse).configOptions,
1256 ) ?? [],
1257 })),
1258 ),
1259 prompt: (payload) =>
1260 negotiatedProtocolGeneration === 1
1261 ? callRpc(
1262 AGENT_METHODS.session_prompt,
1263 rpc[AGENT_METHODS.session_prompt](payload as AcpSchemaV1.PromptRequest),
1264 ).pipe(Effect.map((response) => response as AcpSchema.PromptResponse))
1265 : Effect.gen(function* () {
1266 const existing = promptCompletions.get(payload.sessionId);
1267 if (existing !== undefined) {
1268 return yield* AcpError.AcpRequestError.internalError(
1269 `ACP session '${payload.sessionId}' already has an active prompt.`,
1270 );
1272 const completion = yield* Deferred.make<
1273 AcpSchema.PromptResponse,
1274 AcpError.AcpError
1275 >();
1276 promptCompletions.set(payload.sessionId, completion);
1277 return yield* callRpc(
1278 AGENT_METHODS.session_prompt,
1279 rpc[AGENT_METHODS.session_prompt](payload as AcpSchemaV2.PromptRequest),
1280 ).pipe(
1281 Effect.andThen(Deferred.await(completion)),
1282 Effect.ensuring(
1283 Effect.sync(() => {
1284 if (promptCompletions.get(payload.sessionId) === completion) {
1285 promptCompletions.delete(payload.sessionId);
1287 }),
1288 ),
1289 );
1290 }),
1291 cancel: (payload) => transport.notify(AGENT_METHODS.session_cancel, payload),
1292 },
1293 handleRequestPermission: Effect.fn("AcpClient.handleRequestPermission")(function (handler) {
1294 return Effect.suspend(() => {
1295 coreHandlers.requestPermission = handler;
1296 return Effect.void;
1297 });
1298 }),
1299 handleElicitation: Effect.fn("AcpClient.handleElicitation")(function (handler) {
1300 return Effect.suspend(() => {
1301 coreHandlers.elicitation = handler;
1302 return Effect.void;
1303 });
1304 }),
1305 handleMcpConnect: Effect.fn("AcpClient.handleMcpConnect")(function (handler) {
1306 return Effect.sync(() => {
1307 coreHandlers.mcpConnect = handler;
1308 });
1309 }),
1310 handleMcpMessage: Effect.fn("AcpClient.handleMcpMessage")(function (handler) {
1311 return Effect.sync(() => {
1312 coreHandlers.mcpMessage = handler;
1313 });
1314 }),
1315 handleMcpDisconnect: Effect.fn("AcpClient.handleMcpDisconnect")(function (handler) {
1316 return Effect.sync(() => {
1317 coreHandlers.mcpDisconnect = handler;
1318 });
1319 }),
1320 handleMcpNotification: Effect.fn("AcpClient.handleMcpNotification")(function (handler) {
1321 return Effect.sync(() => {
1322 extNotificationHandlers.set(CLIENT_METHODS.mcp_message, (params) =>
1323 decodeMessageMcpNotification(params).pipe(
1324 Effect.mapError(() => AcpError.AcpRequestError.invalidParams()),
1325 Effect.flatMap(handler),
1326 ),
1327 );
1328 });
1329 }),
1330 handleReadTextFile: Effect.fn("AcpClient.handleReadTextFile")(function (handler) {
1331 return Effect.sync(() => {
1332 coreHandlers.readTextFile = handler;
1333 });
1334 }),
1335 handleWriteTextFile: Effect.fn("AcpClient.handleWriteTextFile")(function (handler) {
1336 return Effect.sync(() => {
1337 coreHandlers.writeTextFile = handler;
1338 });
1339 }),
1340 handleCreateTerminal: Effect.fn("AcpClient.handleCreateTerminal")(function (handler) {
1341 return Effect.sync(() => {
1342 coreHandlers.createTerminal = handler;
1343 });
1344 }),
1345 handleTerminalOutput: Effect.fn("AcpClient.handleTerminalOutput")(function (handler) {
1346 return Effect.sync(() => {
1347 coreHandlers.terminalOutput = handler;
1348 });
1349 }),
1350 handleTerminalWaitForExit: Effect.fn("AcpClient.handleTerminalWaitForExit")(function (handler) {
1351 return Effect.sync(() => {
1352 coreHandlers.terminalWaitForExit = handler;
1353 });
1354 }),
1355 handleTerminalKill: Effect.fn("AcpClient.handleTerminalKill")(function (handler) {
1356 return Effect.sync(() => {
1357 coreHandlers.terminalKill = handler;
1358 });
1359 }),
1360 handleTerminalRelease: Effect.fn("AcpClient.handleTerminalRelease")(function (handler) {
1361 return Effect.sync(() => {
1362 coreHandlers.terminalRelease = handler;
1363 });
1364 }),
1365 handleSessionUpdate: Effect.fn("AcpClient.handleSessionUpdate")(function (handler) {
1366 return Effect.suspend(() => {
1367 notificationHandlers.sessionUpdate.handlers.push(handler);
1368 return flushBufferedNotifications(notificationHandlers.sessionUpdate);
1369 });
1370 }),
1371 handleElicitationComplete: Effect.fn("AcpClient.handleElicitationComplete")(function (handler) {
1372 return Effect.suspend(() => {
1373 notificationHandlers.elicitationComplete.handlers.push(handler);
1374 return flushBufferedNotifications(notificationHandlers.elicitationComplete);
1375 });
1376 }),
1377 handleUnknownExtRequest: Effect.fn("AcpClient.handleUnknownExtRequest")(function (handler) {
1378 return Effect.suspend(() => {
1379 unknownExtRequestHandler = handler;
1380 return Effect.void;
1381 });
1382 }),
1383 handleUnknownExtNotification: Effect.fn("AcpClient.handleUnknownExtNotification")(
1384 function (handler) {
1385 return Effect.suspend(() => {
1386 unknownExtNotificationHandler = handler;
1387 return Effect.void;
1388 });
1389 },
1390 ),
1391 handleExtRequest: Effect.fn("AcpClient.handleExtRequest")(function (method, payload, handler) {
1392 return Effect.suspend(() => {
1393 extRequestHandlers.set(method, decodeExtRequestRegistration(method, payload, handler));
1394 return Effect.void;
1395 });
1396 }),
1397 handleExtNotification: Effect.fn("AcpClient.handleExtNotification")(
1398 function (method, payload, handler) {
1399 return Effect.suspend(() => {
1400 extNotificationHandlers.set(
1401 method,
1402 decodeExtNotificationRegistration(method, payload, handler),
1403 );
1404 return Effect.void;
1405 });
1406 },
1407 ),
1408 });
1409});
1410​
1411export const layerChildProcess = (
1412 handle: ChildProcessSpawner.ChildProcessHandle,
1413 options: AcpClientOptions = {},
1414): Layer.Layer<AcpClient> => {
1415 const stdio = {
1416 ...makeChildStdio(handle),
1417 stdin: options.transformStdout?.(handle.stdout) ?? handle.stdout,
1418 };
1419 const terminationError = makeTerminationError(handle);
1420 return Layer.effect(AcpClient, make(stdio, options, terminationError));