1import * as ScriptProgress from "@t3tools/provider-core/server/ScriptProgress";
2import { makeProviderFailure } from "@t3tools/provider-core/server/failure";
3import {
4 XAiPromptFailureText,
5 isXAiTaskCompletedWakeNotification,
6 xAiRateLimitedErrorCode,
7} from "./xaiAcpExtension.ts";
8import * as HostProcess from "@t3tools/shared/HostProcess";
9import { resolveSelfInvocation, type SelfInvocation } from "@t3tools/shared/nodeRuntime";
10import {
11 defaultInstanceIdForDriver,
12 ProviderDriverKind,
13 type OrchestrationV2ProviderCapabilities,
14 type RuntimeMode,
15} from "@t3tools/contracts";
16import { GrokSettings } from "../settings.ts";
17import * as Crypto from "effect/Crypto";
18import * as Effect from "effect/Effect";
19import * as FileSystem from "effect/FileSystem";
20import * as Path from "effect/Path";
21import * as Layer from "effect/Layer";
22import * as Schema from "effect/Schema";
23import type * as Scope from "effect/Scope";
24import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
25import * as EffectAcpErrors from "effect-acp/errors";
26
27import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions";
28import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost";
29import { makeAcpNativeLoggerFactory } from "@t3tools/provider-acp/server/nativeLogging";
30import {
31 applyGrokAcpModelSelection,
32 currentGrokModelIdFromSessionSetup,
33 grokApprovalOptions,
34 makeGrokAcpRuntime,
35 resolveGrokAcpBaseModelId,
36} from "./acpSupport.ts";
37import {
38 extractGrokPlanMarkdownFromToolCallData,
39 extractXAiAcpBackgroundToolMutation,
40 extractXAiAcpSubagentEndNotice,
41 extractXAiAcpSubagentUpdate,
42 extractXAiAskUserQuestionIdentity,
43 extractXAiAskUserQuestions,
44 extractXAiBackgroundTaskCompletion,
45 extractXAiKilledBackgroundTasks,
46 extractXAiMonitorTaskId,
47 isXAiPersistentMonitor,
48 extractXAiExitPlanMarkdown,
49 registerXAiSubagentFinished,
50 makeXAiAskUserQuestionCancelledResponse,
51 makeXAiAskUserQuestionResponse,
52 makeXAiExitPlanModeCapturedResponse,
53 normalizeXAiAcpToolCallState,
54 registerXAiBackgroundTaskTracking,
55 XAiAskUserQuestionRequest,
56 XAiExitPlanModeRequest,
57} from "./xaiAcpExtension.ts";
58import { mergeProviderInstanceEnvironment } from "@t3tools/provider-core/server/instanceEnvironment";
59import { acpPermissionDisposition } from "@t3tools/provider-acp/server/clientPolicy";
60import * as AcpSessionRuntime from "@t3tools/provider-acp/server/AcpSessionRuntime";
61import * as ProviderEventLoggers from "@t3tools/provider-core/server/ProviderEventLoggers";
62import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator";
63import * as ProviderContinuationRequests from "@t3tools/provider-core/server/ProviderContinuationRequests";
64import * as ProviderAdapter from "@t3tools/provider-core/server/ProviderAdapter";
65import {
66 ProviderAdapterDriverCreateError,
67 type ProviderAdapterDriver,
68 type ProviderAdapterDriverCreateInput,
69} from "@t3tools/provider-core/server/adapterDriver";
70import {
71 AcpProviderCapabilitiesV2,
72 makeAcpAdapterV2,
73 type AcpAdapterV2ExtensionContext,
74 type AcpAdapterV2Flavor,
75 type AcpAdapterV2RuntimeInput,
76} from "@t3tools/provider-acp/server/adapter";
77
78export const GROK_PROVIDER = ProviderDriverKind.make("grok");
79const GROK_DRIVER_KIND = GROK_PROVIDER;
80export const GROK_DEFAULT_INSTANCE_ID = defaultInstanceIdForDriver(GROK_DRIVER_KIND);
81const DEFAULT_GROK_SETTINGS = Schema.decodeSync(GrokSettings)({});
82const isAcpRequestError = Schema.is(EffectAcpErrors.AcpRequestError);
83
84export const GrokProviderCapabilitiesV2 = {
85 ...AcpProviderCapabilitiesV2,
86 sessions: {
87 ...AcpProviderCapabilitiesV2.sessions,
88 supportsModelSwitchInSession: true,
89 supportsRuntimeModeSwitchInSession: false,
90 },
91 threads: {
92 ...AcpProviderCapabilitiesV2.threads,
93 canReadThreadSnapshot: true,
94 canForkThread: false,
95 canForkFromTurn: false,
96 },
97 subagents: {
98 ...AcpProviderCapabilitiesV2.subagents,
99 supportsSubagents: true,
100 exposesSubagentThreadIds: true,
101 emitsSubagentLifecycle: true,
102 },
103 tools: {
104 ...AcpProviderCapabilitiesV2.tools,
105 supportsMcpTools: true,
106 },
107 checkpointing: {
108 ...AcpProviderCapabilitiesV2.checkpointing,
109 providerCanReadConversationSnapshot: true,
110 },
111} satisfies OrchestrationV2ProviderCapabilities;
112
113export interface GrokAdapterV2Options {
114 readonly instanceId: Parameters<typeof makeAcpAdapterV2>[0]["instanceId"];
115 readonly settings: GrokSettings;
116 readonly environment: NodeJS.ProcessEnv;
117 readonly hostPlatform: NodeJS.Platform;
118 readonly selfInvocation: SelfInvocation;
119 readonly nativeLogging?: Parameters<typeof makeAcpAdapterV2>[0]["nativeLogging"];
120 readonly continuationRequests?: Parameters<typeof makeAcpAdapterV2>[0]["continuationRequests"];
121 readonly testHooks?: Parameters<typeof makeAcpAdapterV2>[0]["testHooks"];
122 readonly makeRuntime?: (
123 input: AcpAdapterV2RuntimeInput,
124 ) => Effect.Effect<
125 AcpSessionRuntime.AcpSessionRuntime["Service"],
126 EffectAcpErrors.AcpError,
127 ChildProcessSpawner.ChildProcessSpawner | Crypto.Crypto | Scope.Scope
128 >;
129 readonly assertComplete?: Effect.Effect<void, EffectAcpErrors.AcpError>;
130}
131
132const registerGrokAcpExtensions: NonNullable<AcpAdapterV2Flavor["registerExtensions"]> = ({
133 runtime,
134 requestUserInput,
135 applyBackgroundTaskMutation,
136 finishSubagent,
137 captureProposedPlan,
138 lastProposedPlanMarkdown,
139}) =>
140 registerXAiBackgroundTaskTracking(runtime, applyBackgroundTaskMutation).pipe(
141 Effect.andThen(registerXAiSubagentFinished(runtime, finishSubagent)),
142 Effect.andThen(registerGrokAskUserQuestionExtensions({ runtime, requestUserInput })),
143 Effect.andThen(
144 registerGrokExitPlanModeExtensions({
145 runtime,
146 captureProposedPlan,
147 lastProposedPlanMarkdown,
148 }),
149 ),
150 );
151
152/**
153 * Grok intercepts exit_plan_mode and reverse-requests client approval. Capture
154 * the plan into T3's proposed-plan card and abandon the native gate so the
155 * turn does not hang (#8358; mirrors the Claude ExitPlanMode pattern). Plan
156 * content preference: the request payload, then the plan.md contents sniffed
157 * from tool calls this turn, then the empty-state placeholder.
158 */
159const registerGrokExitPlanModeExtensions = ({
160 runtime,
161 captureProposedPlan,
162 lastProposedPlanMarkdown,
163}: Pick<
164 AcpAdapterV2ExtensionContext,
165 "runtime" | "captureProposedPlan" | "lastProposedPlanMarkdown"
166>) =>
167 Effect.forEach(
168 ["x.ai/exit_plan_mode", "_x.ai/exit_plan_mode"] as const,
169 (method) =>
170 runtime.handleExtRequest(method, XAiExitPlanModeRequest, (params) =>
171 Effect.gen(function* () {
172 const fallback = yield* lastProposedPlanMarkdown;
173 yield* captureProposedPlan({
174 planMarkdown: extractXAiExitPlanMarkdown(params, fallback),
175 });
176 return makeXAiExitPlanModeCapturedResponse();
177 }),
178 ),
179 { discard: true },
180 );
181
182const registerGrokAskUserQuestionExtensions = ({
183 runtime,
184 requestUserInput,
185}: Pick<AcpAdapterV2ExtensionContext, "runtime" | "requestUserInput">) =>
186 Effect.forEach(
187 ["x.ai/ask_user_question", "_x.ai/ask_user_question"] as const,
188 (method) =>
189 runtime.handleExtRequest(method, XAiAskUserQuestionRequest, (params, requestContext) => {
190 const identity = extractXAiAskUserQuestionIdentity(params);
191 const questions = extractXAiAskUserQuestions(params).map((question) => ({
192 id: question.id,
193 header: question.header,
194 question: question.question,
195 options: [...question.options],
196 }));
197 return requestUserInput(
198 {
199 nativeItemId: `${identity.sessionId}:xai-question:${identity.toolCallId}`,
200 nativeRequestId: identity.toolCallId,
201 questions,
202 },
203 requestContext,
204 ).pipe(
205 Effect.flatMap(({ acknowledgeNativeResponse, answers }) =>
206 Effect.succeed(
207 answers === null
208 ? makeXAiAskUserQuestionCancelledResponse()
209 : makeXAiAskUserQuestionResponse(params, answers),
210 ).pipe(Effect.tap(() => acknowledgeNativeResponse)),
211 ),
212 );
213 }),
214 { discard: true },
215 );
216
217/**
218 * Grok's permission mode is fixed at launch. Explicit approval or sandbox
219 * overrides launch it asking, so every mutating prompt reaches T3's policy
220 * check instead of being bypassed by always-approve or Grok's auto classifier.
221 */
222export function grokLaunchRuntimeMode(
223 runtimePolicy: ProviderAdapter.ProviderAdapterV2RuntimePolicy,
224): RuntimeMode {
225 return runtimePolicy.approvalPolicy === undefined && runtimePolicy.sandboxPolicy === undefined
226 ? runtimePolicy.runtimeMode
227 : "approval-required";
228}
229
230/** The flavor runs Grok's launcher through the adapter's spawner. */
231export function makeGrokAcpAdapterFlavor(
232 options: GrokAdapterV2Options,
233 homeDirectory: string,
234): AcpAdapterV2Flavor {
235 return {
236 driver: GROK_PROVIDER,
237 runtimeHarness: "Grok",
238 capabilities: GrokProviderCapabilitiesV2,
239 interruptPromptOnCancel: false,
240 // User Stop (requestRuntimeRestart) still hard-kills the process group and
241 // respawns so existing background tasks stop too. Older 0.2.x builds could
242 // detach a cancelled foreground command (E3 harness 2026-07-18); current
243 // source kills foreground work but intentionally preserves already-
244 // backgrounded tasks. Non-Stop interrupts (mid-prompt steering,
245 // restart_active) omit requestRuntimeRestart and stay soft: session/cancel
246 // carries cancelTrigger=ctrl_c, the session survives, and background work
247 // remains available to the replacement turn.
248 restartRuntimeAfterInterrupt: true,
249 terminateRuntimeProcessGroupOnInterrupt: true,
250 // Steering restarts on a settled turn additionally skip session/cancel so
251 // fire-and-forget subagents survive the steer (E1 harness confirmed the
252 // Grok CLI accepts a concurrent session/prompt in that state).
253 preserveRuntimeOnSettledInterrupt: true,
254 // Grok ACP initialize reports promptCapabilities.image:false but the agent
255 // still accepts image content blocks (verified with real screenshots).
256 supportsImagePrompts: true,
257 supportsCompaction: true,
258 resolveModelId: (selection) => resolveGrokAcpBaseModelId(selection.model),
259 applyModelSelection: ({ runtime, startResult, modelSelection }) =>
260 Effect.gen(function* () {
261 const legacy = startResult.initializeResult.protocolVersion === 1;
262 const options = legacy ? [] : yield* runtime.getConfigOptions;
263 const configuredModel = options.find((option) => option.category === "model")?.currentValue;
264 return yield* applyGrokAcpModelSelection({
265 runtime: legacy
266 ? runtime
267 : { setSessionModel: (model) => runtime.setModel(model).pipe(Effect.as({})) },
268 currentModelId: legacy
269 ? currentGrokModelIdFromSessionSetup(startResult.sessionSetupResult)
270 : typeof configuredModel === "string"
271 ? configuredModel
272 : undefined,
273 requestedModelId: resolveGrokAcpBaseModelId(modelSelection.model),
274 mapError: (cause) => cause,
275 });
276 }),
277 makeRuntime:
278 options.makeRuntime ??
279 (({ runtimePolicy, ...input }) =>
280 makeGrokAcpRuntime({
281 ...input,
282 interruptPromptOnCancel: input.interruptPromptOnCancel ?? false,
283 grokSettings: options.settings,
284 environment: ScriptProgress.withScriptProgressEnvironment(
285 options.environment,
286 input.processEnvironment ?? {},
287 ),
288 runtimeMode: grokLaunchRuntimeMode(runtimePolicy),
289 })),
290 // In its Auto mode Grok decides routine actions itself and only asks about
291 // what its classifier blocked, so every prompt it sends goes to the user.
292 permissionDisposition: (policy, request) =>
293 grokLaunchRuntimeMode(policy) === "auto" ? "ask" : acpPermissionDisposition(policy, request),
294 approvalOptions: grokApprovalOptions,
295 promptFailure: (cause) =>
296 makeProviderFailure({
297 cause,
298 ...(isAcpRequestError(cause)
299 ? {
300 // Grok's own failure text rides on the cause; makeProviderFailure
301 // redacts and bounds it before it reaches the user.
302 message:
303 cause.cause instanceof XAiPromptFailureText
304 ? cause.cause.message
305 : cause.errorMessage,
306 code: String(cause.code),
307 class: cause.code === xAiRateLimitedErrorCode ? "usage_limit" : "provider_error",
308 }
309 : { class: "provider_error" }),
310 }),
311 registerExtensions: registerGrokAcpExtensions,
312 extractSubagentUpdate: extractXAiAcpSubagentUpdate,
313 extractSubagentEndNotice: extractXAiAcpSubagentEndNotice,
314 normalizeToolCall: normalizeXAiAcpToolCallState,
315 // Show the plan while Grok is still writing it: plan.md writes under the
316 // Grok session dir surface as the proposed-plan card before exit (#8358).
317 extractProposedPlanMarkdown: (toolCall) =>
318 extractGrokPlanMarkdownFromToolCallData(toolCall.data, {
319 platform: options.hostPlatform,
320 environment: options.environment,
321 homeDirectory,
322 }),
323 extractBackgroundTaskId: extractXAiMonitorTaskId,
324 extractBackgroundToolMutation: extractXAiAcpBackgroundToolMutation,
325 extractBackgroundTaskCompletion: (toolCall) => [
326 ...extractXAiBackgroundTaskCompletion(toolCall),
327 ...extractXAiKilledBackgroundTasks(toolCall),
328 ],
329 isPersistentBackgroundTool: isXAiPersistentMonitor,
330 isProviderWakeNotification: isXAiTaskCompletedWakeNotification,
331 deferFinalizeForBackgroundWork: true,
332 enablePostSettleContinuation: true,
333 ...(options.assertComplete === undefined ? {} : { assertComplete: options.assertComplete }),
334 };
335}
336
337export const makeGrokAdapterV2 = Effect.fn("makeGrokAdapterV2")(function* (
338 options: GrokAdapterV2Options,
339) {
340 return yield* makeAcpAdapterV2({
341 instanceId: options.instanceId,
342 flavor: makeGrokAcpAdapterFlavor(options, yield* HostProcess.HomeDirectory),
343 selfInvocation: options.selfInvocation,
344 ...(options.nativeLogging === undefined ? {} : { nativeLogging: options.nativeLogging }),
345 ...(options.continuationRequests === undefined
346 ? {}
347 : { continuationRequests: options.continuationRequests }),
348 ...(options.testHooks === undefined ? {} : { testHooks: options.testHooks }),
349 });
350});
351
352export type GrokAdapterV2DriverEnv =
353 | ChildProcessSpawner.ChildProcessSpawner
354 | Crypto.Crypto
355 | FileSystem.FileSystem
356 | IdAllocator.IdAllocatorV2
357 | Path.Path
358 | ScriptProgress.ScriptProgress
359 | McpProviderSessions.McpProviderSessions
360 | ProviderEventLoggers.ProviderEventLoggers
361 | ProviderHost.ProviderHost;
362
363export const GrokAdapterV2Driver: ProviderAdapterDriver<GrokSettings, GrokAdapterV2DriverEnv> = {
364 driverKind: GROK_DRIVER_KIND,
365 configSchema: GrokSettings,
366 defaultConfig: (): GrokSettings => DEFAULT_GROK_SETTINGS,
367 create: Effect.fn("GrokAdapterV2Driver.create")(
368 function* (input: ProviderAdapterDriverCreateInput<GrokSettings>) {
369 const hostEnvironment = yield* HostProcess.Environment;
370 const hostPlatform = yield* HostProcess.Platform;
371 const selfInvocation = yield* resolveSelfInvocation();
372 const providerEventLoggers = yield* ProviderEventLoggers.ProviderEventLoggers;
373 const continuationRequests = yield* ProviderContinuationRequests.ProviderContinuationRequests;
374 const makeNativeLogger = yield* makeAcpNativeLoggerFactory();
375 return yield* makeGrokAdapterV2({
376 instanceId: input.instanceId,
377 settings: { ...input.config, enabled: input.enabled },
378 environment: yield* mergeProviderInstanceEnvironment(input.environment, hostEnvironment),
379 hostPlatform,
380 selfInvocation,
381 continuationRequests,
382 nativeLogging: (threadId) =>
383 makeNativeLogger({
384 nativeEventLogger: providerEventLoggers.native,
385 provider: GROK_PROVIDER,
386 threadId,
387 }),
388 });
389 },
390 (effect, input) =>
391 effect.pipe(
392 Effect.mapError(
393 (cause) =>
394 new ProviderAdapterDriverCreateError({
395 driver: GROK_DRIVER_KIND,
396 instanceId: input.instanceId,
397 detail: "Failed to create Grok ACP adapter.",
398 cause,
399 }),
400 ),
401 ),
402 ),
403};
404
405const layer: Layer.Layer<
406 ProviderAdapter.ProviderAdapterV2,
407 never,
408 | Path.Path
409 | ChildProcessSpawner.ChildProcessSpawner
410 | Crypto.Crypto
411 | FileSystem.FileSystem
412 | IdAllocator.IdAllocatorV2
413 | ScriptProgress.ScriptProgress
414 | McpProviderSessions.McpProviderSessions
415 | ProviderEventLoggers.ProviderEventLoggers
416 | ProviderHost.ProviderHost
417> = Layer.effect(
418 ProviderAdapter.ProviderAdapterV2,
419 Effect.gen(function* () {
420 const hostEnvironment = yield* HostProcess.Environment;
421 const hostPlatform = yield* HostProcess.Platform;
422 const selfInvocation = yield* resolveSelfInvocation();
423 const providerEventLoggers = yield* ProviderEventLoggers.ProviderEventLoggers;
424 const continuationRequests = yield* ProviderContinuationRequests.ProviderContinuationRequests;
425 const makeNativeLogger = yield* makeAcpNativeLoggerFactory();
426 return yield* makeGrokAdapterV2({
427 instanceId: GROK_DEFAULT_INSTANCE_ID,
428 settings: DEFAULT_GROK_SETTINGS,
429 environment: hostEnvironment,
430 hostPlatform,
431 selfInvocation,
432 continuationRequests,
433 nativeLogging: (threadId) =>
434 makeNativeLogger({
435 nativeEventLogger: providerEventLoggers.native,
436 provider: GROK_PROVIDER,
437 threadId,
438 }),
439 });
440 }),
441);