1294 lines · 49.2 KB · TypeScript
RawDownload
import * as Cause from "effect/Cause";import * as Clock from "effect/Clock";import * as Context from "effect/Context";import * as Crypto from "effect/Crypto";import * as Deferred from "effect/Deferred";import * as Duration from "effect/Duration";import * as Effect from "effect/Effect";import * as Exit from "effect/Exit";import * as Fiber from "effect/Fiber";import * as Layer from "effect/Layer";import * as Option from "effect/Option";import * as Queue from "effect/Queue";import * as Ref from "effect/Ref";import * as Scope from "effect/Scope";import * as Semaphore from "effect/Semaphore";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,  decideToolCallUpdateEmission,  extractModelConfigId,  findSessionConfigOption,  mergeToolCallState,  toolCallProgressLength,  parseSessionModeState,  parseSessionUpdateEvent,  sessionUpdateIsReplay,  waitForSessionLoadReplayIdle,  type SessionLoadGate,  type AcpParsedSessionEvent,  type AcpSessionModeState,  type AcpToolCallState,} from "./AcpRuntimeModel.ts"; interface AcpToolCallTrackedState {  readonly state: AcpToolCallState;  readonly lastEmittedDetailLength: number | undefined;  readonly skippedSinceEmit: number;} function formatConfigOptionValue(value: string | boolean): string {  return JSON.stringify(value);} export interface AcpSessionEventStreamBarrier {  readonly _tag: "EventStreamBarrier";  readonly acknowledge: Deferred.Deferred<void>;} export type AcpSessionRuntimeEvent =  | AcpParsedSessionEvent  | AcpSessionEventStreamBarrier  | {      readonly _tag: "ConnectionTerminated";      readonly error: EffectAcpErrors.AcpError;    }; const defaultSessionLoadTimeout = Duration.seconds(90);const defaultSessionLoadReplayIdleGap = Duration.seconds(2);const defaultCancelTimeout = Duration.seconds(15);const maxStartupMetadataUpdates = 32;// Antigravity can emit an accepted 16 KiB Google authorization URL on stderr.const maxStderrChunkLength = 32_768; export interface AcpSpawnInput {  readonly command: string;  readonly args: ReadonlyArray<string>;  readonly cwd?: string;  readonly env?: NodeJS.ProcessEnv;  readonly extendEnv?: boolean;} export interface AcpSessionRuntimeOptions {  readonly spawn: AcpSpawnInput;  readonly cwd: string;  readonly resumeSessionId?: string;  readonly resumeMethod?: "load" | "resume";  readonly sessionLoadTimeout?: Duration.Input;  readonly sessionLoadReplayIdleGap?: Duration.Input;  /** Native cancellation waits for the prompt response and the getEvents consumer to drain. */  readonly cancelBehavior?: "interrupt" | "wait-for-prompt";  readonly cancelTimeout?: Duration.Input;  readonly clientCapabilities?: EffectAcpSchema.InitializeRequest["clientCapabilities"];  readonly clientInfo: {    readonly name: string;    readonly version: string;  };  readonly authMethodId: string;  readonly mcpServers?: ReadonlyArray<EffectAcpSchema.McpServer>;  /** Extra workspace roots the agent may read and write besides `cwd`. */  readonly additionalDirectories?: ReadonlyArray<string>;  /** Transforms provider stdout before protocol parsing and protocol logging. */  readonly transformStdout?: EffectAcpClient.AcpClientOptions["transformStdout"];  /** Normalizes provider-specific fields before notification queues or runtime state retain them. */  readonly transformSessionUpdate?: (    notification: EffectAcpSchema.SessionNotification,  ) => EffectAcpSchema.SessionNotification;  /** Receives bounded stderr chunks. Redact secrets before logging. A failure closes the runtime. */  readonly onStderr?: (text: string) => Effect.Effect<void, EffectAcpErrors.AcpError>;  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"];    /**     * Sends only `initialize` and returns the agent's response. Health probes use this to read     * advertised capabilities without authenticating or opening a session, so a probe can never     * start an interactive login or boot MCP servers.     * @see https://agentclientprotocol.com/protocol/schema#initialize     */    readonly initialize: () => Effect.Effect<      EffectAcpSchema.InitializeResponse,      EffectAcpErrors.AcpError    >;    /**     * 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 root-session events and connection failures. */    readonly getEvents: () => Stream.Stream<AcpSessionRuntimeEvent, never>;    /** Waits for queued events to be processed, or for the runtime scope to close. */    readonly drainEvents: Effect.Effect<void>;    /** 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. `options.dispatched` settles once the     * `session/prompt` RPC is registered as the active prompt, so a caller that forks this     * effect knows when a later `cancel` will target this prompt.     * @see https://agentclientprotocol.com/protocol/schema#session/prompt     */    readonly prompt: (      payload: Omit<EffectAcpSchema.PromptRequest, "sessionId">,      options?: { readonly dispatched?: Deferred.Deferred<void> },    ) => 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,      meta?: EffectAcpSchema.SetSessionModelRequest["_meta"],    ) => 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" }>;} interface AcpActivePrompt {  readonly fiber: Fiber.Fiber<EffectAcpSchema.PromptResponse, EffectAcpErrors.AcpError>;  readonly completed: Deferred.Deferred<void>;} export const make = (  options: AcpSessionRuntimeOptions,): Effect.Effect<  AcpSessionRuntime["Service"],  EffectAcpErrors.AcpError,  ChildProcessSpawner.ChildProcessSpawner | Crypto.Crypto | Scope.Scope> =>  Effect.gen(function* () {    const crypto = yield* Crypto.Crypto;    const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;    const runtimeScope = yield* Scope.Scope;    const eventQueue = yield* Queue.unbounded<AcpSessionRuntimeEvent>();    const modeStateRef = yield* Ref.make<AcpSessionModeState | undefined>(undefined);    const toolCallsRef = yield* Ref.make(new Map<string, AcpToolCallTrackedState>());    const assistantItemRuntimeId = yield* crypto.randomUUIDv4.pipe(      Effect.mapError(        (cause) =>          new EffectAcpErrors.AcpTransportError({            detail: "Failed to generate an ACP assistant item runtime identifier.",            cause,          }),      ),    );    const assistantSegmentRef = yield* Ref.make<AcpAssistantSegmentState>({ nextSegmentIndex: 0 });    const configOptionsRef = yield* Ref.make(sessionConfigOptionsFromSetup(undefined));    const startStateRef = yield* Ref.make<AcpStartState>({ _tag: "NotStarted" });    const startupMetadataRef = yield* Ref.make<ReadonlyArray<EffectAcpSchema.SessionNotification>>(      [],    );    const notificationSemaphore = yield* Semaphore.make(1);    const terminationErrorRef = yield* Ref.make<Option.Option<EffectAcpErrors.AcpError>>(      Option.none(),    );    const stoppingRef = yield* Ref.make(false);    const stderrFailure = yield* Deferred.make<never, EffectAcpErrors.AcpError>();    const runtimeClosed = yield* Deferred.make<void>();    const promptSerializationSemaphore = yield* Semaphore.make(1);    const promptDispatchSemaphore = yield* Semaphore.make(1);    const activePromptRef = yield* Ref.make<Option.Option<AcpActivePrompt>>(Option.none());    const sessionLoadGateRef = yield* Ref.make<Option.Option<SessionLoadGate>>(Option.none());     const ensureConnected = Effect.gen(function* () {      const error = yield* Ref.get(terminationErrorRef);      if (Option.isSome(error)) {        return yield* error.value;      }      if (yield* Ref.get(stoppingRef)) {        return yield* new EffectAcpErrors.AcpTransportError({          detail: "The ACP session runtime is closed.",          cause: undefined,        });      }    });     const recordTermination = Effect.fn("AcpSessionRuntime.recordTermination")(function* (      error: EffectAcpErrors.AcpError,    ) {      if (yield* Ref.get(stoppingRef)) {        return;      }      const firstTermination = yield* Ref.modify(terminationErrorRef, (current) =>        Option.isSome(current)          ? ([false, current] as const)          : ([true, Option.some(error)] as const),      );      if (!firstTermination) {        return;      }      yield* closeActiveAssistantSegment({ queue: eventQueue, assistantSegmentRef });      yield* Queue.offer(eventQueue, { _tag: "ConnectionTerminated", error });    });     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(() =>          (options.onStderr            ? Effect.raceFirst(effect, Deferred.await(stderrFailure))            : 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: options.spawn.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: options.spawn.extendEnv ?? true,          shell: spawnCommand.shell,        }),      )      .pipe(        Effect.provideService(Scope.Scope, runtimeScope),        Effect.mapError(          (cause) =>            new EffectAcpErrors.AcpSpawnError({              command: options.spawn.command,              cause,            }),        ),      );     yield* child.stderr.pipe(      Stream.decodeText(),      Stream.runForEach((chunk) =>        (options.onStderr          ? options.onStderr(chunk.slice(-maxStderrChunkLength))          : Effect.void        ).pipe(          Effect.catch((error) =>            Effect.gen(function* () {              yield* Deferred.fail(stderrFailure, error);              yield* recordTermination(error);              yield* child.kill({ forceKillAfter: "1 second" }).pipe(Effect.ignore);            }),          ),        ),      ),      Effect.ignore,      Effect.forkIn(runtimeScope),    );     const acpContext = yield* Layer.build(      EffectAcpClient.layerChildProcess(child, {        ...(options.transformStdout ? { transformStdout: options.transformStdout } : {}),        ...(options.transformSessionUpdate          ? { transformSessionUpdate: options.transformSessionUpdate }          : {}),        onTermination: recordTermination,        ...(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));     const processSessionUpdate = (notification: EffectAcpSchema.SessionNotification) =>      handleSessionUpdate({        queue: eventQueue,        modeStateRef,        configOptionsRef,        toolCallsRef,        assistantSegmentRef,        assistantItemRuntimeId,        params: notification,      });

Lines 1–500 of 1294.