import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Schema from "effect/Schema"; import * as Scope from "effect/Scope"; import * as Stream from "effect/Stream"; import * as Stdio from "effect/Stdio"; import * as RpcClient from "effect/unstable/rpc/RpcClient"; import * as RpcServer from "effect/unstable/rpc/RpcServer"; import * as AcpSchema from "./_generated/schema.gen.ts"; import { AGENT_METHODS, CLIENT_METHODS } from "./_generated/meta.gen.ts"; import * as AcpError from "./errors.ts"; import * as AcpProtocol from "./protocol.ts"; import * as AcpRpcs from "./rpc.ts"; import { callRpc, decodeExtNotificationRegistration, decodeExtRequestRegistration, runHandler, } from "./_internal/shared.ts"; import * as AcpTerminal from "./terminal.ts"; export interface AcpAgentOptions { readonly logIncoming?: boolean; readonly logOutgoing?: boolean; readonly logger?: (event: AcpProtocol.AcpProtocolLogEvent) => Effect.Effect; } export interface AcpAgentShape { readonly raw: { /** * Stream of inbound ACP notifications observed on the connection. */ readonly notifications: Stream.Stream; /** * Sends a generic ACP extension request. * @see https://agentclientprotocol.com/protocol/extensibility */ readonly request: ( method: string, payload: unknown, ) => Effect.Effect; /** * Sends a generic ACP extension notification. * @see https://agentclientprotocol.com/protocol/extensibility */ readonly notify: (method: string, payload: unknown) => Effect.Effect; }; readonly client: { /** * Requests client permission for an operation. * @see https://agentclientprotocol.com/protocol/schema#session/request_permission */ readonly requestPermission: ( payload: AcpSchema.RequestPermissionRequest, ) => Effect.Effect; /** * Requests structured user input from the client. * @see https://agentclientprotocol.com/protocol/schema#session/elicitation */ readonly elicit: ( payload: AcpSchema.ElicitationRequest, ) => Effect.Effect; /** * Requests file contents from the client. * @see https://agentclientprotocol.com/protocol/schema#fs/read_text_file */ readonly readTextFile: ( payload: AcpSchema.ReadTextFileRequest, ) => Effect.Effect; /** * Writes a text file through the client. * @see https://agentclientprotocol.com/protocol/schema#fs/write_text_file */ readonly writeTextFile: ( payload: AcpSchema.WriteTextFileRequest, ) => Effect.Effect; /** * Creates a terminal on the client side. * @see https://agentclientprotocol.com/protocol/schema#terminal/create */ readonly createTerminal: ( payload: AcpSchema.CreateTerminalRequest, ) => Effect.Effect; /** * Sends a `session/update` notification to the client. * @see https://agentclientprotocol.com/protocol/schema#session/update */ readonly sessionUpdate: ( payload: AcpSchema.SessionNotification, ) => Effect.Effect; /** * Sends a `session/elicitation/complete` notification to the client. * @see https://agentclientprotocol.com/protocol/schema#session/elicitation/complete */ readonly elicitationComplete: ( payload: AcpSchema.ElicitationCompleteNotification, ) => Effect.Effect; /** * Sends an ACP extension request to the client. * @see https://agentclientprotocol.com/protocol/extensibility */ readonly extRequest: ( method: string, payload: unknown, ) => Effect.Effect; /** * Sends an ACP extension notification to the client. * @see https://agentclientprotocol.com/protocol/extensibility */ readonly extNotification: ( method: string, payload: unknown, ) => Effect.Effect; }; /** * Registers a handler for `initialize`. * @see https://agentclientprotocol.com/protocol/schema#initialize */ readonly handleInitialize: ( handler: ( request: AcpSchema.InitializeRequest, ) => Effect.Effect, ) => Effect.Effect; /** * Registers a handler for `authenticate`. * @see https://agentclientprotocol.com/protocol/schema#authenticate */ readonly handleAuthenticate: ( handler: ( request: AcpSchema.AuthenticateRequest, ) => Effect.Effect, ) => Effect.Effect; readonly handleLogout: ( handler: ( request: AcpSchema.LogoutRequest, ) => Effect.Effect, ) => Effect.Effect; readonly handleCreateSession: ( handler: ( request: AcpSchema.NewSessionRequest, ) => Effect.Effect, ) => Effect.Effect; readonly handleLoadSession: ( handler: ( request: AcpSchema.LoadSessionRequest, ) => Effect.Effect, ) => Effect.Effect; readonly handleListSessions: ( handler: ( request: AcpSchema.ListSessionsRequest, ) => Effect.Effect, ) => Effect.Effect; readonly handleForkSession: ( handler: ( request: AcpSchema.ForkSessionRequest, ) => Effect.Effect, ) => Effect.Effect; readonly handleResumeSession: ( handler: ( request: AcpSchema.ResumeSessionRequest, ) => Effect.Effect, ) => Effect.Effect; readonly handleCloseSession: ( handler: ( request: AcpSchema.CloseSessionRequest, ) => Effect.Effect, ) => Effect.Effect; readonly handleSetSessionModel: ( handler: ( request: AcpSchema.SetSessionModelRequest, ) => Effect.Effect, ) => Effect.Effect; readonly handleSetSessionConfigOption: ( handler: ( request: AcpSchema.SetSessionConfigOptionRequest, ) => Effect.Effect, ) => Effect.Effect; readonly handlePrompt: ( handler: ( request: AcpSchema.PromptRequest, ) => Effect.Effect, ) => Effect.Effect; /** * Registers a handler for `session/cancel`. * @see https://agentclientprotocol.com/protocol/schema#session/cancel */ readonly handleCancel: ( handler: (notification: AcpSchema.CancelNotification) => Effect.Effect, ) => Effect.Effect; readonly handleUnknownExtRequest: ( handler: (method: string, params: unknown) => Effect.Effect, ) => Effect.Effect; readonly handleUnknownExtNotification: ( handler: (method: string, params: unknown) => Effect.Effect, ) => Effect.Effect; readonly handleExtRequest: ( method: string, payload: Schema.Codec, handler: (payload: A) => Effect.Effect, ) => Effect.Effect; readonly handleExtNotification: ( method: string, payload: Schema.Codec, handler: (payload: A) => Effect.Effect, ) => Effect.Effect; } export class AcpAgent extends Context.Service()( "effect-acp/agent/AcpAgent", ) {} interface AcpCoreAgentRequestHandlers { initialize?: ( request: AcpSchema.InitializeRequest, ) => Effect.Effect; authenticate?: ( request: AcpSchema.AuthenticateRequest, ) => Effect.Effect; logout?: ( request: AcpSchema.LogoutRequest, ) => Effect.Effect; createSession?: ( request: AcpSchema.NewSessionRequest, ) => Effect.Effect; loadSession?: ( request: AcpSchema.LoadSessionRequest, ) => Effect.Effect; listSessions?: ( request: AcpSchema.ListSessionsRequest, ) => Effect.Effect; forkSession?: ( request: AcpSchema.ForkSessionRequest, ) => Effect.Effect; resumeSession?: ( request: AcpSchema.ResumeSessionRequest, ) => Effect.Effect; closeSession?: ( request: AcpSchema.CloseSessionRequest, ) => Effect.Effect; setSessionModel?: ( request: AcpSchema.SetSessionModelRequest, ) => Effect.Effect; setSessionConfigOption?: ( request: AcpSchema.SetSessionConfigOptionRequest, ) => Effect.Effect; prompt?: ( request: AcpSchema.PromptRequest, ) => Effect.Effect; } const decodeCancelNotification = Schema.decodeUnknownEffect(AcpSchema.CancelNotification); export const make = Effect.fn("effect-acp/AcpAgent.make")(function* ( stdio: Stdio.Stdio, options: AcpAgentOptions = {}, ): Effect.fn.Return { const coreHandlers: AcpCoreAgentRequestHandlers = {}; const cancelHandlers: Array< (notification: AcpSchema.CancelNotification) => Effect.Effect > = []; const extRequestHandlers = new Map< string, (params: unknown) => Effect.Effect >(); const extNotificationHandlers = new Map< string, (params: unknown) => Effect.Effect >(); let unknownExtRequestHandler: | ((method: string, params: unknown) => Effect.Effect) | undefined; let unknownExtNotificationHandler: | ((method: string, params: unknown) => Effect.Effect) | undefined; const transport = yield* AcpProtocol.makeAcpPatchedProtocol({ stdio, serverRequestMethods: new Set(AcpRpcs.AgentRpcs.requests.keys()), ...(options.logIncoming !== undefined ? { logIncoming: options.logIncoming } : {}), ...(options.logOutgoing !== undefined ? { logOutgoing: options.logOutgoing } : {}), ...(options.logger ? { logger: options.logger } : {}), onNotification: (notification) => { if ( notification._tag === "ExtNotification" && notification.method === AGENT_METHODS.session_cancel ) { return decodeCancelNotification(notification.params).pipe( Effect.mapError( (error) => new AcpError.AcpProtocolParseError({ detail: `Invalid ${AGENT_METHODS.session_cancel} notification payload`, cause: error, }), ), Effect.flatMap((decoded) => Effect.forEach(cancelHandlers, (handler) => handler(decoded), { discard: true }), ), ); } if (notification._tag !== "ExtNotification") { return Effect.void; } const handler = extNotificationHandlers.get(notification.method); if (handler) { return handler(notification.params); } return unknownExtNotificationHandler ? unknownExtNotificationHandler(notification.method, notification.params) : Effect.void; }, onExtRequest: (method, params) => { const handler = extRequestHandlers.get(method); if (handler) { return handler(params); } return unknownExtRequestHandler ? unknownExtRequestHandler(method, params) : Effect.fail(AcpError.AcpRequestError.methodNotFound(method)); }, }); const agentHandlerLayer = AcpRpcs.AgentRpcs.toLayer( AcpRpcs.AgentRpcs.of({ [AGENT_METHODS.initialize]: (payload) => runHandler(coreHandlers.initialize, payload, AGENT_METHODS.initialize), [AGENT_METHODS.authenticate]: (payload) => runHandler(coreHandlers.authenticate, payload, AGENT_METHODS.authenticate), [AGENT_METHODS.logout]: (payload) => runHandler(coreHandlers.logout, payload, AGENT_METHODS.logout), [AGENT_METHODS.session_new]: (payload) => runHandler(coreHandlers.createSession, payload, AGENT_METHODS.session_new), [AGENT_METHODS.session_load]: (payload) => runHandler(coreHandlers.loadSession, payload, AGENT_METHODS.session_load), [AGENT_METHODS.session_list]: (payload) => runHandler(coreHandlers.listSessions, payload, AGENT_METHODS.session_list), [AGENT_METHODS.session_fork]: (payload) => runHandler(coreHandlers.forkSession, payload, AGENT_METHODS.session_fork), [AGENT_METHODS.session_resume]: (payload) => runHandler(coreHandlers.resumeSession, payload, AGENT_METHODS.session_resume), [AGENT_METHODS.session_close]: (payload) => runHandler(coreHandlers.closeSession, payload, AGENT_METHODS.session_close), [AGENT_METHODS.session_set_model]: (payload) => runHandler(coreHandlers.setSessionModel, payload, AGENT_METHODS.session_set_model), [AGENT_METHODS.session_set_config_option]: (payload) => runHandler( coreHandlers.setSessionConfigOption, payload, AGENT_METHODS.session_set_config_option, ), [AGENT_METHODS.session_prompt]: (payload) => runHandler(coreHandlers.prompt, payload, AGENT_METHODS.session_prompt), }), ); yield* RpcServer.make(AcpRpcs.AgentRpcs).pipe( Effect.provideService(RpcServer.Protocol, transport.serverProtocol), Effect.provide(agentHandlerLayer), Effect.forkScoped, ); let nextRpcRequestId = 1n << 32n; const rpc = yield* RpcClient.make(AcpRpcs.ClientRpcs, { generateRequestId: () => nextRpcRequestId++ as never, }).pipe(Effect.provideService(RpcClient.Protocol, transport.clientProtocol)); return AcpAgent.of({ raw: { notifications: transport.incoming, request: transport.request, notify: transport.notify, }, client: { requestPermission: (payload) => callRpc(rpc[CLIENT_METHODS.session_request_permission](payload)), elicit: (payload) => callRpc(rpc[CLIENT_METHODS.session_elicitation](payload)), readTextFile: (payload) => callRpc(rpc[CLIENT_METHODS.fs_read_text_file](payload)), writeTextFile: (payload) => callRpc(rpc[CLIENT_METHODS.fs_write_text_file](payload)), createTerminal: (payload) => callRpc(rpc[CLIENT_METHODS.terminal_create](payload)).pipe( Effect.map((response) => AcpTerminal.makeTerminal({ sessionId: payload.sessionId, terminalId: response.terminalId, output: callRpc( rpc[CLIENT_METHODS.terminal_output]({ sessionId: payload.sessionId, terminalId: response.terminalId, }), ), waitForExit: callRpc( rpc[CLIENT_METHODS.terminal_wait_for_exit]({ sessionId: payload.sessionId, terminalId: response.terminalId, }), ), kill: callRpc( rpc[CLIENT_METHODS.terminal_kill]({ sessionId: payload.sessionId, terminalId: response.terminalId, }), ), release: callRpc( rpc[CLIENT_METHODS.terminal_release]({ sessionId: payload.sessionId, terminalId: response.terminalId, }), ), }), ), ), sessionUpdate: (payload) => transport.notify(CLIENT_METHODS.session_update, payload), elicitationComplete: (payload) => transport.notify(CLIENT_METHODS.session_elicitation_complete, payload), extRequest: transport.request, extNotification: transport.notify, }, handleInitialize: (handler) => Effect.suspend(() => { coreHandlers.initialize = handler; return Effect.void; }), handleAuthenticate: (handler) => Effect.suspend(() => { coreHandlers.authenticate = handler; return Effect.void; }), handleLogout: (handler) => Effect.suspend(() => { coreHandlers.logout = handler; return Effect.void; }), handleCreateSession: (handler) => Effect.suspend(() => { coreHandlers.createSession = handler; return Effect.void; }), handleLoadSession: (handler) => Effect.suspend(() => { coreHandlers.loadSession = handler; return Effect.void; }), handleListSessions: (handler) => Effect.suspend(() => { coreHandlers.listSessions = handler; return Effect.void; }), handleForkSession: (handler) => Effect.suspend(() => { coreHandlers.forkSession = handler; return Effect.void; }), handleResumeSession: (handler) => Effect.suspend(() => { coreHandlers.resumeSession = handler; return Effect.void; }), handleCloseSession: (handler) => Effect.suspend(() => { coreHandlers.closeSession = handler; return Effect.void; }), handleSetSessionModel: (handler) => Effect.suspend(() => { coreHandlers.setSessionModel = handler; return Effect.void; }), handleSetSessionConfigOption: (handler) => Effect.suspend(() => { coreHandlers.setSessionConfigOption = handler; return Effect.void; }), handlePrompt: (handler) => Effect.suspend(() => { coreHandlers.prompt = handler; return Effect.void; }), handleCancel: (handler) => Effect.suspend(() => { cancelHandlers.push(handler); return Effect.void; }), handleUnknownExtRequest: (handler) => Effect.suspend(() => { unknownExtRequestHandler = handler; return Effect.void; }), handleUnknownExtNotification: (handler) => Effect.suspend(() => { unknownExtNotificationHandler = handler; return Effect.void; }), handleExtRequest: (method, payload, handler) => Effect.suspend(() => { extRequestHandlers.set(method, decodeExtRequestRegistration(method, payload, handler)); return Effect.void; }), handleExtNotification: (method, payload, handler) => Effect.suspend(() => { extNotificationHandlers.set( method, decodeExtNotificationRegistration(method, payload, handler), ); return Effect.void; }), }); }); export const layer = (stdio: Stdio.Stdio, options: AcpAgentOptions = {}): Layer.Layer => Layer.effect(AcpAgent, make(stdio, options)); export const layerStdio = ( options: AcpAgentOptions = {}, ): Layer.Layer => Layer.effect( AcpAgent, Effect.flatMap(Effect.service(Stdio.Stdio), (stdio) => make(stdio, options)), );