2153 lines · 84.2 KB · TypeScript
RawDownload
import {  ApprovalRequestId,  type GrokSettings,  EventId,  type ProviderApprovalDecision,  type ProviderRuntimeEvent,  type ProviderSession,  type ProviderUserInputAnswers,  ProviderDriverKind,  ProviderInstanceId,  RuntimeRequestId,  type ThreadId,  TurnId,} from "@t3tools/contracts";import { HostProcessEnvironment, HostProcessPlatform } from "@t3tools/shared/hostProcess";import { getModelSelectionStringOptionValue } from "@t3tools/shared/model";import { stableStringify } from "@t3tools/shared/relaySigning";import * as Clock from "effect/Clock";import * as Crypto from "effect/Crypto";import * as DateTime from "effect/DateTime";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 FileSystem from "effect/FileSystem";import * as Option from "effect/Option";import * as Path from "effect/Path";import * as PubSub from "effect/PubSub";import * as Queue from "effect/Queue";import * as Ref from "effect/Ref";import * as Schema from "effect/Schema";import * as Scope from "effect/Scope";import * as Semaphore from "effect/Semaphore";import * as Stream from "effect/Stream";import * as SynchronizedRef from "effect/SynchronizedRef";import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner";import * as EffectAcpErrors from "effect-acp/errors";import type * as EffectAcpSchema from "effect-acp/schema"; import { resolveAttachmentPath } from "../../attachmentStore.ts";import { ServerConfig } from "../../config.ts";import { buildRuntimeInstructions } from "../RuntimeInstructions.ts";import * as McpProviderSession from "../../mcp/McpProviderSession.ts";import {  ProviderAdapterProcessError,  ProviderAdapterRequestError,  ProviderAdapterSessionNotFoundError,  ProviderAdapterValidationError,} from "../Errors.ts";import { mapAcpToAdapterError } from "../acp/AcpAdapterSupport.ts";import type * as AcpSessionRuntime from "../acp/AcpSessionRuntime.ts";import {  makeAcpAssistantItemEvent,  makeAcpContentDeltaEvent,  makeAcpPlanUpdatedEvent,  makeAcpRequestOpenedEvent,  makeAcpRequestResolvedEvent,  makeAcpToolCallEvent,} from "../acp/AcpCoreRuntimeEvents.ts";import { parsePermissionRequest } from "../acp/AcpRuntimeModel.ts";import { makeAcpNativeLoggerFactory } from "../acp/AcpNativeLogging.ts";import {  applyGrokAcpModelSelection,  currentGrokModelIdFromSessionSetup,  currentGrokReasoningEffortFromSessionSetup,  makeGrokAcpRuntime,  normalizeGrokReasoningEffort,  resolveGrokAcpBaseModelId,} from "../acp/GrokAcpSupport.ts";import {  extractGrokPlanMarkdownFromToolCallData,  extractXAiAskUserQuestions,  extractXAiExitPlanMarkdown,  makeXAiAskUserQuestionCancelledResponse,  makeXAiAskUserQuestionResponse,  makeXAiExitPlanModeCapturedResponse,  promptResponseHasMissingXAiStopReason,  XAiAskUserQuestionRequest,  XAiExitPlanModeRequest,} from "../acp/XAiAcpExtension.ts";import { type GrokAdapterShape } from "../Services/GrokAdapter.ts";import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogger.ts"; const encodeUnknownJsonStringExit = Schema.encodeUnknownExit(Schema.fromJsonString(Schema.Unknown)); const PROVIDER = ProviderDriverKind.make("grok");const GROK_RESUME_VERSION = 1 as const;const NANOS_PER_MILLI = 1_000_000n;// ACP does not expose Grok's private `streaming_reasoning` phase. Once it has// emitted standard ACP progress, ten silent minutes is long enough to avoid// treating legitimate reasoning as a stalled stream.const DEFAULT_GROK_TURN_INACTIVITY_TIMEOUT_MS = 10 * 60 * 1_000;// A tool can legitimately run without emitting text for much longer than// reasoning. It still needs a deadline so a lost tool update cannot leave the// turn working forever.const DEFAULT_GROK_ACTIVE_TOOL_INACTIVITY_TIMEOUT_MS = 30 * 60 * 1_000; function encodeJsonStringForDiagnostics(input: unknown): string | undefined {  const result = encodeUnknownJsonStringExit(input);  return Exit.isSuccess(result) ? result.value : undefined;} export interface GrokAdapterLiveOptions {  readonly environment?: NodeJS.ProcessEnv;  readonly nativeEventLogPath?: string;  readonly nativeEventLogger?: EventNdjsonLogger;  readonly instanceId?: ProviderInstanceId;  /** Override the conservative ACP turn liveness timeout in focused tests. */  readonly turnInactivityTimeoutMs?: number;  /** Override the longer active-tool liveness timeout in focused tests. */  readonly activeToolInactivityTimeoutMs?: number;} interface PendingApproval {  readonly decision: Deferred.Deferred<ProviderApprovalDecision>;} type PendingUserInputResolution =  | { readonly _tag: "answered"; readonly answers: ProviderUserInputAnswers }  | { readonly _tag: "cancelled" }; interface PendingUserInput {  readonly resolution: Deferred.Deferred<PendingUserInputResolution>;} interface GrokTurnLivenessSignal {  readonly turnId: TurnId;} interface GrokSessionContext {  readonly threadId: ThreadId;  readonly acpSessionId: string;  session: ProviderSession;  readonly scope: Scope.Closeable;  readonly acp: AcpSessionRuntime.AcpSessionRuntime["Service"];  notificationFiber: Fiber.Fiber<void, never> | undefined;  readonly pendingApprovals: Map<ApprovalRequestId, PendingApproval>;  readonly pendingUserInputs: Map<ApprovalRequestId, PendingUserInput>;  turns: Array<{ id: TurnId; items: Array<unknown> }>;  lastPlanFingerprint: string | undefined;  /**   * Latest plan.md body + turn it was emitted for. Dedupe is turn-scoped so a   * later turn re-proposing the same text still gets a new proposed-plan card.   */  lastKnownProposedPlanMarkdown: string | undefined;  lastKnownProposedPlanTurnId: TurnId | undefined;  /** True after enter_plan_mode until the turn ends or exit_plan_mode resolves. */  planModeActive: boolean;  activeTurnId: TurnId | undefined;  /** Turns already interrupted; late prompt RPCs must not resurrect them. */  interruptedTurnIds: Set<TurnId>;  /** Number of sendTurn prompts currently in flight or being prepared.   * >0 means a turn is actively running, so a new sendTurn is a steer that   * cancels the in-flight prompt and continues the same turn. Only the last   * remaining prompt settles the turn. */  promptsInFlight: number;  /** Monotonic id assigned to each sendTurn. Steers discard older epochs. */  promptEpoch: number;  /** Prompt epochs below this value must not start an ACP session/prompt. */  discardBeforeEpoch: number;  /** Serializes cancel-then-prompt so a steer cannot miss or hit the wrong RPC. */  readonly promptLifecycle: Semaphore.Semaphore;  readonly livenessSignals: Queue.Queue<GrokTurnLivenessSignal>;  livenessTurnId: TurnId | undefined;  lastTurnActivityAtNanos: bigint | undefined;  readonly activeToolCallIds: Set<string>;  livenessUpdatesInFlight: number;  /** Prompt RPCs that returned before their turn settlement acquired the lock. */  promptResponsesReady: number;  currentModelId: string | undefined;  currentReasoningEffort: string | undefined;  stopped: boolean;} function settlePendingApprovalsAsCancelled(  pendingApprovals: ReadonlyMap<ApprovalRequestId, PendingApproval>,): Effect.Effect<void> {  return Effect.forEach(    Array.from(pendingApprovals.values()),    (pending) => Deferred.succeed(pending.decision, "cancel").pipe(Effect.ignore),    { discard: true },  );} function settlePendingUserInputsAsCancelled(  pendingUserInputs: ReadonlyMap<ApprovalRequestId, PendingUserInput>,): Effect.Effect<void> {  return Effect.forEach(    Array.from(pendingUserInputs.values()),    (pending) => Deferred.succeed(pending.resolution, { _tag: "cancelled" }).pipe(Effect.ignore),    { discard: true },  );} function appendPromptResultToTurn(  ctx: GrokSessionContext,  turnId: TurnId,  promptParts: ReadonlyArray<EffectAcpSchema.ContentBlock>,  result: EffectAcpSchema.PromptResponse,): void {  const existingTurnRecord = ctx.turns.find((turn) => turn.id === turnId);  ctx.turns = existingTurnRecord    ? ctx.turns.map((turn) =>        turn.id === turnId          ? { ...turn, items: [...turn.items, { prompt: promptParts, result }] }          : turn,      )    : [...ctx.turns, { id: turnId, items: [{ prompt: promptParts, result }] }];} function isRecord(value: unknown): value is Record<string, unknown> {  return typeof value === "object" && value !== null && !Array.isArray(value);} const resolveNotificationTurnId = (ctx: GrokSessionContext): TurnId | undefined => ctx.activeTurnId; const resolveCallbackTurnId = (ctx: GrokSessionContext): TurnId | undefined => ctx.activeTurnId; function clearProposedPlanFallback(ctx: GrokSessionContext): void {  ctx.lastKnownProposedPlanMarkdown = undefined;  ctx.lastKnownProposedPlanTurnId = undefined;  ctx.planModeActive = false;} /** Detect Grok's enter_plan_mode tool call from ACP tool state. */export function isGrokEnterPlanModeToolCall(toolCall: {  readonly title?: string;  readonly data: Record<string, unknown>;}): boolean {  const title = toolCall.title?.trim().toLowerCase() ?? "";  if (    title === "enter_plan_mode" ||    title === "plan: enter" ||    title === "plan mode entered" ||    title.includes("enter_plan_mode")  ) {    return true;  }  const rawInput = toolCall.data.rawInput;  if (isRecord(rawInput) && rawInput.variant === "EnterPlanMode") {    return true;  }  return false;} /** Failed enter_plan_mode must not leave planModeActive stuck on. */export function nextGrokPlanModeActive(  currentlyActive: boolean,  toolCall: {    readonly title?: string;    readonly status?: "pending" | "inProgress" | "completed" | "failed";    readonly data: Record<string, unknown>;  },): boolean {  if (!isGrokEnterPlanModeToolCall(toolCall)) {    return currentlyActive;  }  if (toolCall.status === "failed") {    return false;  }  if (toolCall.status === "completed" || toolCall.status === "inProgress") {    return true;  }  return currentlyActive;} const resolveSessionCallbackTurnId = (  sessions: ReadonlyMap<ThreadId, GrokSessionContext>,  threadId: ThreadId,): TurnId | undefined => {  const ctx = sessions.get(threadId);  return ctx ? resolveCallbackTurnId(ctx) : undefined;}; function parseGrokResume(raw: unknown): { sessionId: string } | undefined {  if (!isRecord(raw)) return undefined;  if (raw.schemaVersion !== GROK_RESUME_VERSION) return undefined;  if (typeof raw.sessionId !== "string" || !raw.sessionId.trim()) return undefined;  return { sessionId: raw.sessionId.trim() };} export function selectGrokPermissionOptionId(  request: EffectAcpSchema.RequestPermissionRequest,  decision: Exclude<ProviderApprovalDecision, "cancel">,): string | undefined {  const preferredKind =    decision === "acceptForSession"      ? "allow_always"      : decision === "accept"        ? "allow_once"        : "reject_once";  const preferred = request.options.find((entry) => entry.kind === preferredKind);  const preferredId = preferred?.optionId.trim();  if (preferredId) {    return preferredId;  }  // Grok 4.6 often omits allow_always. T3 still offers "Always allow this session".  if (decision === "acceptForSession") {    const once = request.options.find((entry) => entry.kind === "allow_once");    const onceId = once?.optionId.trim();    if (onceId) {      return onceId;    }  }  return undefined;} function selectAutoApprovedPermissionOption(  request: EffectAcpSchema.RequestPermissionRequest,): string | undefined {  return (    selectGrokPermissionOptionId(request, "acceptForSession") ??    selectGrokPermissionOptionId(request, "accept")  );} function completedStopReasonFromPromptResponse(  response: EffectAcpSchema.PromptResponse | undefined,): EffectAcpSchema.StopReason | null {  if (response === undefined || promptResponseHasMissingXAiStopReason(response)) {    return null;  }  return response.stopReason;} export function grokPromptSettlementBelongsToContext(input: {  readonly liveAcpSessionId: string;  readonly expectedAcpSessionId: string;  readonly liveActiveTurnId: TurnId | undefined;  readonly liveSessionActiveTurnId: TurnId | undefined;  readonly turnId: TurnId;}): boolean {  return (    input.liveAcpSessionId === input.expectedAcpSessionId &&    (input.liveActiveTurnId === input.turnId || input.liveSessionActiveTurnId === input.turnId)  );} export function makeGrokAdapter(grokSettings: GrokSettings, options?: GrokAdapterLiveOptions) {  return Effect.gen(function* () {    const boundInstanceId = options?.instanceId ?? ProviderInstanceId.make("grok");    const fileSystem = yield* FileSystem.FileSystem;    const path = yield* Path.Path;    const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;    const serverConfig = yield* Effect.service(ServerConfig);    const crypto = yield* Crypto.Crypto;    const nativeEventLogger =      options?.nativeEventLogger ??      (options?.nativeEventLogPath !== undefined        ? yield* makeEventNdjsonLogger(options.nativeEventLogPath, { stream: "native" })        : undefined);    const managedNativeEventLogger =      options?.nativeEventLogger === undefined ? nativeEventLogger : undefined;    const makeAcpNativeLoggers = yield* makeAcpNativeLoggerFactory();    const hostPlatform = yield* HostProcessPlatform;    const hostEnvironment = yield* HostProcessEnvironment;    const grokPlanPathHost = {      platform: hostPlatform,      environment: options?.environment ?? hostEnvironment,    };     const sessions = new Map<ThreadId, GrokSessionContext>();    const threadLocksRef = yield* SynchronizedRef.make(new Map<string, Semaphore.Semaphore>());    const runtimeEventPubSub = yield* PubSub.unbounded<ProviderRuntimeEvent>();    const requestedTurnInactivityTimeoutMs = options?.turnInactivityTimeoutMs;    const turnInactivityTimeoutMs =      typeof requestedTurnInactivityTimeoutMs === "number" &&      Number.isFinite(requestedTurnInactivityTimeoutMs)        ? Math.max(1, Math.floor(requestedTurnInactivityTimeoutMs))        : DEFAULT_GROK_TURN_INACTIVITY_TIMEOUT_MS;    const turnInactivityTimeoutNanos = BigInt(turnInactivityTimeoutMs) * NANOS_PER_MILLI;    const requestedActiveToolInactivityTimeoutMs = options?.activeToolInactivityTimeoutMs;    const activeToolInactivityTimeoutMs =      typeof requestedActiveToolInactivityTimeoutMs === "number" &&      Number.isFinite(requestedActiveToolInactivityTimeoutMs)        ? Math.max(1, Math.floor(requestedActiveToolInactivityTimeoutMs))        : DEFAULT_GROK_ACTIVE_TOOL_INACTIVITY_TIMEOUT_MS;    const activeToolInactivityTimeoutNanos =      BigInt(activeToolInactivityTimeoutMs) * NANOS_PER_MILLI;     const nowIso = Effect.map(DateTime.now, DateTime.formatIso);    const randomUUIDv4 = crypto.randomUUIDv4.pipe(      Effect.mapError(        (cause) =>          new ProviderAdapterRequestError({            provider: PROVIDER,            method: "crypto/randomUUIDv4",            detail: "Failed to generate Grok runtime identifier.",            cause,          }),      ),    );    const nextEventId = Effect.map(randomUUIDv4, (id) => EventId.make(id));    const makeEventStamp = () => Effect.all({ eventId: nextEventId, createdAt: nowIso });    const mapAcpCallbackFailure = <A, E, R>(effect: Effect.Effect<A, E, R>) =>      effect.pipe(        Effect.mapError(          (cause) =>            new EffectAcpErrors.AcpTransportError({              detail: "Failed to process Grok ACP callback.",              cause,            }),        ),      );     const offerRuntimeEvent = (event: ProviderRuntimeEvent) =>      PubSub.publish(runtimeEventPubSub, event).pipe(Effect.asVoid);     const getThreadSemaphore = (threadId: string) =>      SynchronizedRef.modifyEffect(threadLocksRef, (current) => {        const existing: Option.Option<Semaphore.Semaphore> = Option.fromNullishOr(          current.get(threadId),        );        return Option.match(existing, {          onNone: () =>            Semaphore.make(1).pipe(              Effect.map((semaphore) => {                const next = new Map(current);                next.set(threadId, semaphore);                return [semaphore, next] as const;              }),            ),          onSome: (semaphore) => Effect.succeed([semaphore, current] as const),        });      });     const withThreadLock = <A, E, R>(threadId: string, effect: Effect.Effect<A, E, R>) =>      Effect.flatMap(getThreadSemaphore(threadId), (semaphore) => semaphore.withPermit(effect));     const signalTurnLiveness = (ctx: GrokSessionContext, turnId: TurnId) =>      Queue.offer(ctx.livenessSignals, { turnId }).pipe(Effect.asVoid);     const beginTurnLiveness = (ctx: GrokSessionContext, turnId: TurnId) =>      Effect.sync(() => {        ctx.livenessTurnId = turnId;        // Do not start a deadline until ACP has made observable progress.        // Grok's private reasoning phase is not present in the ACP stream.        ctx.lastTurnActivityAtNanos = undefined;        ctx.activeToolCallIds.clear();      });     const clearTurnLiveness = (ctx: GrokSessionContext) => {      const turnId = ctx.livenessTurnId;      ctx.livenessTurnId = undefined;      ctx.lastTurnActivityAtNanos = undefined;      ctx.activeToolCallIds.clear();      ctx.livenessUpdatesInFlight = 0;      ctx.promptResponsesReady = 0;      return turnId === undefined ? Effect.void : signalTurnLiveness(ctx, turnId);    };     const recordTurnActivity = Effect.fn("GrokAdapter.recordTurnActivity")(function* (      ctx: GrokSessionContext,      turnId: TurnId,      event: Extract<        AcpSessionRuntime.AcpSessionRuntimeEvent,        {          _tag:            | "AssistantItemStarted"            | "AssistantItemCompleted"            | "PlanUpdated"            | "ToolCallUpdated"            | "ContentDelta";        }      >,    ) {      if (        ctx.livenessTurnId !== turnId ||        (event._tag === "ContentDelta" && event.text.length === 0)      ) {        return;      }      ctx.livenessUpdatesInFlight += 1;      try {        const activityAtNanos = yield* Clock.monotonicTimeNanos;        if (ctx.livenessTurnId !== turnId || ctx.interruptedTurnIds.has(turnId)) {          return;        }        if (event._tag === "ToolCallUpdated") {          if (event.toolCall.status === "completed" || event.toolCall.status === "failed") {            ctx.activeToolCallIds.delete(event.toolCall.toolCallId);          } else {            // A tool update without a terminal status receives a longer            // deadline so a long-running tool is not mistaken for a stall.            ctx.activeToolCallIds.add(event.toolCall.toolCallId);          }        }        ctx.lastTurnActivityAtNanos = activityAtNanos;      } finally {        // Decrement before signaling. The watchdog treats in-flight updates as a        // pause; if it consumed a signal while the counter was still > 0 it would        // wait on the next take with no follow-up wake after this decrement.        ctx.livenessUpdatesInFlight = Math.max(0, ctx.livenessUpdatesInFlight - 1);        yield* signalTurnLiveness(ctx, turnId);      }    });     const hasLivenessPause = (ctx: GrokSessionContext) =>      ctx.pendingApprovals.size > 0 ||

Lines 1–500 of 2153.