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 Stdio from "effect/Stdio"; import * as Stream from "effect/Stream"; import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner"; import * as CodexRpc from "./_generated/meta.gen.ts"; import * as CodexSchema from "./_generated/schema.gen.ts"; import * as CodexError from "./errors.ts"; import * as CodexProtocol from "./protocol.ts"; import { decodeNotificationPayload, decodeOptionalPayload, encodeOptionalPayload, runHandler, } from "./_internal/shared.ts"; import { makeChildStdio, makeTerminationError } from "./_internal/stdio.ts"; export interface CodexAppServerClientOptions { readonly logIncoming?: boolean; readonly logOutgoing?: boolean; readonly logger?: ( event: CodexProtocol.CodexAppServerProtocolLogEvent, ) => Effect.Effect; } interface CodexAppServerClientRaw { readonly notifications: CodexProtocol.CodexAppServerPatchedProtocol["incomingNotifications"]; readonly requests: CodexProtocol.CodexAppServerPatchedProtocol["incomingRequests"]; readonly request: CodexProtocol.CodexAppServerPatchedProtocol["request"]; readonly notify: CodexProtocol.CodexAppServerPatchedProtocol["notify"]; readonly respond: CodexProtocol.CodexAppServerPatchedProtocol["respond"]; readonly respondError: CodexProtocol.CodexAppServerPatchedProtocol["respondError"]; } export class CodexAppServerClient extends Context.Service< CodexAppServerClient, { readonly raw: CodexAppServerClientRaw; readonly request: ( method: M, payload: CodexRpc.ClientRequestParamsByMethod[M], ) => Effect.Effect; readonly notify: ( method: M, payload: CodexRpc.ClientNotificationParamsByMethod[M], ) => Effect.Effect; readonly handleServerRequest: ( method: M, handler: ( payload: CodexRpc.ServerRequestParamsByMethod[M], ) => Effect.Effect< CodexRpc.ServerRequestResponsesByMethod[M], CodexError.CodexAppServerError >, ) => Effect.Effect; readonly handleServerNotification: ( method: M, handler: ( payload: CodexRpc.ServerNotificationParamsByMethod[M], ) => Effect.Effect, ) => Effect.Effect; readonly handleUnknownServerRequest: ( handler: ( method: string, params: unknown, ) => Effect.Effect, ) => Effect.Effect; readonly handleUnknownServerNotification: ( handler: ( method: string, params: unknown, ) => Effect.Effect, ) => Effect.Effect; } >()("effect-codex-app-server/client/CodexAppServerClient") {} type ServerRequestHandler = ( payload: unknown, ) => Effect.Effect; type ServerNotificationHandler = ( payload: unknown, ) => Effect.Effect; const V2TurnStartParamsWithCollaborationMode = CodexSchema.V2TurnStartParams.pipe( Schema.fieldsAssign({ collaborationMode: Schema.optionalKey(CodexSchema.ClientRequest__CollaborationMode), }), ); export const make = Effect.fn("effect-codex-app-server/CodexAppServerClient.make")(function* ( stdio: Stdio.Stdio, options: CodexAppServerClientOptions = {}, terminationError?: Effect.Effect, ): Effect.fn.Return { const requestHandlers = new Map(); const notificationHandlers = new Map>(); let unknownRequestHandler: | ((method: string, params: unknown) => Effect.Effect) | undefined; let unknownNotificationHandler: | ((method: string, params: unknown) => Effect.Effect) | undefined; const getServerRequestParamSchema = ( method: M, ): | Schema.Codec | undefined => CodexRpc.SERVER_REQUEST_PARAMS[method] as never; const getServerRequestResponseSchema = ( method: M, ): | Schema.Codec< CodexRpc.ServerRequestResponsesByMethod[M], CodexRpc.ServerRequestResponsesByMethod[M] > | undefined => CodexRpc.SERVER_REQUEST_RESPONSES[method] as never; const getClientRequestParamSchema = ( method: M, ): | Schema.Codec | undefined => method === "turn/start" ? (V2TurnStartParamsWithCollaborationMode as never) : (CodexRpc.CLIENT_REQUEST_PARAMS[method] as never); const getClientRequestResponseSchema = ( method: M, ): | Schema.Codec< CodexRpc.ClientRequestResponsesByMethod[M], CodexRpc.ClientRequestResponsesByMethod[M] > | undefined => CodexRpc.CLIENT_REQUEST_RESPONSES[method] as never; const getClientNotificationParamSchema = ( method: M, ): | Schema.Codec< CodexRpc.ClientNotificationParamsByMethod[M], CodexRpc.ClientNotificationParamsByMethod[M] > | undefined => CodexRpc.CLIENT_NOTIFICATION_PARAMS[method] as never; const dispatchNotification = ( notification: CodexProtocol.CodexAppServerIncomingNotification, ): Effect.Effect => { const schema = notification.method in CodexRpc.SERVER_NOTIFICATION_PARAMS ? CodexRpc.SERVER_NOTIFICATION_PARAMS[ notification.method as CodexRpc.ServerNotificationMethod ] : undefined; const handlers = notificationHandlers.get(notification.method) ?? []; if (schema) { return decodeNotificationPayload(notification.method, schema, notification.params).pipe( Effect.flatMap((decoded) => Effect.forEach(handlers, (handler) => handler(decoded), { discard: true }), ), Effect.catch(() => Effect.void), ); } return unknownNotificationHandler ? unknownNotificationHandler(notification.method, notification.params).pipe( Effect.catch(() => Effect.void), ) : Effect.void; }; const dispatchRequest = ( request: CodexProtocol.CodexAppServerIncomingRequest, ): Effect.Effect => { if (request.method in CodexRpc.SERVER_REQUEST_PARAMS) { const method = request.method as CodexRpc.ServerRequestMethod; const payloadSchema = getServerRequestParamSchema(method); const responseSchema = getServerRequestResponseSchema(method); const handler = requestHandlers.get(method); return decodeOptionalPayload(method, payloadSchema, request.params).pipe( Effect.flatMap((decoded) => runHandler(handler, decoded, method)), Effect.flatMap((result) => encodeOptionalPayload(method, responseSchema, result)), ); } return unknownRequestHandler ? unknownRequestHandler(request.method, request.params) : Effect.fail(CodexError.CodexAppServerRequestError.methodNotFound(request.method)); }; const transport = yield* CodexProtocol.makeCodexAppServerPatchedProtocol({ stdio, ...(terminationError ? { terminationError } : {}), ...(options.logIncoming !== undefined ? { logIncoming: options.logIncoming } : {}), ...(options.logOutgoing !== undefined ? { logOutgoing: options.logOutgoing } : {}), ...(options.logger ? { logger: options.logger } : {}), onNotification: dispatchNotification, onRequest: dispatchRequest, }); const request = ( method: M, payload: CodexRpc.ClientRequestParamsByMethod[M], ): Effect.Effect => encodeOptionalPayload(method, getClientRequestParamSchema(method), payload).pipe( Effect.flatMap((encoded) => transport.request(method, encoded)), Effect.flatMap( ( raw, ): Effect.Effect< CodexRpc.ClientRequestResponsesByMethod[M], CodexError.CodexAppServerError > => decodeOptionalPayload(method, getClientRequestResponseSchema(method), raw), ), ); const notify = ( method: M, payload: CodexRpc.ClientNotificationParamsByMethod[M], ) => encodeOptionalPayload(method, getClientNotificationParamSchema(method), payload).pipe( Effect.flatMap((encoded) => transport.notify(method, encoded)), ); return CodexAppServerClient.of({ raw: { notifications: transport.incomingNotifications, requests: transport.incomingRequests, request: transport.request, notify: transport.notify, respond: transport.respond, respondError: transport.respondError, }, request, notify, handleServerRequest: (method, handler) => Effect.sync(() => { requestHandlers.set(method, handler as ServerRequestHandler); }), handleServerNotification: (method, handler) => Effect.sync(() => { const current = notificationHandlers.get(method) ?? []; current.push(handler as ServerNotificationHandler); notificationHandlers.set(method, current); }), handleUnknownServerRequest: (handler) => Effect.sync(() => { unknownRequestHandler = handler; }), handleUnknownServerNotification: (handler) => Effect.sync(() => { unknownNotificationHandler = handler; }), }); }); export const layer = ( stdio: Stdio.Stdio, options: CodexAppServerClientOptions = {}, ): Layer.Layer => Layer.effect(CodexAppServerClient, make(stdio, options)); export const layerChildProcess = ( handle: ChildProcessSpawner.ChildProcessHandle, options: CodexAppServerClientOptions = {}, ): Layer.Layer => Layer.effect(CodexAppServerClient, makeChildProcessClient(handle, options)); const makeChildProcessClient = Effect.fn( "effect-codex-app-server/CodexAppServerClient.makeChildProcessClient", )(function* (handle: ChildProcessSpawner.ChildProcessHandle, options: CodexAppServerClientOptions) { yield* Stream.runDrain(handle.stderr).pipe(Effect.ignore, Effect.forkScoped); return yield* make(makeChildStdio(handle), options, makeTerminationError(handle)); });