import { ApprovalRequestId, type GrokSettings, EventId, type ProviderApprovalDecision, type ProviderRuntimeEvent, type ProviderSession, type ProviderUserInputAnswers, ProviderDriverKind, ProviderInstanceId, RuntimeRequestId, type ThreadId, TurnId, } from "@t3tools/contracts"; import * as Crypto from "effect/Crypto"; import * as DateTime from "effect/DateTime"; import * as Deferred from "effect/Deferred"; 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 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 * 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, makeGrokAcpRuntime, resolveGrokAcpBaseModelId, } from "../acp/GrokAcpSupport.ts"; import { extractXAiAskUserQuestions, makeXAiAskUserQuestionCancelledResponse, makeXAiAskUserQuestionResponse, XAiAskUserQuestionRequest, } from "../acp/XAiAcpExtension.ts"; import { type GrokAdapterShape } from "../Services/GrokAdapter.ts"; import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogger.ts"; const encodeUnknownJsonStringExit = Schema.encodeUnknownExit(Schema.UnknownFromJsonString); const PROVIDER = ProviderDriverKind.make("grok"); const GROK_RESUME_VERSION = 1 as const; 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; } interface PendingApproval { readonly decision: Deferred.Deferred; } type PendingUserInputResolution = | { readonly _tag: "answered"; readonly answers: ProviderUserInputAnswers } | { readonly _tag: "cancelled" }; interface PendingUserInput { readonly resolution: Deferred.Deferred; } interface GrokSessionContext { readonly threadId: ThreadId; readonly acpSessionId: string; session: ProviderSession; readonly scope: Scope.Closeable; readonly acp: AcpSessionRuntime.AcpSessionRuntime["Service"]; notificationFiber: Fiber.Fiber | undefined; readonly pendingApprovals: Map; readonly pendingUserInputs: Map; turns: Array<{ id: TurnId; items: Array }>; lastPlanFingerprint: string | undefined; activeTurnId: TurnId | undefined; /** 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 * continues it, and only the last remaining prompt settles the turn. */ promptsInFlight: number; currentModelId: string | undefined; stopped: boolean; } function settlePendingApprovalsAsCancelled( pendingApprovals: ReadonlyMap, ): Effect.Effect { return Effect.forEach( Array.from(pendingApprovals.values()), (pending) => Deferred.succeed(pending.decision, "cancel").pipe(Effect.ignore), { discard: true }, ); } function settlePendingUserInputsAsCancelled( pendingUserInputs: ReadonlyMap, ): Effect.Effect { return Effect.forEach( Array.from(pendingUserInputs.values()), (pending) => Deferred.succeed(pending.resolution, { _tag: "cancelled" }).pipe(Effect.ignore), { discard: true }, ); } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } 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() }; } function selectPermissionOptionId( request: EffectAcpSchema.RequestPermissionRequest, decision: Exclude, ): string | undefined { const kind = decision === "acceptForSession" ? "allow_always" : decision === "accept" ? "allow_once" : "reject_once"; const option = request.options.find((entry) => entry.kind === kind); return option?.optionId.trim() || undefined; } function selectAutoApprovedPermissionOption( request: EffectAcpSchema.RequestPermissionRequest, ): string | undefined { return ( selectPermissionOptionId(request, "acceptForSession") ?? selectPermissionOptionId(request, "accept") ); } 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 sessions = new Map(); const threadLocksRef = yield* SynchronizedRef.make(new Map()); const runtimeEventPubSub = yield* PubSub.unbounded(); 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 = (effect: Effect.Effect) => 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 = 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 = (threadId: string, effect: Effect.Effect) => Effect.flatMap(getThreadSemaphore(threadId), (semaphore) => semaphore.withPermit(effect)); const logNative = (threadId: ThreadId, method: string, payload: unknown) => Effect.gen(function* () { if (!nativeEventLogger) return; const observedAt = yield* nowIso; yield* nativeEventLogger.write( { observedAt, event: { id: yield* randomUUIDv4, kind: "notification", provider: PROVIDER, createdAt: observedAt, method, threadId, payload, }, }, threadId, ); }).pipe( Effect.catchCause((cause) => Effect.logWarning("Failed to write native Grok notification log.", { cause, threadId, method, }), ), ); const emitPlanUpdate = ( ctx: GrokSessionContext, payload: { readonly explanation?: string | null; readonly plan: ReadonlyArray<{ readonly step: string; readonly status: "pending" | "inProgress" | "completed"; }>; }, rawPayload: unknown, method: string, ) => Effect.gen(function* () { const fingerprint = `${ctx.activeTurnId ?? "no-turn"}:${encodeJsonStringForDiagnostics(payload) ?? "[unserializable payload]"}`; if (ctx.lastPlanFingerprint === fingerprint) { return; } ctx.lastPlanFingerprint = fingerprint; yield* offerRuntimeEvent( makeAcpPlanUpdatedEvent({ stamp: yield* makeEventStamp(), provider: PROVIDER, threadId: ctx.threadId, turnId: ctx.activeTurnId, payload, source: "acp.jsonrpc", method, rawPayload, }), ); }); const requireSession = ( threadId: ThreadId, ): Effect.Effect => { const ctx = sessions.get(threadId); if (!ctx || ctx.stopped) { return Effect.fail( new ProviderAdapterSessionNotFoundError({ provider: PROVIDER, threadId }), ); } return Effect.succeed(ctx); }; const stopSessionInternal = (ctx: GrokSessionContext) => Effect.gen(function* () { if (ctx.stopped) return; ctx.stopped = true; yield* settlePendingApprovalsAsCancelled(ctx.pendingApprovals); yield* settlePendingUserInputsAsCancelled(ctx.pendingUserInputs); if (ctx.notificationFiber) { yield* Fiber.interrupt(ctx.notificationFiber); } yield* Effect.ignore(Scope.close(ctx.scope, Exit.void)); sessions.delete(ctx.threadId); yield* offerRuntimeEvent({ type: "session.exited", ...(yield* makeEventStamp()), provider: PROVIDER, threadId: ctx.threadId, payload: { exitKind: "graceful" }, }); }); const startSession: GrokAdapterShape["startSession"] = (input) => withThreadLock( input.threadId, Effect.gen(function* () { if (input.provider !== undefined && input.provider !== PROVIDER) { return yield* new ProviderAdapterValidationError({ provider: PROVIDER, operation: "startSession", issue: `Expected provider '${PROVIDER}' but received '${input.provider}'.`, }); } if (!input.cwd?.trim()) { return yield* new ProviderAdapterValidationError({ provider: PROVIDER, operation: "startSession", issue: "cwd is required and must be non-empty.", }); } const cwd = path.resolve(input.cwd.trim()); const grokModelSelection = input.modelSelection?.instanceId === boundInstanceId ? input.modelSelection : undefined; const existing = sessions.get(input.threadId); if (existing && !existing.stopped) { yield* stopSessionInternal(existing); } const pendingApprovals = new Map(); const pendingUserInputs = new Map(); const sessionScope = yield* Scope.make("sequential"); let sessionScopeTransferred = false; yield* Effect.addFinalizer(() => sessionScopeTransferred ? Effect.void : Scope.close(sessionScope, Exit.void), ); const resumeSessionId = parseGrokResume(input.resumeCursor)?.sessionId; const acpNativeLoggers = makeAcpNativeLoggers({ nativeEventLogger, provider: PROVIDER, threadId: input.threadId, }); const mcpSession = McpProviderSession.readMcpProviderSession(input.threadId); const acp = yield* makeGrokAcpRuntime({ grokSettings, ...(options?.environment ? { environment: options.environment } : {}), childProcessSpawner, cwd, ...(resumeSessionId ? { resumeSessionId } : {}), clientInfo: { name: "t3-code", version: "0.0.0" }, ...(mcpSession ? { mcpServers: [ { type: "http" as const, name: "t3-code", url: mcpSession.endpoint, headers: [ { name: "Authorization", value: mcpSession.authorizationHeader, }, ], }, ], } : {}), ...acpNativeLoggers, }).pipe( Effect.provideService(Scope.Scope, sessionScope), Effect.mapError( (cause) => new ProviderAdapterProcessError({ provider: PROVIDER, threadId: input.threadId, detail: cause.message, cause, }), ), ); const started = yield* Effect.gen(function* () { yield* Effect.forEach( ["x.ai/ask_user_question", "_x.ai/ask_user_question"] as const, (method) => acp.handleExtRequest(method, XAiAskUserQuestionRequest, (params) => mapAcpCallbackFailure( Effect.gen(function* () { yield* logNative(input.threadId, method, params); const requestId = ApprovalRequestId.make(yield* randomUUIDv4); const runtimeRequestId = RuntimeRequestId.make(requestId); const resolution = yield* Deferred.make(); pendingUserInputs.set(requestId, { resolution }); yield* offerRuntimeEvent({ type: "user-input.requested", ...(yield* makeEventStamp()), provider: PROVIDER, threadId: input.threadId, turnId: sessions.get(input.threadId)?.activeTurnId, requestId: runtimeRequestId, payload: { questions: extractXAiAskUserQuestions(params) }, raw: { source: "acp.grok.extension", method, payload: params, }, }); const resolved = yield* Deferred.await(resolution); pendingUserInputs.delete(requestId); const resolvedAnswers = resolved._tag === "answered" ? resolved.answers : {}; yield* offerRuntimeEvent({ type: "user-input.resolved", ...(yield* makeEventStamp()), provider: PROVIDER, threadId: input.threadId, turnId: sessions.get(input.threadId)?.activeTurnId, requestId: runtimeRequestId, payload: { answers: resolvedAnswers }, raw: { source: "acp.grok.extension", method, payload: params, }, }); switch (resolved._tag) { case "answered": return makeXAiAskUserQuestionResponse(params, resolved.answers); case "cancelled": return makeXAiAskUserQuestionCancelledResponse(); } }), ), ), { discard: true }, ); yield* acp.handleRequestPermission((params) => mapAcpCallbackFailure( Effect.gen(function* () { yield* logNative(input.threadId, "session/request_permission", params); if (input.runtimeMode === "full-access") { const autoApprovedOptionId = selectAutoApprovedPermissionOption(params); if (autoApprovedOptionId !== undefined) { return { outcome: { outcome: "selected" as const, optionId: autoApprovedOptionId, }, }; } } const permissionRequest = parsePermissionRequest(params); const requestId = ApprovalRequestId.make(yield* randomUUIDv4); const runtimeRequestId = RuntimeRequestId.make(requestId); const decision = yield* Deferred.make(); pendingApprovals.set(requestId, { decision }); yield* offerRuntimeEvent( makeAcpRequestOpenedEvent({ stamp: yield* makeEventStamp(), provider: PROVIDER, threadId: input.threadId, turnId: sessions.get(input.threadId)?.activeTurnId, requestId: runtimeRequestId, permissionRequest, detail: permissionRequest.detail ?? encodeJsonStringForDiagnostics(params)?.slice(0, 2000) ?? "[unserializable params]", args: params, source: "acp.jsonrpc", method: "session/request_permission", rawPayload: params, }), ); const resolved = yield* Deferred.await(decision); pendingApprovals.delete(requestId); yield* offerRuntimeEvent( makeAcpRequestResolvedEvent({ stamp: yield* makeEventStamp(), provider: PROVIDER, threadId: input.threadId, turnId: sessions.get(input.threadId)?.activeTurnId, requestId: runtimeRequestId, permissionRequest, decision: resolved, }), ); const selectedOptionId = resolved === "cancel" ? undefined : selectPermissionOptionId(params, resolved); return { outcome: selectedOptionId ? { outcome: "selected" as const, optionId: selectedOptionId, } : ({ outcome: "cancelled" } as const), }; }), ), ); return yield* acp.start(); }).pipe( Effect.mapError((error) => mapAcpToAdapterError(PROVIDER, input.threadId, "session/start", error), ), ); const requestedStartModelId = grokModelSelection?.model ? resolveGrokAcpBaseModelId(grokModelSelection.model) : undefined; const boundModelId = yield* applyGrokAcpModelSelection({ runtime: acp, currentModelId: currentGrokModelIdFromSessionSetup(started.sessionSetupResult), requestedModelId: requestedStartModelId, mapError: (cause) => mapAcpToAdapterError(PROVIDER, input.threadId, "session/set_model", cause), }); const now = yield* nowIso; const session: ProviderSession = { provider: PROVIDER, providerInstanceId: boundInstanceId, status: "ready", runtimeMode: input.runtimeMode, cwd, ...(boundModelId ? { model: resolveGrokAcpBaseModelId(boundModelId) } : {}), threadId: input.threadId, resumeCursor: { schemaVersion: GROK_RESUME_VERSION, sessionId: started.sessionId, }, createdAt: now, updatedAt: now, }; const ctx: GrokSessionContext = { threadId: input.threadId, acpSessionId: started.sessionId, session, scope: sessionScope, acp, notificationFiber: undefined, pendingApprovals, pendingUserInputs, turns: [], lastPlanFingerprint: undefined, activeTurnId: undefined, promptsInFlight: 0, currentModelId: boundModelId, stopped: false, }; const nf = yield* Stream.runDrain( Stream.mapEffect(acp.getEvents(), (event) => Effect.gen(function* () { switch (event._tag) { case "AssistantItemStarted": yield* offerRuntimeEvent( makeAcpAssistantItemEvent({ stamp: yield* makeEventStamp(), provider: PROVIDER, threadId: ctx.threadId, turnId: ctx.activeTurnId, itemId: event.itemId, lifecycle: "item.started", }), ); return; case "AssistantItemCompleted": yield* offerRuntimeEvent( makeAcpAssistantItemEvent({ stamp: yield* makeEventStamp(), provider: PROVIDER, threadId: ctx.threadId, turnId: ctx.activeTurnId, itemId: event.itemId, lifecycle: "item.completed", }), ); return; case "PlanUpdated": yield* logNative(ctx.threadId, "session/update", event.rawPayload); yield* emitPlanUpdate(ctx, event.payload, event.rawPayload, "session/update"); return; case "ToolCallUpdated": yield* logNative(ctx.threadId, "session/update", event.rawPayload); yield* offerRuntimeEvent( makeAcpToolCallEvent({ stamp: yield* makeEventStamp(), provider: PROVIDER, threadId: ctx.threadId, turnId: ctx.activeTurnId, toolCall: event.toolCall, rawPayload: event.rawPayload, }), ); return; case "ContentDelta": yield* logNative(ctx.threadId, "session/update", event.rawPayload); yield* offerRuntimeEvent( makeAcpContentDeltaEvent({ stamp: yield* makeEventStamp(), provider: PROVIDER, threadId: ctx.threadId, turnId: ctx.activeTurnId, ...(event.itemId ? { itemId: event.itemId } : {}), text: event.text, rawPayload: event.rawPayload, }), ); return; } }), ), ).pipe( Effect.catch((cause) => Effect.logError("Failed to process Grok runtime notification.", { cause }), ), Effect.forkChild, ); ctx.notificationFiber = nf; sessions.set(input.threadId, ctx); sessionScopeTransferred = true; yield* offerRuntimeEvent({ type: "session.started", ...(yield* makeEventStamp()), provider: PROVIDER, threadId: input.threadId, payload: { resume: started.initializeResult }, }); yield* offerRuntimeEvent({ type: "session.state.changed", ...(yield* makeEventStamp()), provider: PROVIDER, threadId: input.threadId, payload: { state: "ready", reason: "Grok ACP session ready" }, }); yield* offerRuntimeEvent({ type: "thread.started", ...(yield* makeEventStamp()), provider: PROVIDER, threadId: input.threadId, payload: { providerThreadId: started.sessionId }, }); return session; }).pipe(Effect.scoped), ); const sendTurn: GrokAdapterShape["sendTurn"] = (input) => Effect.gen(function* () { const prepared = yield* withThreadLock( input.threadId, Effect.gen(function* () { const ctx = yield* requireSession(input.threadId); // A sendTurn while a prompt is in flight is a steer: the agent // folds the new prompt into the ongoing work, so the active turn // id is reused instead of opening a new turn. const steeringTurnId = ctx.promptsInFlight > 0 ? ctx.activeTurnId : undefined; const turnId = steeringTurnId ?? TurnId.make(yield* randomUUIDv4); // Count this prompt immediately so a superseded in-flight prompt // resolving from here on does not settle the turn; decremented on // preparation failure here, and after the prompt below otherwise. ctx.promptsInFlight += 1; return yield* Effect.gen(function* () { const turnModelSelection = input.modelSelection?.instanceId === boundInstanceId ? input.modelSelection : undefined; const requestedTurnModelId = turnModelSelection?.model ? resolveGrokAcpBaseModelId(turnModelSelection.model) : undefined; const currentModelId = yield* applyGrokAcpModelSelection({ runtime: ctx.acp, currentModelId: ctx.currentModelId, requestedModelId: requestedTurnModelId, mapError: (cause) => mapAcpToAdapterError(PROVIDER, input.threadId, "session/set_model", cause), }); const text = input.input?.trim(); const imagePromptParts = yield* Effect.forEach( input.attachments ?? [], (attachment) => Effect.gen(function* () { const attachmentPath = resolveAttachmentPath({ attachmentsDir: serverConfig.attachmentsDir, attachment, }); if (!attachmentPath) { return yield* new ProviderAdapterRequestError({ provider: PROVIDER, method: "session/prompt", detail: `Invalid attachment id '${attachment.id}'.`, }); } const bytes = yield* fileSystem.readFile(attachmentPath).pipe( Effect.mapError( (cause) => new ProviderAdapterRequestError({ provider: PROVIDER, method: "session/prompt", detail: cause.message, cause, }), ), ); return { type: "image", data: Buffer.from(bytes).toString("base64"), mimeType: attachment.mimeType, } satisfies EffectAcpSchema.ContentBlock; }), ); const promptParts: Array = [ ...(text ? [{ type: "text" as const, text }] : []), ...imagePromptParts, ]; if (promptParts.length === 0) { return yield* new ProviderAdapterValidationError({ provider: PROVIDER, operation: "sendTurn", issue: "Turn requires non-empty text or attachments.", }); } ctx.currentModelId = currentModelId; const displayModel = currentModelId ? resolveGrokAcpBaseModelId(currentModelId) : undefined; ctx.activeTurnId = turnId; if (steeringTurnId === undefined) { ctx.lastPlanFingerprint = undefined; } ctx.session = { ...ctx.session, activeTurnId: turnId, updatedAt: yield* nowIso, ...(displayModel ? { model: displayModel } : {}), }; if (steeringTurnId === undefined) { yield* offerRuntimeEvent({ type: "turn.started", ...(yield* makeEventStamp()), provider: PROVIDER, threadId: input.threadId, turnId, payload: displayModel ? { model: displayModel } : {}, }); } return { acp: ctx.acp, acpSessionId: ctx.acpSessionId, displayModel, promptParts, turnId, }; }).pipe( Effect.tapCause(() => Effect.sync(() => { ctx.promptsInFlight = Math.max(0, ctx.promptsInFlight - 1); }), ), ); }), ); return yield* Effect.gen(function* () { const result = yield* prepared.acp .prompt({ prompt: prepared.promptParts, }) .pipe( Effect.mapError((error) => mapAcpToAdapterError(PROVIDER, input.threadId, "session/prompt", error), ), ); return yield* withThreadLock( input.threadId, Effect.gen(function* () { const ctx = yield* requireSession(input.threadId); if (ctx.acpSessionId !== prepared.acpSessionId) { return yield* new ProviderAdapterRequestError({ provider: PROVIDER, method: "session/prompt", detail: "Grok session changed before the turn completed.", }); } const existingTurnRecord = ctx.turns.find((turn) => turn.id === prepared.turnId); ctx.turns = existingTurnRecord ? ctx.turns.map((turn) => turn.id === prepared.turnId ? { ...turn, items: [...turn.items, { prompt: prepared.promptParts, result }], } : turn, ) : [ ...ctx.turns, { id: prepared.turnId, items: [{ prompt: prepared.promptParts, result }] }, ]; ctx.session = { ...ctx.session, activeTurnId: prepared.turnId, updatedAt: yield* nowIso, ...(prepared.displayModel ? { model: prepared.displayModel } : {}), }; // Only the last remaining prompt settles the turn — a steer- // superseded prompt resolving (usually cancelled) while another // is in flight or pending must leave the merged turn running. if (ctx.promptsInFlight === 1) { yield* offerRuntimeEvent({ type: "turn.completed", ...(yield* makeEventStamp()), provider: PROVIDER, threadId: input.threadId, turnId: prepared.turnId, payload: { state: result.stopReason === "cancelled" ? "cancelled" : "completed", stopReason: result.stopReason ?? null, }, }); } return { threadId: input.threadId, turnId: prepared.turnId, resumeCursor: ctx.session.resumeCursor, }; }), ); }).pipe( Effect.ensuring( Effect.sync(() => { const liveCtx = sessions.get(input.threadId); if (liveCtx) { liveCtx.promptsInFlight = Math.max(0, liveCtx.promptsInFlight - 1); } }), ), ); }); const interruptTurn: GrokAdapterShape["interruptTurn"] = (threadId) => Effect.gen(function* () { const ctx = yield* requireSession(threadId); yield* settlePendingApprovalsAsCancelled(ctx.pendingApprovals); yield* settlePendingUserInputsAsCancelled(ctx.pendingUserInputs); yield* Effect.ignore( ctx.acp.cancel.pipe( Effect.mapError((error) => mapAcpToAdapterError(PROVIDER, threadId, "session/cancel", error), ), ), ); }); const respondToRequest: GrokAdapterShape["respondToRequest"] = ( threadId, requestId, decision, ) => Effect.gen(function* () { const ctx = yield* requireSession(threadId); const pending = ctx.pendingApprovals.get(requestId); if (!pending) { return yield* new ProviderAdapterRequestError({ provider: PROVIDER, method: "session/request_permission", detail: `Unknown pending approval request: ${requestId}`, }); } yield* Deferred.succeed(pending.decision, decision); }); const respondToUserInput: GrokAdapterShape["respondToUserInput"] = ( threadId, requestId, answers, ) => Effect.gen(function* () { const ctx = yield* requireSession(threadId); const pending = ctx.pendingUserInputs.get(requestId); if (!pending) { return yield* new ProviderAdapterRequestError({ provider: PROVIDER, method: "_x.ai/ask_user_question", detail: `Unknown pending user-input request: ${requestId}`, }); } yield* Deferred.succeed(pending.resolution, { _tag: "answered", answers }); }); const readThread: GrokAdapterShape["readThread"] = (threadId) => Effect.gen(function* () { const ctx = yield* requireSession(threadId); return { threadId, turns: ctx.turns }; }); const rollbackThread: GrokAdapterShape["rollbackThread"] = (threadId, numTurns) => Effect.gen(function* () { yield* requireSession(threadId); if (!Number.isInteger(numTurns) || numTurns < 1) { return yield* new ProviderAdapterValidationError({ provider: PROVIDER, operation: "rollbackThread", issue: "numTurns must be an integer >= 1.", }); } return yield* new ProviderAdapterRequestError({ provider: PROVIDER, method: "thread/rollback", detail: "Grok ACP sessions do not support provider-side rollback yet.", }); }); const stopSession: GrokAdapterShape["stopSession"] = (threadId) => withThreadLock( threadId, Effect.gen(function* () { const ctx = yield* requireSession(threadId); yield* stopSessionInternal(ctx); }), ); const listSessions: GrokAdapterShape["listSessions"] = () => Effect.sync(() => Array.from(sessions.values(), (c) => ({ ...c.session }))); const hasSession: GrokAdapterShape["hasSession"] = (threadId) => Effect.sync(() => { const c = sessions.get(threadId); return c !== undefined && !c.stopped; }); const stopAll: GrokAdapterShape["stopAll"] = () => Effect.forEach(Array.from(sessions.values()), stopSessionInternal, { discard: true }); yield* Effect.addFinalizer(() => Effect.ignore(stopAll()).pipe( Effect.tap(() => PubSub.shutdown(runtimeEventPubSub)), Effect.tap(() => managedNativeEventLogger?.close() ?? Effect.void), ), ); const streamEvents = Stream.fromPubSub(runtimeEventPubSub); return { provider: PROVIDER, capabilities: { sessionModelSwitch: "in-session" }, startSession, sendTurn, interruptTurn, readThread, rollbackThread, respondToRequest, respondToUserInput, stopSession, listSessions, hasSession, stopAll, streamEvents, } satisfies GrokAdapterShape; }); }