857 lines · 31.3 KB · TypeScript
RawDownload
import * as Cause from "effect/Cause";import * as Context from "effect/Context";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 Stream from "effect/Stream";import * as ChildProcess from "effect/unstable/process/ChildProcess";import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner";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 { resolveSpawnCommand } from "@t3tools/shared/shell"; 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 mcpServers?: ReadonlyArray<EffectAcpSchema.McpServer>;  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 class AcpSessionRuntime extends Context.Service<  AcpSessionRuntime,  {    /**     * Registers a handler for `session/request_permission`.     * @see https://agentclientprotocol.com/protocol/schema#session/request_permission     */    readonly handleRequestPermission: EffectAcpClient.AcpClient["Service"]["handleRequestPermission"];    /**     * Registers a handler for `session/elicitation`.     * @see https://agentclientprotocol.com/protocol/schema#session/elicitation     */    readonly handleElicitation: EffectAcpClient.AcpClient["Service"]["handleElicitation"];    /**     * Registers a handler for `fs/read_text_file`.     * @see https://agentclientprotocol.com/protocol/schema#fs/read_text_file     */    readonly handleReadTextFile: EffectAcpClient.AcpClient["Service"]["handleReadTextFile"];    /**     * Registers a handler for `fs/write_text_file`.     * @see https://agentclientprotocol.com/protocol/schema#fs/write_text_file     */    readonly handleWriteTextFile: EffectAcpClient.AcpClient["Service"]["handleWriteTextFile"];    /**     * Registers a handler for `terminal/create`.     * @see https://agentclientprotocol.com/protocol/schema#terminal/create     */    readonly handleCreateTerminal: EffectAcpClient.AcpClient["Service"]["handleCreateTerminal"];    /**     * Registers a handler for `terminal/output`.     * @see https://agentclientprotocol.com/protocol/schema#terminal/output     */    readonly handleTerminalOutput: EffectAcpClient.AcpClient["Service"]["handleTerminalOutput"];    /**     * Registers a handler for `terminal/wait_for_exit`.     * @see https://agentclientprotocol.com/protocol/schema#terminal/wait_for_exit     */    readonly handleTerminalWaitForExit: EffectAcpClient.AcpClient["Service"]["handleTerminalWaitForExit"];    /**     * Registers a handler for `terminal/kill`.     * @see https://agentclientprotocol.com/protocol/schema#terminal/kill     */    readonly handleTerminalKill: EffectAcpClient.AcpClient["Service"]["handleTerminalKill"];    /**     * Registers a handler for `terminal/release`.     * @see https://agentclientprotocol.com/protocol/schema#terminal/release     */    readonly handleTerminalRelease: EffectAcpClient.AcpClient["Service"]["handleTerminalRelease"];    /**     * Registers a handler for `session/update`.     * @see https://agentclientprotocol.com/protocol/schema#session/update     */    readonly handleSessionUpdate: EffectAcpClient.AcpClient["Service"]["handleSessionUpdate"];    /**     * Registers a handler for `session/elicitation/complete`.     * @see https://agentclientprotocol.com/protocol/schema#session/elicitation/complete     */    readonly handleElicitationComplete: EffectAcpClient.AcpClient["Service"]["handleElicitationComplete"];    /**     * Registers a fallback extension request handler.     * @see https://agentclientprotocol.com/protocol/extensibility     */    readonly handleUnknownExtRequest: EffectAcpClient.AcpClient["Service"]["handleUnknownExtRequest"];    /**     * Registers a fallback extension notification handler.     * @see https://agentclientprotocol.com/protocol/extensibility     */    readonly handleUnknownExtNotification: EffectAcpClient.AcpClient["Service"]["handleUnknownExtNotification"];    /**     * Registers a typed extension request handler.     * @see https://agentclientprotocol.com/protocol/extensibility     */    readonly handleExtRequest: EffectAcpClient.AcpClient["Service"]["handleExtRequest"];    /**     * Registers a typed extension notification handler.     * @see https://agentclientprotocol.com/protocol/extensibility     */    readonly handleExtNotification: EffectAcpClient.AcpClient["Service"]["handleExtNotification"];    /**     * Initializes the ACP connection, authenticates, and loads, resumes, or creates the session.     * Concurrent calls share the same in-flight startup and a failed startup may be retried.     */    readonly start: () => Effect.Effect<AcpSessionRuntimeStartResult, EffectAcpErrors.AcpError>;    /** Stream of parsed ACP session events emitted after startup. */    readonly getEvents: () => Stream.Stream<AcpParsedSessionEvent, never>;    /** Latest mode state observed from session setup and `session/update` notifications. */    readonly getModeState: Effect.Effect<AcpSessionModeState | undefined>;    /** Latest configuration options observed from session setup and configuration writes. */    readonly getConfigOptions: Effect.Effect<ReadonlyArray<EffectAcpSchema.SessionConfigOption>>;    /**     * Sends a prompt turn to the active session.     * @see https://agentclientprotocol.com/protocol/schema#session/prompt     */    readonly prompt: (      payload: Omit<EffectAcpSchema.PromptRequest, "sessionId">,    ) => Effect.Effect<EffectAcpSchema.PromptResponse, EffectAcpErrors.AcpError>;    /**     * Sends a real ACP `session/cancel` notification for the active session.     * @see https://agentclientprotocol.com/protocol/schema#session/cancel     */    readonly cancel: Effect.Effect<void, EffectAcpErrors.AcpError>;    /**     * Selects the active mode through the negotiated `mode` configuration option.     * This is a no-op when the requested mode is already active.     * @see https://agentclientprotocol.com/protocol/schema#session/set_config_option     */    readonly setMode: (      modeId: string,    ) => Effect.Effect<EffectAcpSchema.SetSessionModeResponse, EffectAcpErrors.AcpError>;    /**     * Updates a session configuration option and the runtime configuration snapshot.     * @see https://agentclientprotocol.com/protocol/schema#session/set_config_option     */    readonly setConfigOption: (      configId: string,      value: string | boolean,    ) => Effect.Effect<EffectAcpSchema.SetSessionConfigOptionResponse, EffectAcpErrors.AcpError>;    /**     * Selects the base model through the negotiated model configuration option.     * @see https://agentclientprotocol.com/protocol/schema#session/set_config_option     */    readonly setModel: (model: string) => Effect.Effect<void, EffectAcpErrors.AcpError>;    /**     * Selects the active model through the unstable ACP `session/set_model` capability.     * @see https://agentclientprotocol.com/protocol/schema#session/set_model     */    readonly setSessionModel: (      modelId: string,    ) => Effect.Effect<EffectAcpSchema.SetSessionModelResponse, EffectAcpErrors.AcpError>;    /**     * Sends a generic ACP extension request and records it through the request logger.     * @see https://agentclientprotocol.com/protocol/extensibility     */    readonly request: (      method: string,      payload: unknown,    ) => Effect.Effect<unknown, EffectAcpErrors.AcpError>;    /**     * Sends a generic ACP extension notification.     * @see https://agentclientprotocol.com/protocol/extensibility     */    readonly notify: (      method: string,      payload: unknown,    ) => Effect.Effect<void, EffectAcpErrors.AcpError>;  }>()("t3/provider/acp/AcpSessionRuntime") {} 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 const make = (  options: AcpSessionRuntimeOptions,): Effect.Effect<  AcpSessionRuntime["Service"],  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 spawnCommand = yield* resolveSpawnCommand(      options.spawn.command,      options.spawn.args,      options.spawn.env ? { env: options.spawn.env, extendEnv: true } : {},    );    const child = yield* spawner      .spawn(        ChildProcess.make(spawnCommand.command, spawnCommand.args, {          ...(options.spawn.cwd ? { cwd: options.spawn.cwd } : {}),          ...(options.spawn.env ? { env: options.spawn.env, extendEnv: true } : {}),          shell: spawnCommand.shell,        }),      )      .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: options.mcpServers ?? [],

Lines 1–500 of 857.