Skip to content
Source
Sign inSign up

adapter.ts

482 lines19.0 KB

1import { makeProviderFailure } from "@t3tools/provider-core/server/failure";
2import {
3 XAiPromptFailureText,
4 isXAiTaskCompletedWakeNotification,
5 xAiRateLimitedErrorCode,
6} from "./xaiAcpExtension.ts";
7import { discoverGrokSkills, leadingGrokSkillMention } from "./skills.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 SecureRandom from "effect/crypto/SecureRandom";
18import * as Effect from "effect/Effect";
19import * as FileSystem from "effect/FileSystem";
20import * as Path from "effect/Path";
21import * as Schema from "effect/Schema";
22import type * as Scope from "effect/Scope";
23import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
24import * as EffectAcpErrors from "effect-acp/errors";
25import { getModelSelectionStringOptionValue } from "@t3tools/shared/model";
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 GROK_DEFAULT_MODEL_SLUG,
34 grokApprovalOptions,
35 makeGrokAcpRuntime,
36 resolveGrokAcpBaseModelId,
37} from "./acpSupport.ts";
38import { grokReasoningOptionsFromModel } from "./status.ts";
39import {
40 extractGrokPlanMarkdownFromToolCallData,
41 extractXAiAcpBackgroundToolMutation,
42 extractXAiAcpSubagentEndNotice,
43 extractXAiAcpSubagentUpdate,
44 extractXAiAskUserQuestionIdentity,
45 extractXAiAskUserQuestions,
46 extractXAiBackgroundTaskCompletion,
47 extractXAiKilledBackgroundTasks,
48 extractXAiMonitorTaskId,
49 xAiBackgroundShellTaskId,
50 isXAiPersistentMonitor,
51 extractXAiExitPlanMarkdown,
52 registerXAiSubagentFinished,
53 makeXAiAskUserQuestionCancelledResponse,
54 makeXAiAskUserQuestionResponse,
55 makeXAiExitPlanModeCapturedResponse,
56 normalizeXAiAcpToolCallState,
57 registerXAiBackgroundTaskTracking,
58 XAiAskUserQuestionRequest,
59 XAiExitPlanModeRequest,
60} from "./xaiAcpExtension.ts";
61import { mergeProviderInstanceEnvironment } from "@t3tools/provider-core/server/instanceEnvironment";
62import { acpPermissionDisposition } from "@t3tools/provider-acp/server/clientPolicy";
63import {
64 findModelConfigOption,
65 sessionModelStateFromInitialize,
66} from "@t3tools/provider-acp/server/runtimeModel";
67import * as AcpSessionRuntime from "@t3tools/provider-acp/server/AcpSessionRuntime";
68import * as ProviderEventLoggers from "@t3tools/provider-core/server/ProviderEventLoggers";
69import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator";
70import * as ProviderContinuationRequests from "@t3tools/provider-core/server/ProviderContinuationRequests";
71import * as ProviderAdapter from "@t3tools/provider-core/server/ProviderAdapter";
72import {
73 ProviderAdapterDriverCreateError,
74 type ProviderAdapterDriver,
75 type ProviderAdapterDriverCreateInput,
76} from "@t3tools/provider-core/server/adapterDriver";
77import {
78 AcpProviderCapabilitiesV2,
79 makeAcpAdapterV2,
80 type AcpAdapterV2ExtensionContext,
81 type AcpAdapterV2Flavor,
82 type AcpAdapterV2RuntimeInput,
83} from "@t3tools/provider-acp/server/adapter";
84​
85const isXAiPromptFailureText = Schema.is(XAiPromptFailureText);
86​
87export const GROK_PROVIDER = ProviderDriverKind.make("grok");
88const GROK_DRIVER_KIND = GROK_PROVIDER;
89export const GROK_DEFAULT_INSTANCE_ID = defaultInstanceIdForDriver(GROK_DRIVER_KIND);
90const DEFAULT_GROK_SETTINGS = Schema.decodeSync(GrokSettings)({});
91const isAcpRequestError = Schema.is(EffectAcpErrors.AcpRequestError);
92​
93const GrokProviderCapabilitiesV2 = {
94 ...AcpProviderCapabilitiesV2,
95 sessions: {
96 ...AcpProviderCapabilitiesV2.sessions,
97 supportsModelSwitchInSession: true,
98 supportsRuntimeModeSwitchInSession: false,
99 },
100 threads: {
101 ...AcpProviderCapabilitiesV2.threads,
102 canReadThreadSnapshot: true,
103 canForkThread: false,
104 canForkFromTurn: false,
105 },
106 subagents: {
107 ...AcpProviderCapabilitiesV2.subagents,
108 supportsSubagents: true,
109 exposesSubagentThreadIds: true,
110 emitsSubagentLifecycle: true,
111 },
112 tools: {
113 ...AcpProviderCapabilitiesV2.tools,
114 supportsMcpTools: true,
115 },
116 checkpointing: {
117 ...AcpProviderCapabilitiesV2.checkpointing,
118 providerCanReadConversationSnapshot: true,
119 },
120} satisfies OrchestrationV2ProviderCapabilities;
121​
122export interface GrokAdapterV2Options {
123 readonly instanceId: Parameters<typeof makeAcpAdapterV2>[0]["instanceId"];
124 readonly settings: GrokSettings;
125 readonly environment: NodeJS.ProcessEnv;
126 readonly hostPlatform: NodeJS.Platform;
127 readonly selfInvocation: SelfInvocation;
128 readonly nativeLogging?: Parameters<typeof makeAcpAdapterV2>[0]["nativeLogging"];
129 readonly continuationRequests?: Parameters<typeof makeAcpAdapterV2>[0]["continuationRequests"];
130 readonly testHooks?: Parameters<typeof makeAcpAdapterV2>[0]["testHooks"];
131 readonly makeRuntime?: (
132 input: AcpAdapterV2RuntimeInput,
133 ) => Effect.Effect<
134 AcpSessionRuntime.AcpSessionRuntime["Service"],
135 EffectAcpErrors.AcpError,
136 ChildProcessSpawner.ChildProcessSpawner | SecureRandom.SecureRandom | Scope.Scope
137 >;
138 readonly assertComplete?: Effect.Effect<void, EffectAcpErrors.AcpError>;
140​
141const registerGrokAcpExtensions: NonNullable<AcpAdapterV2Flavor["registerExtensions"]> = ({
142 runtime,
143 requestUserInput,
144 applyBackgroundTaskMutation,
145 finishSubagent,
146 captureProposedPlan,
147 lastProposedPlanMarkdown,
148}) =>
149 registerXAiBackgroundTaskTracking(runtime, applyBackgroundTaskMutation).pipe(
150 Effect.andThen(registerXAiSubagentFinished(runtime, finishSubagent)),
151 Effect.andThen(registerGrokAskUserQuestionExtensions({ runtime, requestUserInput })),
152 Effect.andThen(
153 registerGrokExitPlanModeExtensions({
154 runtime,
155 captureProposedPlan,
156 lastProposedPlanMarkdown,
157 }),
158 ),
159 );
160​
161// Capture the reverse-requested plan, then abandon the native gate to finish the turn.
162const registerGrokExitPlanModeExtensions = ({
163 runtime,
164 captureProposedPlan,
165 lastProposedPlanMarkdown,
166}: Pick<
167 AcpAdapterV2ExtensionContext,
168 "runtime" | "captureProposedPlan" | "lastProposedPlanMarkdown"
169>) =>
170 Effect.forEach(
171 ["x.ai/exit_plan_mode", "_x.ai/exit_plan_mode"] as const,
172 (method) =>
173 runtime.handleExtRequest(method, XAiExitPlanModeRequest, (params) =>
174 Effect.gen(function* () {
175 const fallback = yield* lastProposedPlanMarkdown;
176 yield* captureProposedPlan({
177 planMarkdown: extractXAiExitPlanMarkdown(params, fallback),
178 });
179 return makeXAiExitPlanModeCapturedResponse();
180 }),
181 ),
182 { discard: true },
183 );
184​
185const registerGrokAskUserQuestionExtensions = ({
186 runtime,
187 requestUserInput,
188}: Pick<AcpAdapterV2ExtensionContext, "runtime" | "requestUserInput">) =>
189 Effect.forEach(
190 ["x.ai/ask_user_question", "_x.ai/ask_user_question"] as const,
191 (method) =>
192 runtime.handleExtRequest(method, XAiAskUserQuestionRequest, (params, requestContext) => {
193 const identity = extractXAiAskUserQuestionIdentity(params);
194 const questions = extractXAiAskUserQuestions(params).map((question) => ({
195 ...question,
196 options: [...question.options],
197 }));
198 return requestUserInput(
199 {
200 nativeItemId: `${identity.sessionId}:xai-question:${identity.toolCallId}`,
201 nativeRequestId: identity.toolCallId,
202 questions,
203 },
204 requestContext,
205 ).pipe(
206 Effect.flatMap(({ acknowledgeNativeResponse, answers }) =>
207 Effect.succeed(
208 answers === null
209 ? makeXAiAskUserQuestionCancelledResponse()
210 : makeXAiAskUserQuestionResponse(params, answers),
211 ).pipe(Effect.tap(() => acknowledgeNativeResponse)),
212 ),
213 );
214 }),
215 { discard: true },
216 );
217​
218/** Explicit policy overrides launch in ask mode so Grok cannot bypass T3 approval checks. */
219export function grokLaunchRuntimeMode(
220 runtimePolicy: ProviderAdapter.ProviderAdapterV2RuntimePolicy,
221): RuntimeMode {
222 return runtimePolicy.approvalPolicy === undefined && runtimePolicy.sandboxPolicy === undefined
223 ? runtimePolicy.runtimeMode
224 : "approval-required";
226​
227/** The capability id the composer stores Grok's reasoning effort under. */
228const GROK_REASONING_EFFORT_OPTION_ID = "reasoningEffort";
229​
230// Protocol 1 sends the effort as `session/set_model` metadata on every call: that response
231// carries no config options, so there is no live effort to compare against. Newer protocols
232// write the `thought_level` config option after the model switch. An effort the model does
233// not offer, or that Grok refuses, is skipped with a warning and Grok's default applies.
234const applyGrokSessionModelSelection: NonNullable<AcpAdapterV2Flavor["applyModelSelection"]> =
235 Effect.fn("GrokAdapter.applyModelSelection")(function* ({
236 runtime,
237 startResult,
238 modelSelection,
239 }) {
240 const requestedModelId = resolveGrokAcpBaseModelId(modelSelection.model);
241 const requestedEffort = getModelSelectionStringOptionValue(
242 modelSelection,
243 GROK_REASONING_EFFORT_OPTION_ID,
244 );
245 const skipEffort = (detail: string) =>
246 Effect.logWarning("Grok session skipped the requested reasoning effort", {
247 sessionId: startResult.sessionId,
248 reasoningEffort: requestedEffort,
249 detail,
250 });
251​
252 if (startResult.initializeResult.protocolVersion === 1) {
253 const setup = startResult.sessionSetupResult;
254 const currentModelId = currentGrokModelIdFromSessionSetup(setup);
255 const targetModelId =
256 requestedModelId === GROK_DEFAULT_MODEL_SLUG ? currentModelId : requestedModelId;
257 const targetModel = setup.models?.availableModels.find(
258 (model) => model.modelId.trim() === targetModelId,
259 );
260 const offeredEfforts =
261 targetModel === undefined
262 ? []
263 : grokReasoningOptionsFromModel(targetModel).options.map((option) => option.value);
264 const effort =
265 requestedEffort !== undefined &&
266 (offeredEfforts.length === 0 || offeredEfforts.includes(requestedEffort))
267 ? requestedEffort
268 : undefined;
269 if (requestedEffort !== undefined && effort === undefined) {
270 yield* skipEffort(`${targetModelId} does not offer it`);
271 }
272 const apply = (requestedReasoningEffort: string | undefined) =>
273 applyGrokAcpModelSelection({
274 runtime,
275 currentModelId,
276 requestedModelId,
277 requestedReasoningEffort,
278 mapError: (cause) => cause,
279 });
280 return yield* effort === undefined
281 ? apply(undefined)
282 : apply(effort).pipe(
283 Effect.catchTags({
284 AcpRequestError: (error) =>
285 skipEffort(error.message).pipe(Effect.andThen(apply(undefined))),
286 }),
287 );
288 }
289​
290 const appliedModelId = yield* applyGrokAcpModelSelection({
291 runtime: { setSessionModel: (model) => runtime.setModel(model).pipe(Effect.as({})) },
292 currentModelId: findModelConfigOption(yield* runtime.getConfigOptions)?.currentValue,
293 requestedModelId,
294 mapError: (cause) => cause,
295 });
296 if (requestedEffort === undefined) {
297 return appliedModelId;
298 }
299 // Read after the model switch: the effort choices belong to the new model.
300 const effortOption = (yield* runtime.getConfigOptions).find(
301 (option) => option.type === "select" && option.category === "thought_level",
302 );
303 if (effortOption === undefined) {
304 yield* skipEffort("the session has no reasoning effort option");
305 return appliedModelId;
306 }
307 // setConfigOption rejects a value the option does not offer and skips the current one.
308 yield* runtime
309 .setConfigOption(effortOption.id, requestedEffort)
310 .pipe(Effect.catchTags({ AcpRequestError: (error) => skipEffort(error.message) }));
311 return appliedModelId;
312 });
313​
314export function makeGrokAcpAdapterFlavor(
315 options: GrokAdapterV2Options,
316 homeDirectory: string,
317): AcpAdapterV2Flavor {
318 return {
319 driver: GROK_PROVIDER,
320 runtimeHarness: "Grok",
321 capabilities: GrokProviderCapabilitiesV2,
322 interruptPromptOnCancel: false,
323 // User Stop restarts the process group; steering cancels softly to preserve background work.
324 restartRuntimeAfterInterrupt: true,
325 terminateRuntimeProcessGroupOnInterrupt: true,
326 // Settled Grok sessions accept concurrent prompts; skip cancel to preserve background subagents.
327 preserveRuntimeOnSettledInterrupt: true,
328 // Grok accepts image blocks despite advertising promptCapabilities.image:false.
329 supportsImagePrompts: true,
330 supportsCompaction: true,
331 // Grok expands only a leading `/name`, while the `$` picker inserts `$name` from the
332 // `grok inspect` catalog. Ask that catalog again so other `$` text is left alone.
333 rewriteUserText: (text, turnInput) => {
334 const mention = leadingGrokSkillMention(text);
335 if (mention === undefined) return Effect.succeed(text);
336 return discoverGrokSkills(
337 options.settings,
338 options.environment,
339 turnInput.runtimePolicy.cwd ?? undefined,
340 ).pipe(
341 Effect.map((skills) =>
342 skills.some((skill) => skill.enabled && skill.name === mention.name)
343 ? `/${mention.name}${mention.rest}`
344 : text,
345 ),
346 Effect.orElseSucceed(() => text),
347 );
348 },
349 resolveModelId: (selection) => resolveGrokAcpBaseModelId(selection.model),
350 getModelContextWindow: (selection, session) => {
351 if (selection.instanceId !== options.instanceId) return undefined;
352 const modelId = resolveGrokAcpBaseModelId(selection.model);
353 const models =
354 session.sessionSetupResult.models ??
355 sessionModelStateFromInitialize(session.initializeResult);
356 const window = models?.availableModels.find((model) => model.modelId === modelId)?._meta
357 ?.totalContextTokens;
358 // contextWindows includes selectable larger windows; totalContextTokens is the default.
359 return typeof window === "number" && Number.isSafeInteger(window) && window > 0
360 ? window
361 : undefined;
362 },
363 applyModelSelection: applyGrokSessionModelSelection,
364 ownedModelOptionIds: [GROK_REASONING_EFFORT_OPTION_ID],
365 makeRuntime:
366 options.makeRuntime ??
367 (({ runtimePolicy, ...input }) =>
368 makeGrokAcpRuntime({
369 ...input,
370 interruptPromptOnCancel: input.interruptPromptOnCancel ?? false,
371 grokSettings: options.settings,
372 environment: options.environment,
373 runtimeMode: grokLaunchRuntimeMode(runtimePolicy),
374 })),
375 // Auto prompts represent classifier refusals and must reach the user.
376 permissionDisposition: (policy, request) =>
377 grokLaunchRuntimeMode(policy) === "auto" ? "ask" : acpPermissionDisposition(policy, request),
378 approvalOptions: grokApprovalOptions,
379 promptFailure: (cause) =>
380 makeProviderFailure({
381 cause,
382 ...(isAcpRequestError(cause)
383 ? {
384 message: isXAiPromptFailureText(cause.cause)
385 ? cause.cause.message
386 : cause.errorMessage,
387 code: String(cause.code),
388 class: cause.code === xAiRateLimitedErrorCode ? "usage_limit" : "provider_error",
389 }
390 : { class: "provider_error" }),
391 }),
392 registerExtensions: registerGrokAcpExtensions,
393 extractSubagentUpdate: extractXAiAcpSubagentUpdate,
394 extractSubagentEndNotice: extractXAiAcpSubagentEndNotice,
395 normalizeToolCall: normalizeXAiAcpToolCallState,
396 extractProposedPlanMarkdown: (toolCall) =>
397 extractGrokPlanMarkdownFromToolCallData(toolCall.data, {
398 platform: options.hostPlatform,
399 environment: options.environment,
400 homeDirectory,
401 }),
402 extractBackgroundTaskId: extractXAiMonitorTaskId,
403 isBackgroundCommandTool: (toolCall) => xAiBackgroundShellTaskId(toolCall) !== undefined,
404 extractBackgroundToolMutation: extractXAiAcpBackgroundToolMutation,
405 extractBackgroundTaskCompletion: (toolCall) => [
406 ...extractXAiBackgroundTaskCompletion(toolCall),
407 ...extractXAiKilledBackgroundTasks(toolCall),
408 ],
409 isPersistentBackgroundTool: isXAiPersistentMonitor,
410 isProviderWakeNotification: isXAiTaskCompletedWakeNotification,
411 deferFinalizeForBackgroundWork: true,
412 enablePostSettleContinuation: true,
413 ...(options.assertComplete === undefined ? {} : { assertComplete: options.assertComplete }),
414 };
416​
417export const makeGrokAdapterV2 = Effect.fn("makeGrokAdapterV2")(function* (
418 options: GrokAdapterV2Options,
419) {
420 return yield* makeAcpAdapterV2({
421 instanceId: options.instanceId,
422 flavor: makeGrokAcpAdapterFlavor(options, yield* HostProcess.HomeDirectory),
423 selfInvocation: options.selfInvocation,
424 ...(options.nativeLogging === undefined ? {} : { nativeLogging: options.nativeLogging }),
425 ...(options.continuationRequests === undefined
426 ? {}
427 : { continuationRequests: options.continuationRequests }),
428 ...(options.testHooks === undefined ? {} : { testHooks: options.testHooks }),
429 });
430});
431​
432export type GrokAdapterV2DriverEnv =
433 | ChildProcessSpawner.ChildProcessSpawner
434 | SecureRandom.SecureRandom
435 | FileSystem.FileSystem
436 | IdAllocator.IdAllocatorV2
437 | Path.Path
438 | McpProviderSessions.McpProviderSessions
439 | ProviderEventLoggers.ProviderEventLoggers
440 | ProviderHost.ProviderHost;
441​
442export const GrokAdapterV2Driver: ProviderAdapterDriver<GrokSettings, GrokAdapterV2DriverEnv> = {
443 driverKind: GROK_DRIVER_KIND,
444 configSchema: GrokSettings,
445 defaultConfig: (): GrokSettings => DEFAULT_GROK_SETTINGS,
446 create: Effect.fn("GrokAdapterV2Driver.create")(
447 function* (input: ProviderAdapterDriverCreateInput<GrokSettings>) {
448 const hostEnvironment = yield* HostProcess.Environment;
449 const hostPlatform = yield* HostProcess.Platform;
450 const selfInvocation = yield* resolveSelfInvocation();
451 const providerEventLoggers = yield* ProviderEventLoggers.ProviderEventLoggers;
452 const continuationRequests = yield* ProviderContinuationRequests.ProviderContinuationRequests;
453 const makeNativeLogger = yield* makeAcpNativeLoggerFactory();
454 return yield* makeGrokAdapterV2({
455 instanceId: input.instanceId,
456 settings: { ...input.config, enabled: input.enabled },
457 environment: yield* mergeProviderInstanceEnvironment(input.environment, hostEnvironment),
458 hostPlatform,
459 selfInvocation,
460 continuationRequests,
461 nativeLogging: (threadId) =>
462 makeNativeLogger({
463 nativeEventLogger: providerEventLoggers.native,
464 provider: GROK_PROVIDER,
465 threadId,
466 }),
467 });
468 },
469 (effect, input) =>
470 effect.pipe(
471 Effect.mapError(
472 (cause) =>
473 new ProviderAdapterDriverCreateError({
474 driver: GROK_DRIVER_KIND,
475 instanceId: input.instanceId,
476 detail: "Failed to create Grok ACP adapter.",
477 cause,
478 }),
479 ),
480 ),
481 ),
482};