import * as Cause from "effect/Cause";import * as Deferred from "effect/Deferred";import * as Effect from "effect/Effect";import * as Exit from "effect/Exit";import * as Layer from "effect/Layer";import * as Queue from "effect/Queue";import * as Ref from "effect/Ref";import * as Scope from "effect/Scope";import * as Context from "effect/Context";import * as Stream from "effect/Stream";import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";import * as EffectAcpClient from "effect-acp/client";import * as EffectAcpErrors from "effect-acp/errors";import type * as EffectAcpSchema from "effect-acp/schema";import type * as EffectAcpProtocol from "effect-acp/protocol"; import { collectSessionConfigOptionValues, extractModelConfigId, findSessionConfigOption, mergeToolCallState, parseSessionModeState, parseSessionUpdateEvent, type AcpParsedSessionEvent, type AcpSessionModeState, type AcpToolCallState,} from "./AcpRuntimeModel.ts"; function formatConfigOptionValue(value: string | boolean): string { return JSON.stringify(value);} export interface AcpSpawnInput { readonly command: string; readonly args: ReadonlyArray<string>; readonly cwd?: string; readonly env?: NodeJS.ProcessEnv;} export interface AcpSessionRuntimeOptions { readonly spawn: AcpSpawnInput; readonly cwd: string; readonly resumeSessionId?: string; readonly clientCapabilities?: EffectAcpSchema.InitializeRequest["clientCapabilities"]; readonly clientInfo: { readonly name: string; readonly version: string; }; readonly authMethodId: string; readonly requestLogger?: (event: AcpSessionRequestLogEvent) => Effect.Effect<void, never>; readonly protocolLogging?: { readonly logIncoming?: boolean; readonly logOutgoing?: boolean; readonly logger?: (event: EffectAcpProtocol.AcpProtocolLogEvent) => Effect.Effect<void, never>; };} export interface AcpSessionRequestLogEvent { readonly method: string; readonly payload: unknown; readonly status: "started" | "succeeded" | "failed"; readonly result?: unknown; readonly cause?: Cause.Cause<EffectAcpErrors.AcpError>;} export interface AcpSessionRuntimeStartResult { readonly sessionId: string; readonly initializeResult: EffectAcpSchema.InitializeResponse; readonly sessionSetupResult: | EffectAcpSchema.LoadSessionResponse | EffectAcpSchema.NewSessionResponse | EffectAcpSchema.ResumeSessionResponse; readonly modelConfigId: string | undefined;} export interface AcpSessionRuntimeShape { readonly handleRequestPermission: EffectAcpClient.AcpClientShape["handleRequestPermission"]; readonly handleElicitation: EffectAcpClient.AcpClientShape["handleElicitation"]; readonly handleReadTextFile: EffectAcpClient.AcpClientShape["handleReadTextFile"]; readonly handleWriteTextFile: EffectAcpClient.AcpClientShape["handleWriteTextFile"]; readonly handleCreateTerminal: EffectAcpClient.AcpClientShape["handleCreateTerminal"]; readonly handleTerminalOutput: EffectAcpClient.AcpClientShape["handleTerminalOutput"]; readonly handleTerminalWaitForExit: EffectAcpClient.AcpClientShape["handleTerminalWaitForExit"]; readonly handleTerminalKill: EffectAcpClient.AcpClientShape["handleTerminalKill"]; readonly handleTerminalRelease: EffectAcpClient.AcpClientShape["handleTerminalRelease"]; readonly handleSessionUpdate: EffectAcpClient.AcpClientShape["handleSessionUpdate"]; readonly handleElicitationComplete: EffectAcpClient.AcpClientShape["handleElicitationComplete"]; readonly handleUnknownExtRequest: EffectAcpClient.AcpClientShape["handleUnknownExtRequest"]; readonly handleUnknownExtNotification: EffectAcpClient.AcpClientShape["handleUnknownExtNotification"]; readonly handleExtRequest: EffectAcpClient.AcpClientShape["handleExtRequest"]; readonly handleExtNotification: EffectAcpClient.AcpClientShape["handleExtNotification"]; readonly start: () => Effect.Effect<AcpSessionRuntimeStartResult, EffectAcpErrors.AcpError>; readonly getEvents: () => Stream.Stream<AcpParsedSessionEvent, never>; readonly getModeState: Effect.Effect<AcpSessionModeState | undefined>; readonly getConfigOptions: Effect.Effect<ReadonlyArray<EffectAcpSchema.SessionConfigOption>>; readonly prompt: ( payload: Omit<EffectAcpSchema.PromptRequest, "sessionId">, ) => Effect.Effect<EffectAcpSchema.PromptResponse, EffectAcpErrors.AcpError>; readonly cancel: Effect.Effect<void, EffectAcpErrors.AcpError>; readonly setMode: ( modeId: string, ) => Effect.Effect<EffectAcpSchema.SetSessionModeResponse, EffectAcpErrors.AcpError>; readonly setConfigOption: ( configId: string, value: string | boolean, ) => Effect.Effect<EffectAcpSchema.SetSessionConfigOptionResponse, EffectAcpErrors.AcpError>; readonly setModel: (model: string) => Effect.Effect<void, EffectAcpErrors.AcpError>; readonly setSessionModel: ( modelId: string, ) => Effect.Effect<EffectAcpSchema.SetSessionModelResponse, EffectAcpErrors.AcpError>; readonly request: ( method: string, payload: unknown, ) => Effect.Effect<unknown, EffectAcpErrors.AcpError>; readonly notify: ( method: string, payload: unknown, ) => Effect.Effect<void, EffectAcpErrors.AcpError>;} interface AcpStartedState extends AcpSessionRuntimeStartResult {} type AcpStartState = | { readonly _tag: "NotStarted" } | { readonly _tag: "Starting"; readonly deferred: Deferred.Deferred<AcpSessionRuntimeStartResult, EffectAcpErrors.AcpError>; } | { readonly _tag: "Started"; readonly result: AcpStartedState }; interface AcpAssistantSegmentState { readonly nextSegmentIndex: number; readonly activeItemId?: string;} interface EnsureActiveAssistantSegmentResult { readonly itemId: string; readonly startedEvent?: Extract<AcpParsedSessionEvent, { readonly _tag: "AssistantItemStarted" }>;} export class AcpSessionRuntime extends Context.Service<AcpSessionRuntime, AcpSessionRuntimeShape>()( "t3/provider/acp/AcpSessionRuntime",) { static layer( options: AcpSessionRuntimeOptions, ): Layer.Layer< AcpSessionRuntime, EffectAcpErrors.AcpError, ChildProcessSpawner.ChildProcessSpawner > { return Layer.effect(AcpSessionRuntime, makeAcpSessionRuntime(options)); }} const makeAcpSessionRuntime = ( options: AcpSessionRuntimeOptions,): Effect.Effect< AcpSessionRuntimeShape, EffectAcpErrors.AcpError, ChildProcessSpawner.ChildProcessSpawner | Scope.Scope> => Effect.gen(function* () { const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const runtimeScope = yield* Scope.Scope; const eventQueue = yield* Queue.unbounded<AcpParsedSessionEvent>(); const modeStateRef = yield* Ref.make<AcpSessionModeState | undefined>(undefined); const toolCallsRef = yield* Ref.make(new Map<string, AcpToolCallState>()); const assistantSegmentRef = yield* Ref.make<AcpAssistantSegmentState>({ nextSegmentIndex: 0 }); const configOptionsRef = yield* Ref.make(sessionConfigOptionsFromSetup(undefined)); const startStateRef = yield* Ref.make<AcpStartState>({ _tag: "NotStarted" }); const logRequest = (event: AcpSessionRequestLogEvent) => options.requestLogger ? options.requestLogger(event) : Effect.void; const runLoggedRequest = <A>( method: string, payload: unknown, effect: Effect.Effect<A, EffectAcpErrors.AcpError>, ): Effect.Effect<A, EffectAcpErrors.AcpError> => logRequest({ method, payload, status: "started" }).pipe( Effect.flatMap(() => effect.pipe( Effect.tap((result) => logRequest({ method, payload, status: "succeeded", result, }), ), Effect.onError((cause) => logRequest({ method, payload, status: "failed", cause, }), ), ), ), ); const child = yield* spawner .spawn( ChildProcess.make(options.spawn.command, [...options.spawn.args], { ...(options.spawn.cwd ? { cwd: options.spawn.cwd } : {}), ...(options.spawn.env ? { env: { ...process.env, ...options.spawn.env } } : {}), shell: process.platform === "win32", }), ) .pipe( Effect.provideService(Scope.Scope, runtimeScope), Effect.mapError( (cause) => new EffectAcpErrors.AcpSpawnError({ command: options.spawn.command, cause, }), ), ); const acpContext = yield* Layer.build( EffectAcpClient.layerChildProcess(child, { ...(options.protocolLogging?.logIncoming !== undefined ? { logIncoming: options.protocolLogging.logIncoming } : {}), ...(options.protocolLogging?.logOutgoing !== undefined ? { logOutgoing: options.protocolLogging.logOutgoing } : {}), ...(options.protocolLogging?.logger ? { logger: options.protocolLogging.logger } : {}), }), ).pipe(Effect.provideService(Scope.Scope, runtimeScope)); const acp = yield* Effect.service(EffectAcpClient.AcpClient).pipe(Effect.provide(acpContext)); yield* acp.handleSessionUpdate((notification) => handleSessionUpdate({ queue: eventQueue, modeStateRef, toolCallsRef, assistantSegmentRef, params: notification, }), ); const initializeClientCapabilities = { fs: { readTextFile: false, writeTextFile: false, ...options.clientCapabilities?.fs, }, terminal: options.clientCapabilities?.terminal ?? false, ...(options.clientCapabilities?.auth ? { auth: options.clientCapabilities.auth } : {}), ...(options.clientCapabilities?.elicitation ? { elicitation: options.clientCapabilities.elicitation } : {}), ...(options.clientCapabilities?._meta ? { _meta: options.clientCapabilities._meta } : {}), } satisfies NonNullable<EffectAcpSchema.InitializeRequest["clientCapabilities"]>; const getStartedState = Effect.gen(function* () { const state = yield* Ref.get(startStateRef); if (state._tag === "Started") { return state.result; } return yield* new EffectAcpErrors.AcpTransportError({ detail: "ACP session runtime has not been started", cause: "ACP session runtime has not been started", }); }); const validateConfigOptionValue = ( configId: string, value: string | boolean, ): Effect.Effect<void, EffectAcpErrors.AcpError> => Effect.gen(function* () { const configOption = findSessionConfigOption(yield* Ref.get(configOptionsRef), configId); if (!configOption) { return; } if (configOption.type === "boolean") { if (typeof value === "boolean") { return; } return yield* new EffectAcpErrors.AcpRequestError({ code: -32602, errorMessage: `Invalid value ${formatConfigOptionValue(value)} for session config option "${configOption.id}": expected boolean`, data: { configId: configOption.id, expectedType: "boolean", receivedValue: value, }, }); } if (typeof value !== "string") { return yield* new EffectAcpErrors.AcpRequestError({ code: -32602, errorMessage: `Invalid value ${formatConfigOptionValue(value)} for session config option "${configOption.id}": expected string`, data: { configId: configOption.id, expectedType: "string", receivedValue: value, }, }); } const allowedValues = collectSessionConfigOptionValues(configOption); if (allowedValues.includes(value)) { return; } return yield* new EffectAcpErrors.AcpRequestError({ code: -32602, errorMessage: `Invalid value ${formatConfigOptionValue(value)} for session config option "${configOption.id}": expected one of ${allowedValues.join(", ")}`, data: { configId: configOption.id, allowedValues, receivedValue: value, }, }); }); const updateConfigOptions = ( response: | EffectAcpSchema.SetSessionConfigOptionResponse | EffectAcpSchema.LoadSessionResponse | EffectAcpSchema.NewSessionResponse | EffectAcpSchema.ResumeSessionResponse, ): Effect.Effect<void> => Ref.set(configOptionsRef, sessionConfigOptionsFromSetup(response)); const updateCurrentModeId = (modeId: string): Effect.Effect<void> => Ref.update(modeStateRef, (current) => current ? { ...current, currentModeId: modeId } : current, ); const setConfigOption = ( configId: string, value: string | boolean, ): Effect.Effect<EffectAcpSchema.SetSessionConfigOptionResponse, EffectAcpErrors.AcpError> => validateConfigOptionValue(configId, value).pipe( Effect.flatMap(() => getStartedState), Effect.flatMap((started) => Ref.get(configOptionsRef).pipe( Effect.flatMap((configOptions) => { const existing = findSessionConfigOption(configOptions, configId); if (existing && configOptionCurrentValueMatches(existing, value)) { return Effect.succeed({ configOptions, } satisfies EffectAcpSchema.SetSessionConfigOptionResponse); } const requestPayload = typeof value === "boolean" ? ({ sessionId: started.sessionId, configId, type: "boolean", value, } satisfies EffectAcpSchema.SetSessionConfigOptionRequest) : ({ sessionId: started.sessionId, configId, value: String(value), } satisfies EffectAcpSchema.SetSessionConfigOptionRequest); return runLoggedRequest( "session/set_config_option", requestPayload, acp.agent.setSessionConfigOption(requestPayload), ).pipe(Effect.tap((response) => updateConfigOptions(response))); }), ), ), ); const startOnce = Effect.gen(function* () { const initializePayload = { protocolVersion: 1, clientCapabilities: initializeClientCapabilities, clientInfo: options.clientInfo, } satisfies EffectAcpSchema.InitializeRequest; const initializeResult = yield* runLoggedRequest( "initialize", initializePayload, acp.agent.initialize(initializePayload), ); const authenticatePayload = { methodId: options.authMethodId, } satisfies EffectAcpSchema.AuthenticateRequest; yield* runLoggedRequest( "authenticate", authenticatePayload, acp.agent.authenticate(authenticatePayload), ); let sessionId: string; let sessionSetupResult: | EffectAcpSchema.LoadSessionResponse | EffectAcpSchema.NewSessionResponse | EffectAcpSchema.ResumeSessionResponse; if (options.resumeSessionId) { const loadPayload = { sessionId: options.resumeSessionId, cwd: options.cwd, mcpServers: [], } satisfies EffectAcpSchema.LoadSessionRequest; const resumed = yield* runLoggedRequest( "session/load", loadPayload, acp.agent.loadSession(loadPayload), ).pipe(Effect.exit); if (Exit.isSuccess(resumed)) { sessionId = options.resumeSessionId; sessionSetupResult = resumed.value; } else { const createPayload = { cwd: options.cwd, mcpServers: [], } satisfies EffectAcpSchema.NewSessionRequest; const created = yield* runLoggedRequest( "session/new", createPayload, acp.agent.createSession(createPayload), ); sessionId = created.sessionId; sessionSetupResult = created; } } else { const createPayload = { cwd: options.cwd, mcpServers: [], } satisfies EffectAcpSchema.NewSessionRequest; const created = yield* runLoggedRequest( "session/new", createPayload, acp.agent.createSession(createPayload), ); sessionId = created.sessionId; sessionSetupResult = created; } yield* Ref.set(modeStateRef, parseSessionModeState(sessionSetupResult)); yield* Ref.set(configOptionsRef, sessionConfigOptionsFromSetup(sessionSetupResult)); const nextState = { sessionId, initializeResult, sessionSetupResult, modelConfigId: extractModelConfigId(sessionSetupResult), } satisfies AcpStartedState; return nextState; }); const start = Effect.gen(function* () { const deferred = yield* Deferred.make< AcpSessionRuntimeStartResult, EffectAcpErrors.AcpError >(); const effect = yield* Ref.modify(startStateRef, (state) => { switch (state._tag) { case "Started": return [Effect.succeed(state.result), state] as const; case "Starting": return [Deferred.await(state.deferred), state] as const; case "NotStarted": return [ startOnce.pipe( Effect.tap((result) => Ref.set(startStateRef, { _tag: "Started", result }).pipe( Effect.andThen(Deferred.succeed(deferred, result)), ), ), Effect.onError((cause) => Deferred.failCause(deferred, cause).pipe( Effect.andThen(Ref.set(startStateRef, { _tag: "NotStarted" })), ), ), ), { _tag: "Starting", deferred } satisfies AcpStartState, ] as const; } }); return yield* effect; }); return { handleRequestPermission: acp.handleRequestPermission, handleElicitation: acp.handleElicitation, handleReadTextFile: acp.handleReadTextFile, handleWriteTextFile: acp.handleWriteTextFile, handleCreateTerminal: acp.handleCreateTerminal, handleTerminalOutput: acp.handleTerminalOutput, handleTerminalWaitForExit: acp.handleTerminalWaitForExit, handleTerminalKill: acp.handleTerminalKill, handleTerminalRelease: acp.handleTerminalRelease, handleSessionUpdate: acp.handleSessionUpdate, handleElicitationComplete: acp.handleElicitationComplete, handleUnknownExtRequest: acp.handleUnknownExtRequest, handleUnknownExtNotification: acp.handleUnknownExtNotification, handleExtRequest: acp.handleExtRequest, handleExtNotification: acp.handleExtNotification, start: () => start,Lines 1–500 of 752. Load more
import * as Cause from "effect/Cause"; import * as Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; import * as Exit from "effect/Exit"; import * as Layer from "effect/Layer"; import * as Queue from "effect/Queue"; import * as Ref from "effect/Ref"; import * as Scope from "effect/Scope"; import * as Context from "effect/Context"; import * as Stream from "effect/Stream"; import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; import * as EffectAcpClient from "effect-acp/client"; import * as EffectAcpErrors from "effect-acp/errors"; import type * as EffectAcpSchema from "effect-acp/schema"; import type * as EffectAcpProtocol from "effect-acp/protocol"; import { collectSessionConfigOptionValues, extractModelConfigId, findSessionConfigOption, mergeToolCallState, parseSessionModeState, parseSessionUpdateEvent, type AcpParsedSessionEvent, type AcpSessionModeState, type AcpToolCallState, } from "./AcpRuntimeModel.ts"; function formatConfigOptionValue(value: string | boolean): string { return JSON.stringify(value); } export interface AcpSpawnInput { readonly command: string; readonly args: ReadonlyArray<string>; readonly cwd?: string; readonly env?: NodeJS.ProcessEnv; } export interface AcpSessionRuntimeOptions { readonly spawn: AcpSpawnInput; readonly cwd: string; readonly resumeSessionId?: string; readonly clientCapabilities?: EffectAcpSchema.InitializeRequest["clientCapabilities"]; readonly clientInfo: { readonly name: string; readonly version: string; }; readonly authMethodId: string; readonly requestLogger?: (event: AcpSessionRequestLogEvent) => Effect.Effect<void, never>; readonly protocolLogging?: { readonly logIncoming?: boolean; readonly logOutgoing?: boolean; readonly logger?: (event: EffectAcpProtocol.AcpProtocolLogEvent) => Effect.Effect<void, never>; }; } export interface AcpSessionRequestLogEvent { readonly method: string; readonly payload: unknown; readonly status: "started" | "succeeded" | "failed"; readonly result?: unknown; readonly cause?: Cause.Cause<EffectAcpErrors.AcpError>; } export interface AcpSessionRuntimeStartResult { readonly sessionId: string; readonly initializeResult: EffectAcpSchema.InitializeResponse; readonly sessionSetupResult: | EffectAcpSchema.LoadSessionResponse | EffectAcpSchema.NewSessionResponse | EffectAcpSchema.ResumeSessionResponse; readonly modelConfigId: string | undefined; } export interface AcpSessionRuntimeShape { readonly handleRequestPermission: EffectAcpClient.AcpClientShape["handleRequestPermission"]; readonly handleElicitation: EffectAcpClient.AcpClientShape["handleElicitation"]; readonly handleReadTextFile: EffectAcpClient.AcpClientShape["handleReadTextFile"]; readonly handleWriteTextFile: EffectAcpClient.AcpClientShape["handleWriteTextFile"]; readonly handleCreateTerminal: EffectAcpClient.AcpClientShape["handleCreateTerminal"]; readonly handleTerminalOutput: EffectAcpClient.AcpClientShape["handleTerminalOutput"]; readonly handleTerminalWaitForExit: EffectAcpClient.AcpClientShape["handleTerminalWaitForExit"]; readonly handleTerminalKill: EffectAcpClient.AcpClientShape["handleTerminalKill"]; readonly handleTerminalRelease: EffectAcpClient.AcpClientShape["handleTerminalRelease"]; readonly handleSessionUpdate: EffectAcpClient.AcpClientShape["handleSessionUpdate"]; readonly handleElicitationComplete: EffectAcpClient.AcpClientShape["handleElicitationComplete"]; readonly handleUnknownExtRequest: EffectAcpClient.AcpClientShape["handleUnknownExtRequest"]; readonly handleUnknownExtNotification: EffectAcpClient.AcpClientShape["handleUnknownExtNotification"]; readonly handleExtRequest: EffectAcpClient.AcpClientShape["handleExtRequest"]; readonly handleExtNotification: EffectAcpClient.AcpClientShape["handleExtNotification"]; readonly start: () => Effect.Effect<AcpSessionRuntimeStartResult, EffectAcpErrors.AcpError>; readonly getEvents: () => Stream.Stream<AcpParsedSessionEvent, never>; readonly getModeState: Effect.Effect<AcpSessionModeState | undefined>; readonly getConfigOptions: Effect.Effect<ReadonlyArray<EffectAcpSchema.SessionConfigOption>>; readonly prompt: ( payload: Omit<EffectAcpSchema.PromptRequest, "sessionId">, ) => Effect.Effect<EffectAcpSchema.PromptResponse, EffectAcpErrors.AcpError>; readonly cancel: Effect.Effect<void, EffectAcpErrors.AcpError>; readonly setMode: ( modeId: string, ) => Effect.Effect<EffectAcpSchema.SetSessionModeResponse, EffectAcpErrors.AcpError>; readonly setConfigOption: ( configId: string, value: string | boolean, ) => Effect.Effect<EffectAcpSchema.SetSessionConfigOptionResponse, EffectAcpErrors.AcpError>; readonly setModel: (model: string) => Effect.Effect<void, EffectAcpErrors.AcpError>; readonly setSessionModel: ( modelId: string, ) => Effect.Effect<EffectAcpSchema.SetSessionModelResponse, EffectAcpErrors.AcpError>; readonly request: ( method: string, payload: unknown, ) => Effect.Effect<unknown, EffectAcpErrors.AcpError>; readonly notify: ( method: string, payload: unknown, ) => Effect.Effect<void, EffectAcpErrors.AcpError>; } interface AcpStartedState extends AcpSessionRuntimeStartResult {} type AcpStartState = | { readonly _tag: "NotStarted" } | { readonly _tag: "Starting"; readonly deferred: Deferred.Deferred<AcpSessionRuntimeStartResult, EffectAcpErrors.AcpError>; } | { readonly _tag: "Started"; readonly result: AcpStartedState }; interface AcpAssistantSegmentState { readonly nextSegmentIndex: number; readonly activeItemId?: string; } interface EnsureActiveAssistantSegmentResult { readonly itemId: string; readonly startedEvent?: Extract<AcpParsedSessionEvent, { readonly _tag: "AssistantItemStarted" }>; } export class AcpSessionRuntime extends Context.Service<AcpSessionRuntime, AcpSessionRuntimeShape>()( "t3/provider/acp/AcpSessionRuntime", ) { static layer( options: AcpSessionRuntimeOptions, ): Layer.Layer< AcpSessionRuntime, EffectAcpErrors.AcpError, ChildProcessSpawner.ChildProcessSpawner > { return Layer.effect(AcpSessionRuntime, makeAcpSessionRuntime(options)); } } const makeAcpSessionRuntime = ( options: AcpSessionRuntimeOptions, ): Effect.Effect< AcpSessionRuntimeShape, EffectAcpErrors.AcpError, ChildProcessSpawner.ChildProcessSpawner | Scope.Scope > => Effect.gen(function* () { const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const runtimeScope = yield* Scope.Scope; const eventQueue = yield* Queue.unbounded<AcpParsedSessionEvent>(); const modeStateRef = yield* Ref.make<AcpSessionModeState | undefined>(undefined); const toolCallsRef = yield* Ref.make(new Map<string, AcpToolCallState>()); const assistantSegmentRef = yield* Ref.make<AcpAssistantSegmentState>({ nextSegmentIndex: 0 }); const configOptionsRef = yield* Ref.make(sessionConfigOptionsFromSetup(undefined)); const startStateRef = yield* Ref.make<AcpStartState>({ _tag: "NotStarted" }); const logRequest = (event: AcpSessionRequestLogEvent) => options.requestLogger ? options.requestLogger(event) : Effect.void; const runLoggedRequest = <A>( method: string, payload: unknown, effect: Effect.Effect<A, EffectAcpErrors.AcpError>, ): Effect.Effect<A, EffectAcpErrors.AcpError> => logRequest({ method, payload, status: "started" }).pipe( Effect.flatMap(() => effect.pipe( Effect.tap((result) => logRequest({ method, payload, status: "succeeded", result, }), ), Effect.onError((cause) => logRequest({ method, payload, status: "failed", cause, }), ), ), ), ); const child = yield* spawner .spawn( ChildProcess.make(options.spawn.command, [...options.spawn.args], { ...(options.spawn.cwd ? { cwd: options.spawn.cwd } : {}), ...(options.spawn.env ? { env: { ...process.env, ...options.spawn.env } } : {}), shell: process.platform === "win32", }), ) .pipe( Effect.provideService(Scope.Scope, runtimeScope), Effect.mapError( (cause) => new EffectAcpErrors.AcpSpawnError({ command: options.spawn.command, cause, }), ), ); const acpContext = yield* Layer.build( EffectAcpClient.layerChildProcess(child, { ...(options.protocolLogging?.logIncoming !== undefined ? { logIncoming: options.protocolLogging.logIncoming } : {}), ...(options.protocolLogging?.logOutgoing !== undefined ? { logOutgoing: options.protocolLogging.logOutgoing } : {}), ...(options.protocolLogging?.logger ? { logger: options.protocolLogging.logger } : {}), }), ).pipe(Effect.provideService(Scope.Scope, runtimeScope)); const acp = yield* Effect.service(EffectAcpClient.AcpClient).pipe(Effect.provide(acpContext)); yield* acp.handleSessionUpdate((notification) => handleSessionUpdate({ queue: eventQueue, modeStateRef, toolCallsRef, assistantSegmentRef, params: notification, }), ); const initializeClientCapabilities = { fs: { readTextFile: false, writeTextFile: false, ...options.clientCapabilities?.fs, }, terminal: options.clientCapabilities?.terminal ?? false, ...(options.clientCapabilities?.auth ? { auth: options.clientCapabilities.auth } : {}), ...(options.clientCapabilities?.elicitation ? { elicitation: options.clientCapabilities.elicitation } : {}), ...(options.clientCapabilities?._meta ? { _meta: options.clientCapabilities._meta } : {}), } satisfies NonNullable<EffectAcpSchema.InitializeRequest["clientCapabilities"]>; const getStartedState = Effect.gen(function* () { const state = yield* Ref.get(startStateRef); if (state._tag === "Started") { return state.result; } return yield* new EffectAcpErrors.AcpTransportError({ detail: "ACP session runtime has not been started", cause: "ACP session runtime has not been started", }); }); const validateConfigOptionValue = ( configId: string, value: string | boolean, ): Effect.Effect<void, EffectAcpErrors.AcpError> => Effect.gen(function* () { const configOption = findSessionConfigOption(yield* Ref.get(configOptionsRef), configId); if (!configOption) { return; } if (configOption.type === "boolean") { if (typeof value === "boolean") { return; } return yield* new EffectAcpErrors.AcpRequestError({ code: -32602, errorMessage: `Invalid value ${formatConfigOptionValue(value)} for session config option "${configOption.id}": expected boolean`, data: { configId: configOption.id, expectedType: "boolean", receivedValue: value, }, }); } if (typeof value !== "string") { return yield* new EffectAcpErrors.AcpRequestError({ code: -32602, errorMessage: `Invalid value ${formatConfigOptionValue(value)} for session config option "${configOption.id}": expected string`, data: { configId: configOption.id, expectedType: "string", receivedValue: value, }, }); } const allowedValues = collectSessionConfigOptionValues(configOption); if (allowedValues.includes(value)) { return; } return yield* new EffectAcpErrors.AcpRequestError({ code: -32602, errorMessage: `Invalid value ${formatConfigOptionValue(value)} for session config option "${configOption.id}": expected one of ${allowedValues.join(", ")}`, data: { configId: configOption.id, allowedValues, receivedValue: value, }, }); }); const updateConfigOptions = ( response: | EffectAcpSchema.SetSessionConfigOptionResponse | EffectAcpSchema.LoadSessionResponse | EffectAcpSchema.NewSessionResponse | EffectAcpSchema.ResumeSessionResponse, ): Effect.Effect<void> => Ref.set(configOptionsRef, sessionConfigOptionsFromSetup(response)); const updateCurrentModeId = (modeId: string): Effect.Effect<void> => Ref.update(modeStateRef, (current) => current ? { ...current, currentModeId: modeId } : current, ); const setConfigOption = ( configId: string, value: string | boolean, ): Effect.Effect<EffectAcpSchema.SetSessionConfigOptionResponse, EffectAcpErrors.AcpError> => validateConfigOptionValue(configId, value).pipe( Effect.flatMap(() => getStartedState), Effect.flatMap((started) => Ref.get(configOptionsRef).pipe( Effect.flatMap((configOptions) => { const existing = findSessionConfigOption(configOptions, configId); if (existing && configOptionCurrentValueMatches(existing, value)) { return Effect.succeed({ configOptions, } satisfies EffectAcpSchema.SetSessionConfigOptionResponse); } const requestPayload = typeof value === "boolean" ? ({ sessionId: started.sessionId, configId, type: "boolean", value, } satisfies EffectAcpSchema.SetSessionConfigOptionRequest) : ({ sessionId: started.sessionId, configId, value: String(value), } satisfies EffectAcpSchema.SetSessionConfigOptionRequest); return runLoggedRequest( "session/set_config_option", requestPayload, acp.agent.setSessionConfigOption(requestPayload), ).pipe(Effect.tap((response) => updateConfigOptions(response))); }), ), ), ); const startOnce = Effect.gen(function* () { const initializePayload = { protocolVersion: 1, clientCapabilities: initializeClientCapabilities, clientInfo: options.clientInfo, } satisfies EffectAcpSchema.InitializeRequest; const initializeResult = yield* runLoggedRequest( "initialize", initializePayload, acp.agent.initialize(initializePayload), ); const authenticatePayload = { methodId: options.authMethodId, } satisfies EffectAcpSchema.AuthenticateRequest; yield* runLoggedRequest( "authenticate", authenticatePayload, acp.agent.authenticate(authenticatePayload), ); let sessionId: string; let sessionSetupResult: | EffectAcpSchema.LoadSessionResponse | EffectAcpSchema.NewSessionResponse | EffectAcpSchema.ResumeSessionResponse; if (options.resumeSessionId) { const loadPayload = { sessionId: options.resumeSessionId, cwd: options.cwd, mcpServers: [], } satisfies EffectAcpSchema.LoadSessionRequest; const resumed = yield* runLoggedRequest( "session/load", loadPayload, acp.agent.loadSession(loadPayload), ).pipe(Effect.exit); if (Exit.isSuccess(resumed)) { sessionId = options.resumeSessionId; sessionSetupResult = resumed.value; } else { const createPayload = { cwd: options.cwd, mcpServers: [], } satisfies EffectAcpSchema.NewSessionRequest; const created = yield* runLoggedRequest( "session/new", createPayload, acp.agent.createSession(createPayload), ); sessionId = created.sessionId; sessionSetupResult = created; } } else { const createPayload = { cwd: options.cwd, mcpServers: [], } satisfies EffectAcpSchema.NewSessionRequest; const created = yield* runLoggedRequest( "session/new", createPayload, acp.agent.createSession(createPayload), ); sessionId = created.sessionId; sessionSetupResult = created; } yield* Ref.set(modeStateRef, parseSessionModeState(sessionSetupResult)); yield* Ref.set(configOptionsRef, sessionConfigOptionsFromSetup(sessionSetupResult)); const nextState = { sessionId, initializeResult, sessionSetupResult, modelConfigId: extractModelConfigId(sessionSetupResult), } satisfies AcpStartedState; return nextState; }); const start = Effect.gen(function* () { const deferred = yield* Deferred.make< AcpSessionRuntimeStartResult, EffectAcpErrors.AcpError >(); const effect = yield* Ref.modify(startStateRef, (state) => { switch (state._tag) { case "Started": return [Effect.succeed(state.result), state] as const; case "Starting": return [Deferred.await(state.deferred), state] as const; case "NotStarted": return [ startOnce.pipe( Effect.tap((result) => Ref.set(startStateRef, { _tag: "Started", result }).pipe( Effect.andThen(Deferred.succeed(deferred, result)), ), ), Effect.onError((cause) => Deferred.failCause(deferred, cause).pipe( Effect.andThen(Ref.set(startStateRef, { _tag: "NotStarted" })), ), ), ), { _tag: "Starting", deferred } satisfies AcpStartState, ] as const; } }); return yield* effect; }); return { handleRequestPermission: acp.handleRequestPermission, handleElicitation: acp.handleElicitation, handleReadTextFile: acp.handleReadTextFile, handleWriteTextFile: acp.handleWriteTextFile, handleCreateTerminal: acp.handleCreateTerminal, handleTerminalOutput: acp.handleTerminalOutput, handleTerminalWaitForExit: acp.handleTerminalWaitForExit, handleTerminalKill: acp.handleTerminalKill, handleTerminalRelease: acp.handleTerminalRelease, handleSessionUpdate: acp.handleSessionUpdate, handleElicitationComplete: acp.handleElicitationComplete, handleUnknownExtRequest: acp.handleUnknownExtRequest, handleUnknownExtNotification: acp.handleUnknownExtNotification, handleExtRequest: acp.handleExtRequest, handleExtNotification: acp.handleExtNotification, start: () => start,