| 1 | import * as Context from "effect/Context"; |
| 2 | import * as Deferred from "effect/Deferred"; |
| 3 | import * as Effect from "effect/Effect"; |
| 4 | import * as ErrorReporter from "effect/ErrorReporter"; |
| 5 | import * as Layer from "effect/Layer"; |
| 6 | import * as Schema from "effect/Schema"; |
| 7 | import * as Predicate from "effect/Predicate"; |
| 8 | import * as Scope from "effect/Scope"; |
| 9 | import * as Stream from "effect/Stream"; |
| 10 | import * as RpcClient from "effect/rpc/RpcClient"; |
| 11 | import * as RpcMessage from "effect/rpc/RpcMessage"; |
| 12 | import * as RpcServer from "effect/rpc/RpcServer"; |
| 13 | import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; |
| 14 | |
| 15 | import * as AcpError from "./errors.ts"; |
| 16 | import * as AcpProtocol from "./protocol.ts"; |
| 17 | import * as AcpRpcs from "./rpc.ts"; |
| 18 | import * as AcpSchema from "./compat.ts"; |
| 19 | import * as AcpSchemaV1 from "./_generated/schema-v1.gen.ts"; |
| 20 | import * as AcpSchemaV2 from "./_generated/schema.gen.ts"; |
| 21 | import { AGENT_METHODS, CLIENT_METHODS } from "./_generated/meta.gen.ts"; |
| 22 | import { |
| 23 | callRpc, |
| 24 | decodeExtNotificationRegistration, |
| 25 | decodeExtRequestRegistration, |
| 26 | isolateNotificationHandler, |
| 27 | runHandler, |
| 28 | } from "./_internal/shared.ts"; |
| 29 | import { makeChildStdio, makeTerminationError } from "./_internal/stdio.ts"; |
| 30 | |
| 31 | const decodeElicitationRequest = Schema.decodeUnknownEffect( |
| 32 | Schema.Union([AcpSchemaV2.CreateElicitationRequest, AcpSchemaV1.CreateElicitationRequest]), |
| 33 | ); |
| 34 | |
| 35 | export 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 | |
| 53 | type 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 | |
| 59 | export type AcpRequestHandler<Request, Response> = AcpProtocol.AcpRequestHandler<Request, Response>; |
| 60 | |
| 61 | export 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 | |
| 301 | interface 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 | |
| 343 | interface AcpNotificationHandlers { |
| 344 | readonly sessionUpdate: BufferedNotificationHandler<AcpSchema.SessionNotification>; |
| 345 | readonly elicitationComplete: BufferedNotificationHandler<AcpSchema.CompleteElicitationNotification>; |
| 346 | } |
| 347 | |
| 348 | interface BufferedNotificationHandler<A> { |
| 349 | readonly handlers: Array<(notification: A) => Effect.Effect<void, AcpError.AcpError>>; |
| 350 | readonly pending: Array<A>; |
| 351 | } |
| 352 | |
| 353 | function 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 | |
| 366 | function 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 | |
| 381 | function 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 | |
| 400 | function 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 | |
| 431 | function 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 | |
| 440 | function 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 | |
| 577 | function 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 | |
| 598 | function 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 | |
| 610 | function 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 | |
| 622 | function 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 | |
| 630 | function 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 | |
| 733 | function 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 | |
| 749 | function 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 | |
| 759 | function isV2InitializeResponse( |
| 760 | response: AcpSchemaV1.InitializeResponse | AcpSchemaV2.InitializeResponse, |
| 761 | ): response is AcpSchemaV2.InitializeResponse { |
| 762 | return "info" in response; |
| 763 | } |
| 764 | |
| 765 | function 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 | |
| 779 | function 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 | |
| 808 | export 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 | |
| 1423 | export 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 | }; |