diff --git a/apps/mobile/package.json b/apps/mobile/package.json
index 2b73304bebd16a7eed4001419783cc891f565711..97a0cd33ac744a6672e7e5f47d939d8d29a14353 100644
--- a/apps/mobile/package.json
+++ b/apps/mobile/package.json
@@ -71,6 +71,7 @@
"@t3tools/mobile-terminal-native": "file:./modules/t3-terminal",
"@t3tools/provider-core": "workspace:*",
"@t3tools/provider-cursor": "workspace:*",
+ "@t3tools/provider-grok": "workspace:*",
"@t3tools/provider-muse": "workspace:*",
"@t3tools/provider-opencode": "workspace:*",
"@t3tools/provider-pi": "workspace:*",
diff --git a/apps/mobile/src/components/ProviderIcon.tsx b/apps/mobile/src/components/ProviderIcon.tsx
index f2eb4e3c7f70fd2da7b5f4078278536ba8ba33b5..34141a3121d3ddb9c51997f3de120b16a8a00bb4 100644
--- a/apps/mobile/src/components/ProviderIcon.tsx
+++ b/apps/mobile/src/components/ProviderIcon.tsx
@@ -93,22 +93,6 @@ export function ProviderIcon(props: ProviderIconProps) {
);
}
- if (props.provider === "grok") {
- const fill = isDarkMode ? "#F5F5F5" : "#0F0F0F";
- return (
-
- );
- }
-
const packageIcon = getProviderClient(props.provider)?.icon;
if (packageIcon) {
const themeFill = (fill: { readonly light: string; readonly dark: string }) =>
diff --git a/apps/mobile/src/lib/providerClients.ts b/apps/mobile/src/lib/providerClients.ts
index cb469a28706c86a7c93020238c84ea2d348dc80e..d434e6bde328a507056fa6d09f2f5a4254dfbd83 100644
--- a/apps/mobile/src/lib/providerClients.ts
+++ b/apps/mobile/src/lib/providerClients.ts
@@ -1,6 +1,7 @@
import { isProviderDriverKind } from "@t3tools/contracts";
import { makeProviderClientRegistry } from "@t3tools/provider-core/client";
import { cursorClient } from "@t3tools/provider-cursor/client";
+import { grokClient } from "@t3tools/provider-grok/client";
import { museClient } from "@t3tools/provider-muse/client";
import { openCodeClient } from "@t3tools/provider-opencode/client";
import { piClient } from "@t3tools/provider-pi/client";
@@ -8,6 +9,7 @@ import { piClient } from "@t3tools/provider-pi/client";
/** The provider client definitions this mobile build ships. */
const providerClients = makeProviderClientRegistry([
cursorClient,
+ grokClient,
museClient,
openCodeClient,
piClient,
diff --git a/apps/server/package.json b/apps/server/package.json
index 5e731012c583a1be73dc1d890d53368580f7d8bd..cb0f2dfcdc135d2f964f1a6fb45b832b9803db37 100644
--- a/apps/server/package.json
+++ b/apps/server/package.json
@@ -41,6 +41,7 @@
"@t3tools/provider-acp": "workspace:*",
"@t3tools/provider-core": "workspace:*",
"@t3tools/provider-cursor": "workspace:*",
+ "@t3tools/provider-grok": "workspace:*",
"@t3tools/provider-muse": "workspace:*",
"@t3tools/provider-opencode": "workspace:*",
"@t3tools/provider-pi": "workspace:*",
diff --git a/apps/server/scripts/record-grok-acp-replay-fixture.ts b/apps/server/scripts/record-grok-acp-replay-fixture.ts
index 30db5e0ef85a7feb95ba5768364bb08bb32d641a..d7c7da2c8e08e97ec73863bd0cdb4ba734f1589f 100644
--- a/apps/server/scripts/record-grok-acp-replay-fixture.ts
+++ b/apps/server/scripts/record-grok-acp-replay-fixture.ts
@@ -10,7 +10,8 @@
* node scripts/record-grok-acp-replay-fixture.ts --scenario simple
*/
import * as NodeServices from "@effect/platform-node/NodeServices";
-import { GrokSettings, type ProviderReplayEntry } from "@t3tools/contracts";
+import { type ProviderReplayEntry } from "@t3tools/contracts";
+import { GrokSettings } from "@t3tools/provider-grok/settings";
import { HostProcessEnvironment, HostProcessPlatform } from "@t3tools/shared/hostProcess";
import { resolveSelfInvocation } from "@t3tools/shared/nodeRuntime";
import * as Clock from "effect/Clock";
@@ -31,7 +32,7 @@ import {
GROK_PROVIDER,
grokLaunchRuntimeMode,
makeGrokAdapterV2,
-} from "../src/orchestration-v2/Adapters/GrokAdapterV2.ts";
+} from "@t3tools/provider-grok/testing";
import { ACP_PROTOCOL } from "@t3tools/provider-acp/server/adapter";
import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator";
import type { ProviderAdapterV2SessionRuntime } from "@t3tools/provider-core/server/ProviderAdapter";
@@ -43,7 +44,7 @@ import { materializeFixtureInput } from "../src/orchestration-v2/testkit/fixture
import { runOrchestratorV2Scenario } from "../src/orchestration-v2/testkit/OrchestratorScenario.ts";
import * as ProviderReplayHarness from "../src/orchestration-v2/testkit/ProviderReplayHarness.ts";
import { checkpointWorkspace } from "@t3tools/provider-testing/replayWorkspace";
-import { makeGrokAcpRuntime } from "../src/provider/acp/GrokAcpSupport.ts";
+import { makeGrokAcpRuntime } from "@t3tools/provider-grok/testing";
import { buildRuntimeInstructions } from "@t3tools/provider-core/server/runtimeInstructions";
const wallClock = Clock.Clock.defaultValue();
diff --git a/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts
index 5e225d570d7a4f79bcc09f92efbf4d9e2c84faee..62bff5add8ef7966f84d4515672a9726c33888b2 100644
--- a/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts
+++ b/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts
@@ -12,7 +12,6 @@ import * as NodeServices from "@effect/platform-node/NodeServices";
import { assert, describe, it } from "@effect/vitest";
import {
CheckpointId,
- GrokSettings,
EnvironmentId,
MessageId,
type ModelSelection,
@@ -27,6 +26,7 @@ import {
ThreadId,
type OrchestrationV2ProviderThread,
} from "@t3tools/contracts";
+import { GrokSettings } from "@t3tools/provider-grok/settings";
import { HostProcessIsExecutable, HostProcessPlatform } from "@t3tools/shared/hostProcess";
import { resolveSelfInvocation } from "@t3tools/shared/nodeRuntime";
import * as DateTime from "effect/DateTime";
@@ -64,7 +64,7 @@ import {
makeXAiPromptCompletionRuntime,
normalizeXAiAcpToolCallState,
registerXAiBackgroundTaskTracking,
-} from "../../provider/acp/XAiAcpExtension.ts";
+} from "@t3tools/provider-grok/testing";
import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator";
import {
ProviderAdapterProtocolError,
@@ -93,7 +93,7 @@ import {
type AcpAdapterV2SubagentUpdate,
} from "@t3tools/provider-acp/server/adapter";
-import { makeGrokAdapterV2 } from "./GrokAdapterV2.ts";
+import { makeGrokAdapterV2 } from "@t3tools/provider-grok/testing";
import {
acpRegistryPromptFailure,
registerMistralVibeAcpExtensions,
diff --git a/apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.test.ts
index 23ebb1b4f2a13258fca847fc126535595a4c760e..1dcb231ea103aff5dead71b06aa9e398e9915736 100644
--- a/apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.test.ts
+++ b/apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.test.ts
@@ -1,16 +1,16 @@
import * as NodeServices from "@effect/platform-node/NodeServices";
import {
- GrokSettings,
ProjectId,
ProviderInstanceId,
ProviderSessionId,
type RuntimeMode,
ThreadId,
} from "@t3tools/contracts";
+import { GrokSettings } from "@t3tools/provider-grok/settings";
import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
import { resolveSelfInvocation } from "@t3tools/shared/nodeRuntime";
import * as EffectAcpErrors from "effect-acp/errors";
-import { xAiRateLimitedErrorCode } from "../../provider/acp/XAiAcpExtension.ts";
+import { xAiRateLimitedErrorCode } from "@t3tools/provider-grok/testing";
import { assert, describe, it } from "@effect/vitest";
import * as Crypto from "effect/Crypto";
import * as DateTime from "effect/DateTime";
@@ -25,7 +25,7 @@ import type * as EffectAcpSchema from "effect-acp/compat";
import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost";
import { layerTestProviderHost } from "@t3tools/provider-testing/host";
import * as ProjectStore from "../ProjectStore.ts";
-import { buildInitialGrokProviderSnapshot } from "../../provider/GrokProvider.ts";
+import { buildInitialGrokProviderSnapshot } from "@t3tools/provider-grok/testing";
import type { ProviderInstance } from "@t3tools/provider-core/server/driver";
import * as ProviderInstanceRegistry from "../../provider/ProviderInstanceRegistry.ts";
import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator";
@@ -43,7 +43,7 @@ import {
makeGrokAdapterV2,
GrokProviderCapabilitiesV2,
type GrokAdapterV2Options,
-} from "./GrokAdapterV2.ts";
+} from "@t3tools/provider-grok/testing";
const LAUNCH_TEST_GROK_SETTINGS = Schema.decodeSync(GrokSettings)({
binaryPath: "grok-launch-test",
diff --git a/apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.testkit.ts b/apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.testkit.ts
index 58446793decd9bd221b7ae4adc27415024a0a352..172150432f537bf93afd9bd425ddb764d67b919b 100644
--- a/apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.testkit.ts
+++ b/apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.testkit.ts
@@ -1,5 +1,5 @@
import * as NodeServices from "@effect/platform-node/NodeServices";
-import { GrokSettings } from "@t3tools/contracts";
+import { GrokSettings } from "@t3tools/provider-grok/settings";
import * as Effect from "effect/Effect";
import * as Crypto from "effect/Crypto";
import * as FileSystem from "effect/FileSystem";
@@ -12,11 +12,8 @@ import { resolveSelfInvocation } from "@t3tools/shared/nodeRuntime";
import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost";
import { layerTestProviderHost } from "@t3tools/provider-testing/host";
-import {
- GROK_ACP_CANCEL_META,
- GROK_ACP_INITIALIZE_META,
-} from "../../provider/acp/GrokAcpSupport.ts";
-import { makeXAiPromptCompletionRuntime } from "../../provider/acp/XAiAcpExtension.ts";
+import { GROK_ACP_CANCEL_META, GROK_ACP_INITIALIZE_META } from "@t3tools/provider-grok/testing";
+import { makeXAiPromptCompletionRuntime } from "@t3tools/provider-grok/testing";
import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator";
import * as ProviderContinuationRequests from "@t3tools/provider-core/server/continuationRequests";
import * as ProviderAdapterRegistry from "../ProviderAdapterRegistry.ts";
@@ -29,7 +26,11 @@ import {
makeAcpReplayCompletenessAssertion,
makeAcpReplayRuntime,
} from "./AcpAdapterV2.testkit.ts";
-import { GROK_DEFAULT_INSTANCE_ID, GROK_PROVIDER, makeGrokAdapterV2 } from "./GrokAdapterV2.ts";
+import {
+ GROK_DEFAULT_INSTANCE_ID,
+ GROK_PROVIDER,
+ makeGrokAdapterV2,
+} from "@t3tools/provider-grok/testing";
const DEFAULT_GROK_SETTINGS = Schema.decodeUnknownSync(GrokSettings)({});
diff --git a/apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.ts
deleted file mode 100644
index e44c2da421a9e39b29d164922be16b033c293180..0000000000000000000000000000000000000000
--- a/apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.ts
+++ /dev/null
@@ -1,455 +0,0 @@
-import { makeProviderFailure } from "@t3tools/provider-core/server/failure";
-import {
- XAiPromptFailureText,
- isXAiTaskCompletedWakeNotification,
- xAiRateLimitedErrorCode,
-} from "../../provider/acp/XAiAcpExtension.ts";
-import { HostProcessEnvironment, HostProcessPlatform } from "@t3tools/shared/hostProcess";
-import { resolveSelfInvocation, type SelfInvocation } from "@t3tools/shared/nodeRuntime";
-import {
- defaultInstanceIdForDriver,
- GrokSettings,
- ProviderDriverKind,
- type OrchestrationV2ProviderCapabilities,
- type RuntimeMode,
-} from "@t3tools/contracts";
-import * as Crypto from "effect/Crypto";
-import * as Effect from "effect/Effect";
-import * as FileSystem from "effect/FileSystem";
-import * as Path from "effect/Path";
-import * as Layer from "effect/Layer";
-import * as Schema from "effect/Schema";
-import type * as Scope from "effect/Scope";
-import { ChildProcessSpawner } from "effect/process";
-import * as EffectAcpErrors from "effect-acp/errors";
-
-import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost";
-import { makeAcpNativeLoggerFactory } from "@t3tools/provider-acp/server/nativeLogging";
-import {
- applyGrokAcpModelSelection,
- currentGrokModelIdFromSessionSetup,
- grokApprovalOptions,
- makeGrokAcpRuntime,
- resolveGrokAcpBaseModelId,
-} from "../../provider/acp/GrokAcpSupport.ts";
-import {
- extractGrokPlanMarkdownFromToolCallData,
- extractXAiAcpBackgroundToolMutation,
- extractXAiAcpSubagentEndNotice,
- extractXAiAcpSubagentUpdate,
- extractXAiAskUserQuestionIdentity,
- extractXAiAskUserQuestions,
- extractXAiBackgroundTaskCompletion,
- extractXAiKilledBackgroundTasks,
- extractXAiMonitorTaskId,
- isXAiPersistentMonitor,
- extractXAiExitPlanMarkdown,
- registerXAiSubagentFinished,
- makeXAiAskUserQuestionCancelledResponse,
- makeXAiAskUserQuestionResponse,
- makeXAiExitPlanModeCapturedResponse,
- normalizeXAiAcpToolCallState,
- registerXAiBackgroundTaskTracking,
- XAiAskUserQuestionRequest,
- XAiExitPlanModeRequest,
-} from "../../provider/acp/XAiAcpExtension.ts";
-import { mergeProviderInstanceEnvironment } from "@t3tools/provider-core/server/instanceEnvironment";
-import { acpPermissionDisposition } from "@t3tools/provider-acp/server/clientPolicy";
-import * as AcpSessionRuntime from "@t3tools/provider-acp/server/AcpSessionRuntime";
-import * as ProviderEventLoggers from "../../provider/ProviderEventLoggers.ts";
-import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator";
-import * as ProviderContinuationRequests from "@t3tools/provider-core/server/continuationRequests";
-import * as ProviderAdapter from "@t3tools/provider-core/server/ProviderAdapter";
-import {
- ProviderAdapterDriverCreateError,
- type ProviderAdapterDriver,
- type ProviderAdapterDriverCreateInput,
-} from "@t3tools/provider-core/server/adapterDriver";
-import {
- AcpProviderCapabilitiesV2,
- makeAcpAdapterV2,
- type AcpAdapterV2ExtensionContext,
- type AcpAdapterV2Flavor,
- type AcpAdapterV2RuntimeInput,
-} from "@t3tools/provider-acp/server/adapter";
-
-export const GROK_PROVIDER = ProviderDriverKind.make("grok");
-const GROK_DRIVER_KIND = GROK_PROVIDER;
-export const GROK_DEFAULT_INSTANCE_ID = defaultInstanceIdForDriver(GROK_DRIVER_KIND);
-const DEFAULT_GROK_SETTINGS = Schema.decodeSync(GrokSettings)({});
-
-export const GrokProviderCapabilitiesV2 = {
- ...AcpProviderCapabilitiesV2,
- sessions: {
- ...AcpProviderCapabilitiesV2.sessions,
- supportsModelSwitchInSession: true,
- supportsRuntimeModeSwitchInSession: false,
- },
- threads: {
- ...AcpProviderCapabilitiesV2.threads,
- canReadThreadSnapshot: true,
- canForkThread: false,
- canForkFromTurn: false,
- },
- subagents: {
- ...AcpProviderCapabilitiesV2.subagents,
- supportsSubagents: true,
- exposesSubagentThreadIds: true,
- emitsSubagentLifecycle: true,
- },
- tools: {
- ...AcpProviderCapabilitiesV2.tools,
- supportsMcpTools: true,
- },
- checkpointing: {
- ...AcpProviderCapabilitiesV2.checkpointing,
- providerCanReadConversationSnapshot: true,
- },
-} satisfies OrchestrationV2ProviderCapabilities;
-
-export interface GrokAdapterV2Options {
- readonly instanceId: Parameters[0]["instanceId"];
- readonly settings: GrokSettings;
- readonly environment: NodeJS.ProcessEnv;
- readonly hostPlatform: NodeJS.Platform;
- readonly childProcessSpawner: ChildProcessSpawner.ChildProcessSpawner["Service"];
- readonly crypto: Crypto.Crypto;
- readonly selfInvocation: SelfInvocation;
- readonly fileSystem: FileSystem.FileSystem;
- readonly idAllocator: IdAllocator.IdAllocatorV2["Service"];
- readonly host: ProviderHost.ProviderHostShape;
- readonly nativeLogging?: Parameters[0]["nativeLogging"];
- readonly continuationRequests?: Parameters[0]["continuationRequests"];
- readonly testHooks?: Parameters[0]["testHooks"];
- readonly makeRuntime?: (
- input: AcpAdapterV2RuntimeInput,
- ) => Effect.Effect<
- AcpSessionRuntime.AcpSessionRuntime["Service"],
- EffectAcpErrors.AcpError,
- Crypto.Crypto | Scope.Scope
- >;
- readonly assertComplete?: Effect.Effect;
-}
-
-const registerGrokAcpExtensions: NonNullable = ({
- runtime,
- requestUserInput,
- applyBackgroundTaskMutation,
- finishSubagent,
- captureProposedPlan,
- lastProposedPlanMarkdown,
-}) =>
- registerXAiBackgroundTaskTracking(runtime, applyBackgroundTaskMutation).pipe(
- Effect.andThen(registerXAiSubagentFinished(runtime, finishSubagent)),
- Effect.andThen(registerGrokAskUserQuestionExtensions({ runtime, requestUserInput })),
- Effect.andThen(
- registerGrokExitPlanModeExtensions({
- runtime,
- captureProposedPlan,
- lastProposedPlanMarkdown,
- }),
- ),
- );
-
-/**
- * Grok intercepts exit_plan_mode and reverse-requests client approval. Capture
- * the plan into T3's proposed-plan card and abandon the native gate so the
- * turn does not hang (#8358; mirrors the Claude ExitPlanMode pattern). Plan
- * content preference: the request payload, then the plan.md contents sniffed
- * from tool calls this turn, then the empty-state placeholder.
- */
-const registerGrokExitPlanModeExtensions = ({
- runtime,
- captureProposedPlan,
- lastProposedPlanMarkdown,
-}: Pick<
- AcpAdapterV2ExtensionContext,
- "runtime" | "captureProposedPlan" | "lastProposedPlanMarkdown"
->) =>
- Effect.forEach(
- ["x.ai/exit_plan_mode", "_x.ai/exit_plan_mode"] as const,
- (method) =>
- runtime.handleExtRequest(method, XAiExitPlanModeRequest, (params) =>
- Effect.gen(function* () {
- const fallback = yield* lastProposedPlanMarkdown;
- yield* captureProposedPlan({
- planMarkdown: extractXAiExitPlanMarkdown(params, fallback),
- });
- return makeXAiExitPlanModeCapturedResponse();
- }),
- ),
- { discard: true },
- );
-
-const registerGrokAskUserQuestionExtensions = ({
- runtime,
- requestUserInput,
-}: Pick) =>
- Effect.forEach(
- ["x.ai/ask_user_question", "_x.ai/ask_user_question"] as const,
- (method) =>
- runtime.handleExtRequest(method, XAiAskUserQuestionRequest, (params, requestContext) => {
- const identity = extractXAiAskUserQuestionIdentity(params);
- const questions = extractXAiAskUserQuestions(params).map((question) => ({
- id: question.id,
- header: question.header,
- question: question.question,
- options: [...question.options],
- }));
- return requestUserInput(
- {
- nativeItemId: `${identity.sessionId}:xai-question:${identity.toolCallId}`,
- nativeRequestId: identity.toolCallId,
- questions,
- },
- requestContext,
- ).pipe(
- Effect.flatMap(({ acknowledgeNativeResponse, answers }) =>
- Effect.succeed(
- answers === null
- ? makeXAiAskUserQuestionCancelledResponse()
- : makeXAiAskUserQuestionResponse(params, answers),
- ).pipe(Effect.tap(() => acknowledgeNativeResponse)),
- ),
- );
- }),
- { discard: true },
- );
-
-/**
- * Grok's permission mode is fixed at launch. Explicit approval or sandbox
- * overrides launch it asking, so every mutating prompt reaches T3's policy
- * check instead of being bypassed by always-approve or Grok's auto classifier.
- */
-export function grokLaunchRuntimeMode(
- runtimePolicy: ProviderAdapter.ProviderAdapterV2RuntimePolicy,
-): RuntimeMode {
- return runtimePolicy.approvalPolicy === undefined && runtimePolicy.sandboxPolicy === undefined
- ? runtimePolicy.runtimeMode
- : "approval-required";
-}
-
-export function makeGrokAcpAdapterFlavor(options: GrokAdapterV2Options): AcpAdapterV2Flavor {
- return {
- driver: GROK_PROVIDER,
- runtimeHarness: "Grok",
- capabilities: GrokProviderCapabilitiesV2,
- interruptPromptOnCancel: false,
- // User Stop (requestRuntimeRestart) still hard-kills the process group and
- // respawns so existing background tasks stop too. Older 0.2.x builds could
- // detach a cancelled foreground command (E3 harness 2026-07-18); current
- // source kills foreground work but intentionally preserves already-
- // backgrounded tasks. Non-Stop interrupts (mid-prompt steering,
- // restart_active) omit requestRuntimeRestart and stay soft: session/cancel
- // carries cancelTrigger=ctrl_c, the session survives, and background work
- // remains available to the replacement turn.
- restartRuntimeAfterInterrupt: true,
- terminateRuntimeProcessGroupOnInterrupt: true,
- // Steering restarts on a settled turn additionally skip session/cancel so
- // fire-and-forget subagents survive the steer (E1 harness confirmed the
- // Grok CLI accepts a concurrent session/prompt in that state).
- preserveRuntimeOnSettledInterrupt: true,
- // Grok ACP initialize reports promptCapabilities.image:false but the agent
- // still accepts image content blocks (verified with real screenshots).
- supportsImagePrompts: true,
- supportsCompaction: true,
- resolveModelId: (selection) => resolveGrokAcpBaseModelId(selection.model),
- applyModelSelection: ({ runtime, startResult, modelSelection }) =>
- Effect.gen(function* () {
- const legacy = startResult.initializeResult.protocolVersion === 1;
- const options = legacy ? [] : yield* runtime.getConfigOptions;
- const configuredModel = options.find((option) => option.category === "model")?.currentValue;
- return yield* applyGrokAcpModelSelection({
- runtime: legacy
- ? runtime
- : { setSessionModel: (model) => runtime.setModel(model).pipe(Effect.as({})) },
- currentModelId: legacy
- ? currentGrokModelIdFromSessionSetup(startResult.sessionSetupResult)
- : typeof configuredModel === "string"
- ? configuredModel
- : undefined,
- requestedModelId: resolveGrokAcpBaseModelId(modelSelection.model),
- mapError: (cause) => cause,
- });
- }),
- makeRuntime:
- options.makeRuntime ??
- (({ runtimePolicy, ...input }) =>
- makeGrokAcpRuntime({
- ...input,
- interruptPromptOnCancel: input.interruptPromptOnCancel ?? false,
- grokSettings: options.settings,
- environment: options.environment,
- childProcessSpawner: options.childProcessSpawner,
- runtimeMode: grokLaunchRuntimeMode(runtimePolicy),
- })),
- // In its Auto mode Grok decides routine actions itself and only asks about
- // what its classifier blocked, so every prompt it sends goes to the user.
- permissionDisposition: (policy, request) =>
- grokLaunchRuntimeMode(policy) === "auto" ? "ask" : acpPermissionDisposition(policy, request),
- approvalOptions: grokApprovalOptions,
- promptFailure: (cause) =>
- makeProviderFailure({
- cause,
- ...(Schema.is(EffectAcpErrors.AcpRequestError)(cause)
- ? {
- // Grok's own failure text rides on the cause; makeProviderFailure
- // redacts and bounds it before it reaches the user.
- message:
- cause.cause instanceof XAiPromptFailureText
- ? cause.cause.message
- : cause.errorMessage,
- code: String(cause.code),
- class: cause.code === xAiRateLimitedErrorCode ? "usage_limit" : "provider_error",
- }
- : { class: "provider_error" }),
- }),
- registerExtensions: registerGrokAcpExtensions,
- extractSubagentUpdate: extractXAiAcpSubagentUpdate,
- extractSubagentEndNotice: extractXAiAcpSubagentEndNotice,
- normalizeToolCall: normalizeXAiAcpToolCallState,
- // Show the plan while Grok is still writing it: plan.md writes under the
- // Grok session dir surface as the proposed-plan card before exit (#8358).
- extractProposedPlanMarkdown: (toolCall) =>
- extractGrokPlanMarkdownFromToolCallData(toolCall.data, {
- platform: options.hostPlatform,
- environment: options.environment,
- }),
- extractBackgroundTaskId: extractXAiMonitorTaskId,
- extractBackgroundToolMutation: extractXAiAcpBackgroundToolMutation,
- extractBackgroundTaskCompletion: (toolCall) => [
- ...extractXAiBackgroundTaskCompletion(toolCall),
- ...extractXAiKilledBackgroundTasks(toolCall),
- ],
- isPersistentBackgroundTool: isXAiPersistentMonitor,
- isProviderWakeNotification: isXAiTaskCompletedWakeNotification,
- deferFinalizeForBackgroundWork: true,
- enablePostSettleContinuation: true,
- ...(options.assertComplete === undefined ? {} : { assertComplete: options.assertComplete }),
- };
-}
-
-export function makeGrokAdapterV2(options: GrokAdapterV2Options) {
- const flavor = makeGrokAcpAdapterFlavor(options);
- return makeAcpAdapterV2({
- instanceId: options.instanceId,
- flavor,
- crypto: options.crypto,
- fileSystem: options.fileSystem,
- idAllocator: options.idAllocator,
- host: options.host,
- selfInvocation: options.selfInvocation,
- ...(options.nativeLogging === undefined ? {} : { nativeLogging: options.nativeLogging }),
- ...(options.continuationRequests === undefined
- ? {}
- : { continuationRequests: options.continuationRequests }),
- ...(options.testHooks === undefined ? {} : { testHooks: options.testHooks }),
- });
-}
-
-export type GrokAdapterV2DriverEnv =
- | ChildProcessSpawner.ChildProcessSpawner
- | Crypto.Crypto
- | FileSystem.FileSystem
- | IdAllocator.IdAllocatorV2
- | Path.Path
- | ProviderEventLoggers.ProviderEventLoggers
- | ProviderHost.ProviderHost;
-
-export const GrokAdapterV2Driver: ProviderAdapterDriver = {
- driverKind: GROK_DRIVER_KIND,
- configSchema: GrokSettings,
- defaultConfig: (): GrokSettings => DEFAULT_GROK_SETTINGS,
- create: Effect.fn("GrokAdapterV2Driver.create")(
- function* (input: ProviderAdapterDriverCreateInput) {
- const hostEnvironment = yield* HostProcessEnvironment;
- const hostPlatform = yield* HostProcessPlatform;
- const selfInvocation = yield* resolveSelfInvocation();
- const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;
- const crypto = yield* Crypto.Crypto;
- const fileSystem = yield* FileSystem.FileSystem;
- const idAllocator = yield* IdAllocator.IdAllocatorV2;
- const providerEventLoggers = yield* ProviderEventLoggers.ProviderEventLoggers;
- const host = yield* ProviderHost.ProviderHost;
- const continuationRequests = yield* ProviderContinuationRequests.ProviderContinuationRequests;
- const makeNativeLogger = yield* makeAcpNativeLoggerFactory();
- return makeGrokAdapterV2({
- instanceId: input.instanceId,
- settings: { ...input.config, enabled: input.enabled },
- environment: mergeProviderInstanceEnvironment(input.environment, hostEnvironment),
- hostPlatform,
- childProcessSpawner,
- crypto,
- fileSystem,
- idAllocator,
- host,
- selfInvocation,
- continuationRequests,
- nativeLogging: (threadId) =>
- makeNativeLogger({
- nativeEventLogger: providerEventLoggers.native,
- provider: GROK_PROVIDER,
- threadId,
- }),
- });
- },
- (effect, input) =>
- effect.pipe(
- Effect.mapError(
- (cause) =>
- new ProviderAdapterDriverCreateError({
- driver: GROK_DRIVER_KIND,
- instanceId: input.instanceId,
- detail: "Failed to create Grok ACP adapter.",
- cause,
- }),
- ),
- ),
- ),
-};
-
-const layer: Layer.Layer<
- ProviderAdapter.ProviderAdapterV2,
- never,
- | Path.Path
- | ChildProcessSpawner.ChildProcessSpawner
- | Crypto.Crypto
- | FileSystem.FileSystem
- | IdAllocator.IdAllocatorV2
- | ProviderEventLoggers.ProviderEventLoggers
- | ProviderHost.ProviderHost
-> = Layer.effect(
- ProviderAdapter.ProviderAdapterV2,
- Effect.gen(function* () {
- const hostEnvironment = yield* HostProcessEnvironment;
- const hostPlatform = yield* HostProcessPlatform;
- const selfInvocation = yield* resolveSelfInvocation();
- const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;
- const crypto = yield* Crypto.Crypto;
- const fileSystem = yield* FileSystem.FileSystem;
- const idAllocator = yield* IdAllocator.IdAllocatorV2;
- const providerEventLoggers = yield* ProviderEventLoggers.ProviderEventLoggers;
- const host = yield* ProviderHost.ProviderHost;
- const continuationRequests = yield* ProviderContinuationRequests.ProviderContinuationRequests;
- const makeNativeLogger = yield* makeAcpNativeLoggerFactory();
- return makeGrokAdapterV2({
- instanceId: GROK_DEFAULT_INSTANCE_ID,
- settings: DEFAULT_GROK_SETTINGS,
- environment: hostEnvironment,
- hostPlatform,
- childProcessSpawner,
- crypto,
- fileSystem,
- idAllocator,
- host,
- selfInvocation,
- continuationRequests,
- nativeLogging: (threadId) =>
- makeNativeLogger({
- nativeEventLogger: providerEventLoggers.native,
- provider: GROK_PROVIDER,
- threadId,
- }),
- });
- }),
-);
diff --git a/apps/server/src/orchestration-v2/CommandPolicy.test.ts b/apps/server/src/orchestration-v2/CommandPolicy.test.ts
index 517229c222ea9773172d20ff9c662b6107dd5e4c..8df7da5cacdcf9670dc80931db875cd331ff7789 100644
--- a/apps/server/src/orchestration-v2/CommandPolicy.test.ts
+++ b/apps/server/src/orchestration-v2/CommandPolicy.test.ts
@@ -13,7 +13,7 @@ import * as Effect from "effect/Effect";
import { CodexProviderCapabilitiesV2 } from "./Adapters/CodexAdapterV2.ts";
import { CursorProviderCapabilitiesV2 } from "@t3tools/provider-cursor/testing";
-import { GrokProviderCapabilitiesV2 } from "./Adapters/GrokAdapterV2.ts";
+import { GrokProviderCapabilitiesV2 } from "@t3tools/provider-grok/testing";
import * as CommandPolicy from "./CommandPolicy.ts";
const commandId = CommandId.make("command-policy-test");
diff --git a/apps/server/src/orchestration-v2/builtInProviderAdapterDrivers.ts b/apps/server/src/orchestration-v2/builtInProviderAdapterDrivers.ts
index e48ceb60177c95fbfcf5162b0a59b2f1720291fd..f1ce18c12f88c83bcd6c7ea28bb51d2c45d4a010 100644
--- a/apps/server/src/orchestration-v2/builtInProviderAdapterDrivers.ts
+++ b/apps/server/src/orchestration-v2/builtInProviderAdapterDrivers.ts
@@ -13,7 +13,7 @@ import {
CursorAdapterV2Driver,
type CursorAdapterV2DriverEnv,
} from "@t3tools/provider-cursor/server";
-import { GrokAdapterV2Driver, type GrokAdapterV2DriverEnv } from "./Adapters/GrokAdapterV2.ts";
+import { GrokAdapterV2Driver, type GrokAdapterV2DriverEnv } from "@t3tools/provider-grok/server";
import {
OpenCodeAdapterV2Driver,
type OpenCodeAdapterV2DriverEnv,
diff --git a/apps/server/src/provider/Drivers/GrokDriver.test.ts b/apps/server/src/provider/Drivers/GrokDriver.test.ts
deleted file mode 100644
index e0da3203902c1eea50c1de3f69b9661f31ba883f..0000000000000000000000000000000000000000
--- a/apps/server/src/provider/Drivers/GrokDriver.test.ts
+++ /dev/null
@@ -1,111 +0,0 @@
-import * as NodeServices from "@effect/platform-node/NodeServices";
-import { expect, it } from "@effect/vitest";
-import { ProviderInstanceId } from "@t3tools/contracts";
-import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
-import * as Effect from "effect/Effect";
-import * as FileSystem from "effect/FileSystem";
-import * as Layer from "effect/Layer";
-import * as Path from "effect/Path";
-import { HttpClient } from "effect/http";
-import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
-
-import * as BackgroundPolicy from "../../background/BackgroundPolicy.ts";
-import * as ServerConfig from "../../config.ts";
-import * as ServerSettings from "../../serverSettings.ts";
-import * as ProviderEventLoggers from "../ProviderEventLoggers.ts";
-import { GrokDriver } from "./GrokDriver.ts";
-
-import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator";
-import * as ProviderHostLive from "../ProviderHostLive.ts";
-import * as ServerSecretStore from "../../auth/ServerSecretStore.ts";
-
-const layerDeps = ServerConfig.layerTest(process.cwd(), {
- prefix: "t3-grok-driver-update-",
-}).pipe(
- Layer.provideMerge(NodeServices.layer),
- Layer.provideMerge(IdAllocator.layer),
- Layer.provideMerge(ServerSettings.layerTest()),
- Layer.provideMerge(
- Layer.mock(BackgroundPolicy.BackgroundPolicy)({
- shouldRunScopeWork: () => Effect.succeed(false),
- }),
- ),
- Layer.provideMerge(
- Layer.succeed(
- ProviderEventLoggers.ProviderEventLoggers,
- ProviderEventLoggers.NoOpProviderEventLoggers,
- ),
- ),
- Layer.provideMerge(
- Layer.succeed(
- HttpClient.HttpClient,
- HttpClient.make(() => Effect.die("Disabled Grok must not make an HTTP request")),
- ),
- ),
-);
-const layerTest = ProviderHostLive.layer.pipe(
- Layer.provideMerge(ServerSecretStore.layer),
- Layer.provideMerge(layerDeps),
-);
-
-const noSpawner = ChildProcessSpawner.make(() =>
- Effect.die("Disabled Grok must not spawn a process"),
-);
-
-// The `#!/bin/sh` stub below cannot be resolved as an executable on Windows.
-const windowsHost = HostProcessPlatform.defaultValue() === "win32";
-
-it.layer(layerTest)("GrokDriver", (it) => {
- it.effect.skipIf(windowsHost)("updates through the configured executable's own updater", () =>
- Effect.gen(function* () {
- const fs = yield* FileSystem.FileSystem;
- const path = yield* Path.Path;
- const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-grok-driver-" });
- const grokHome = path.join(tempDir, "Grok Home");
- const binaryPath = path.join(grokHome, "bin", "grok");
- yield* fs.makeDirectory(path.dirname(binaryPath), { recursive: true });
- yield* fs.writeFileString(binaryPath, "#!/bin/sh\n");
- yield* fs.chmod(binaryPath, 0o755);
-
- const instance = yield* GrokDriver.create({
- instanceId: ProviderInstanceId.make("grok-update"),
- displayName: "Grok test",
- enabled: false,
- environment: [{ name: "GROK_HOME", value: grokHome, sensitive: false }],
- config: { ...GrokDriver.defaultConfig(), binaryPath },
- });
-
- const capabilities = yield* instance.snapshot.resolveMaintenance();
- expect(capabilities.packageName).toBe("@xai-official/grok");
- expect(capabilities.update).toMatchObject({
- command: `'${binaryPath}' update`,
- executable: binaryPath,
- args: ["update"],
- });
- // `grok update` installs under GROK_HOME, so it must target this instance's home.
- expect(capabilities.update?.env?.GROK_HOME).toBe(grokHome);
- }).pipe(
- Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, noSpawner),
- Effect.scoped,
- ),
- );
-
- it.effect("stays manual-only when the configured executable does not exist", () =>
- Effect.gen(function* () {
- const fs = yield* FileSystem.FileSystem;
- const path = yield* Path.Path;
- const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-grok-missing-" });
- const instance = yield* GrokDriver.create({
- instanceId: ProviderInstanceId.make("grok-missing"),
- displayName: "Grok test",
- enabled: false,
- environment: [],
- config: { ...GrokDriver.defaultConfig(), binaryPath: path.join(tempDir, "grok") },
- });
- expect((yield* instance.snapshot.resolveMaintenance()).update).toBeNull();
- }).pipe(
- Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, noSpawner),
- Effect.scoped,
- ),
- );
-});
diff --git a/apps/server/src/provider/Drivers/GrokDriver.ts b/apps/server/src/provider/Drivers/GrokDriver.ts
deleted file mode 100644
index f7226d5f1e9fb1b85016fa41c41cdec1d9fef0e2..0000000000000000000000000000000000000000
--- a/apps/server/src/provider/Drivers/GrokDriver.ts
+++ /dev/null
@@ -1,231 +0,0 @@
-import { GrokSettings, ProviderDriverKind } from "@t3tools/contracts";
-import * as Crypto from "effect/Crypto";
-import * as Effect from "effect/Effect";
-import * as FileSystem from "effect/FileSystem";
-import * as Path from "effect/Path";
-import * as Schema from "effect/Schema";
-import { HttpClient } from "effect/http";
-import { ChildProcessSpawner } from "effect/process";
-
-import { ProviderHost } from "@t3tools/provider-core/server/ProviderHost";
-import * as ServerConfig from "../../config.ts";
-import * as ProviderEventLoggers from "../ProviderEventLoggers.ts";
-import { makeGrokTextGeneration } from "../../textGeneration/GrokTextGeneration.ts";
-import {
- GrokAdapterV2Driver,
- type GrokAdapterV2DriverEnv,
-} from "../../orchestration-v2/Adapters/GrokAdapterV2.ts";
-import { ProviderDriverError } from "../Errors.ts";
-import {
- buildInitialGrokProviderSnapshot,
- checkGrokProviderStatus,
- enrichGrokSnapshot,
-} from "../GrokProvider.ts";
-import { readGrokAccount } from "../grokUsageLimits.ts";
-import { makeManagedServerProvider } from "@t3tools/provider-core/server/managedProvider";
-import {
- defaultProviderContinuationIdentity,
- type ProviderDriver,
- type ProviderInstance,
-} from "@t3tools/provider-core/server/driver";
-import { withInstanceIdentity } from "@t3tools/provider-core/server/instanceIdentity";
-import { mergeProviderInstanceEnvironment } from "@t3tools/provider-core/server/instanceEnvironment";
-import { discoverGrokSkills } from "./GrokSkills.ts";
-import {
- makeCachedProviderMaintenanceResolution,
- makeManualOnlyProviderMaintenanceCapabilities,
- makeProviderMaintenanceCapabilities,
- type ProviderMaintenanceCapabilitiesResolver,
- resolveProviderMaintenanceCapabilitiesEffect,
-} from "@t3tools/provider-core/server/maintenanceResolver";
-import {
- haveProviderSnapshotSettingsChanged,
- makeProviderSnapshotSettingsSource,
- type ProviderSnapshotSettings,
-} from "@t3tools/provider-core/server/snapshotSettings";
-const decodeGrokSettings = Schema.decodeSync(GrokSettings);
-
-const DRIVER_KIND = ProviderDriverKind.make("grok");
-// npm's `latest` tracks Grok's stable channel, the one `grok update` installs
-// by default, so the registry stays the source for "latest".
-const GROK_NPM_PACKAGE = "@xai-official/grok";
-// `grok update` finds the installer that owns the binary itself, so the
-// resolved executable is its own updater. It installs under `GROK_HOME`, so it
-// runs with the instance's environment. No executable means nothing to update,
-// not "whatever is on PATH".
-const UPDATE: ProviderMaintenanceCapabilitiesResolver = {
- resolve: (context) =>
- Effect.succeed(
- context
- ? makeProviderMaintenanceCapabilities({
- provider: DRIVER_KIND,
- packageName: GROK_NPM_PACKAGE,
- updateExecutable: context.resolvedCommandPath,
- updateArgs: ["update"],
- updateLockKey: "grok",
- platform: context.platform,
- env: context.env,
- })
- : makeManualOnlyProviderMaintenanceCapabilities({
- provider: DRIVER_KIND,
- packageName: GROK_NPM_PACKAGE,
- }),
- ),
-};
-
-export type GrokDriverEnv =
- | GrokAdapterV2DriverEnv
- | ProviderHost
- | ChildProcessSpawner.ChildProcessSpawner
- | Crypto.Crypto
- | FileSystem.FileSystem
- | HttpClient.HttpClient
- | Path.Path
- | ProviderEventLoggers.ProviderEventLoggers
- | ServerConfig.ServerConfig;
-
-export const GrokDriver: ProviderDriver = {
- driverKind: DRIVER_KIND,
- metadata: {
- displayName: "Grok",
- supportsMultipleInstances: true,
- },
- configSchema: GrokSettings,
- defaultConfig: (): GrokSettings => decodeGrokSettings({}),
- create: ({ instanceId, displayName, accentColor, environment, enabled, config }) =>
- Effect.gen(function* () {
- const crypto = yield* Crypto.Crypto;
- const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
- const httpClient = yield* HttpClient.HttpClient;
- const fileSystem = yield* FileSystem.FileSystem;
- const path = yield* Path.Path;
- const host = yield* ProviderHost;
- const { cwd } = yield* ServerConfig.ServerConfig;
- const processEnv = mergeProviderInstanceEnvironment(environment);
- const continuationIdentity = defaultProviderContinuationIdentity({
- driverKind: DRIVER_KIND,
- instanceId,
- });
- const stampIdentity = withInstanceIdentity({
- instanceId,
- driverKind: DRIVER_KIND,
- displayName,
- accentColor,
- continuationGroupKey: continuationIdentity.continuationKey,
- });
- const effectiveConfig = { ...config, enabled } satisfies GrokSettings;
- const resolveMaintenance = yield* makeCachedProviderMaintenanceResolution(
- resolveProviderMaintenanceCapabilitiesEffect(UPDATE, {
- binaryPath: effectiveConfig.binaryPath,
- env: processEnv,
- }).pipe(
- Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
- Effect.provideService(FileSystem.FileSystem, fileSystem),
- Effect.provideService(Path.Path, path),
- ),
- );
- const orchestrationAdapter = yield* GrokAdapterV2Driver.create({
- instanceId,
- displayName,
- accentColor,
- environment,
- enabled,
- config,
- }).pipe(
- Effect.mapError(
- (cause) =>
- new ProviderDriverError({
- driver: DRIVER_KIND,
- instanceId,
- detail: "Failed to build Grok orchestration adapter.",
- cause,
- }),
- ),
- );
- const textGeneration = yield* makeGrokTextGeneration(effectiveConfig, processEnv);
-
- const checkProvider = checkGrokProviderStatus(effectiveConfig, processEnv, cwd).pipe(
- Effect.flatMap((snapshot) =>
- effectiveConfig.enabled && snapshot.installed && snapshot.auth.status === "authenticated"
- ? readGrokAccount(processEnv).pipe(
- Effect.map(({ email, usageLimits }) => ({
- ...snapshot,
- auth: email ? { ...snapshot.auth, email } : snapshot.auth,
- usageLimits,
- })),
- )
- : Effect.succeed(snapshot),
- ),
- Effect.map(stampIdentity),
- Effect.provideService(HttpClient.HttpClient, httpClient),
- Effect.provideService(FileSystem.FileSystem, fileSystem),
- Effect.provideService(Path.Path, path),
- Effect.provideService(Crypto.Crypto, crypto),
- Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
- );
-
- const snapshotSettings = makeProviderSnapshotSettingsSource(effectiveConfig, host.settings);
- const snapshot = yield* makeManagedServerProvider>({
- resolveMaintenance,
- getSettings: snapshotSettings.getSettings,
- streamSettings: snapshotSettings.streamSettings,
- haveSettingsChanged: haveProviderSnapshotSettingsChanged,
- initialSnapshot: (settings) =>
- buildInitialGrokProviderSnapshot(settings.provider).pipe(Effect.map(stampIdentity)),
- checkProvider,
- enrichSnapshot: ({ settings, snapshot: currentSnapshot, publishSnapshot }) =>
- resolveMaintenance().pipe(
- Effect.flatMap((maintenanceCapabilities) =>
- enrichGrokSnapshot({
- snapshot: currentSnapshot,
- maintenanceCapabilities,
- enableProviderUpdateChecks: settings.enableProviderUpdateChecks,
- publishSnapshot,
- httpClient,
- }),
- ),
- ),
- }).pipe(
- Effect.mapError(
- (cause) =>
- new ProviderDriverError({
- driver: DRIVER_KIND,
- instanceId,
- detail: `Failed to build Grok snapshot: ${cause.message ?? String(cause)}`,
- cause,
- }),
- ),
- );
- const snapshotForCwd = (workspaceCwd: string) =>
- !effectiveConfig.enabled
- ? snapshot.getSnapshot
- : Effect.all([
- snapshot.getSnapshot,
- discoverGrokSkills(effectiveConfig, processEnv, workspaceCwd).pipe(
- Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
- Effect.mapError(
- (cause) =>
- new ProviderDriverError({
- driver: DRIVER_KIND,
- instanceId,
- detail: `Failed to discover Grok skills for '${workspaceCwd}'`,
- cause,
- }),
- ),
- ),
- ]).pipe(Effect.map(([machineSnapshot, skills]) => ({ ...machineSnapshot, skills })));
-
- return {
- instanceId,
- driverKind: DRIVER_KIND,
- continuationIdentity,
- displayName,
- accentColor,
- enabled,
- snapshot,
- snapshotForCwd,
- orchestrationAdapter,
- textGeneration,
- } satisfies ProviderInstance;
- }),
-};
diff --git a/apps/server/src/provider/Drivers/GrokSkills.test.ts b/apps/server/src/provider/Drivers/GrokSkills.test.ts
deleted file mode 100644
index 6f194644617f2e07671e06f8fec44fa5cf6c06f5..0000000000000000000000000000000000000000
--- a/apps/server/src/provider/Drivers/GrokSkills.test.ts
+++ /dev/null
@@ -1,174 +0,0 @@
-import { describe, expect, it } from "@effect/vitest";
-import * as Effect from "effect/Effect";
-import * as Sink from "effect/Sink";
-import * as Stream from "effect/Stream";
-import { ChildProcessSpawner } from "effect/process";
-
-import { discoverGrokSkills } from "./GrokSkills.ts";
-
-const inspectPayload = (skills: ReadonlyArray) => JSON.stringify({ skills });
-
-const makeInspectSpawner = (stdout: string, exitCode = 0, spawnCwds?: Array) =>
- ChildProcessSpawner.make((command) => {
- spawnCwds?.push(command._tag === "StandardCommand" ? command.options.cwd : undefined);
- return Effect.succeed(
- ChildProcessSpawner.makeHandle({
- pid: ChildProcessSpawner.ProcessId(1),
- exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(exitCode)),
- isRunning: Effect.succeed(false),
- kill: () => Effect.void,
- unref: Effect.succeed(Effect.void),
- stdin: Sink.drain,
- stdout: Stream.encodeText(Stream.make(stdout)),
- stderr: Stream.empty,
- all: Stream.empty,
- getInputFd: () => Sink.drain,
- getOutputFd: () => Stream.empty,
- }),
- );
- });
-
-describe("discoverGrokSkills", () => {
- it.effect("maps inspect entries onto provider skills, sorted by name", () =>
- Effect.gen(function* () {
- const skills = yield* discoverGrokSkills({ binaryPath: "grok" }, {});
-
- expect(skills).toEqual([
- {
- name: "deploy",
- description: "Deploy the app.",
- path: "/home/dev/.grok/installed-plugins/pkg/plug/skills/deploy/SKILL.md",
- scope: "plugin",
- enabled: true,
- },
- {
- name: "writing-docs",
- description: "Write user docs.",
- path: "/home/dev/.grok/skills/writing-docs/SKILL.md",
- scope: "user",
- enabled: true,
- },
- ]);
- }).pipe(
- Effect.provideService(
- ChildProcessSpawner.ChildProcessSpawner,
- makeInspectSpawner(
- inspectPayload([
- {
- name: "writing-docs",
- description: "Write user docs.",
- source: { type: "user", path: "/home/dev/.grok/skills/writing-docs/SKILL.md" },
- userInvocable: true,
- },
- {
- name: "deploy",
- description: "Deploy the app.",
- source: {
- type: "plugin",
- path: "/home/dev/.grok/installed-plugins/pkg/plug/skills/deploy/SKILL.md",
- },
- userInvocable: true,
- },
- ]),
- ),
- ),
- ),
- );
-
- it.effect("disables skills the CLI marks as not user-invocable", () =>
- Effect.gen(function* () {
- const skills = yield* discoverGrokSkills({ binaryPath: "grok" }, {});
-
- expect(skills).toEqual([
- {
- name: "internal-helper",
- path: "/opt/grok/bundled/skills/internal-helper/SKILL.md",
- scope: "bundled",
- enabled: false,
- },
- ]);
- }).pipe(
- Effect.provideService(
- ChildProcessSpawner.ChildProcessSpawner,
- makeInspectSpawner(
- inspectPayload([
- {
- name: "internal-helper",
- source: {
- type: "bundled",
- path: "/opt/grok/bundled/skills/internal-helper/SKILL.md",
- },
- userInvocable: false,
- },
- ]),
- ),
- ),
- ),
- );
-
- it.effect("skips entries without a name or a filesystem path", () =>
- Effect.gen(function* () {
- const skills = yield* discoverGrokSkills({ binaryPath: "grok" }, {});
- expect(skills.map((skill) => skill.name)).toEqual(["kept"]);
- }).pipe(
- Effect.provideService(
- ChildProcessSpawner.ChildProcessSpawner,
- makeInspectSpawner(
- inspectPayload([
- { name: " ", source: { type: "user", path: "/tmp/skills/a/SKILL.md" } },
- { name: "no-path", source: { type: "user" } },
- { name: "no-source" },
- "not-an-object",
- { name: "kept", source: { type: "project", path: "/repo/.grok/skills/kept/SKILL.md" } },
- ]),
- ),
- ),
- ),
- );
-
- it.effect("rejects malformed or unexpected output as a decode failure", () =>
- Effect.gen(function* () {
- for (const stdout of ["not json", "null", '{"skills":"nope"}', "{}"]) {
- const error = yield* discoverGrokSkills({ binaryPath: "grok" }, {}).pipe(
- Effect.flip,
- Effect.provideService(
- ChildProcessSpawner.ChildProcessSpawner,
- makeInspectSpawner(stdout),
- ),
- );
- expect(error).toMatchObject({ _tag: "GrokSkillsProbeError", stage: "decode" });
- }
- }),
- );
-
- it.effect("spawns in the configured cwd and rejects a failed probe", () => {
- const spawnCwds: Array = [];
- const stdout = inspectPayload([
- {
- name: "kept",
- source: { type: "project", path: "/workspaces/demo/.grok/skills/kept/SKILL.md" },
- },
- ]);
-
- return Effect.gen(function* () {
- const skills = yield* discoverGrokSkills({ binaryPath: "grok" }, {}, "/workspaces/demo").pipe(
- Effect.provideService(
- ChildProcessSpawner.ChildProcessSpawner,
- makeInspectSpawner(stdout, 0, spawnCwds),
- ),
- );
-
- expect(spawnCwds).toEqual(["/workspaces/demo"]);
- expect(skills.map((skill) => skill.name)).toEqual(["kept"]);
-
- const failed = yield* discoverGrokSkills({ binaryPath: "grok" }).pipe(
- Effect.result,
- Effect.provideService(
- ChildProcessSpawner.ChildProcessSpawner,
- makeInspectSpawner(stdout, 1),
- ),
- );
- expect(failed._tag).toBe("Failure");
- });
- });
-});
diff --git a/apps/server/src/provider/Drivers/GrokSkills.ts b/apps/server/src/provider/Drivers/GrokSkills.ts
deleted file mode 100644
index f552b0f4b2bcc676a163ba6ea13bca2d89005d7e..0000000000000000000000000000000000000000
--- a/apps/server/src/provider/Drivers/GrokSkills.ts
+++ /dev/null
@@ -1,151 +0,0 @@
-/**
- * GrokSkills — skill discovery for the `$` picker via `grok inspect --json`.
- *
- * Unlike Claude Code, the Grok CLI reports its full skill catalog itself:
- * `grok inspect --json` returns `skills[]` with `name`, `description`,
- * `source.type` (`user` / `project` / `bundled` / `plugin`), `source.path`
- * (the absolute `SKILL.md` path), and `userInvocable`. Asking the CLI beats
- * scanning the filesystem because the catalog honors Grok's own skill config
- * (ignore lists, disabled skills) and includes plugin skills, which live
- * three levels deep under `~/.grok/installed-plugins/` where a flat scan
- * cannot see them. This mirrors how the Codex app-server reports skills over
- * `skills/list`. Probe failures stay typed so workspace snapshots do not
- * cache an empty catalog; machine-level discovery recovers them to an empty
- * list without degrading the provider.
- *
- * @module provider/Drivers/GrokSkills
- */
-import type { GrokSettings, ServerProviderSkill } from "@t3tools/contracts";
-import * as Effect from "effect/Effect";
-import * as Option from "effect/Option";
-import * as Schema from "effect/Schema";
-import { ChildProcess } from "effect/process";
-import { resolveSpawnCommand } from "@t3tools/shared/shell";
-
-import { spawnAndCollect } from "@t3tools/provider-core/server/snapshotProbe";
-
-const GROK_SKILLS_PROBE_TIMEOUT_MS = 4_000;
-
-class GrokSkillsProbeError extends Schema.TaggedError()(
- "GrokSkillsProbeError",
- {
- stage: Schema.Literals(["spawn", "timeout", "exit", "decode"]),
- cwd: Schema.optional(Schema.String),
- exitCode: Schema.optional(Schema.Number),
- cause: Schema.optional(Schema.Defect()),
- },
-) {
- override get message(): string {
- const location = this.cwd === undefined ? "" : ` for '${this.cwd}'`;
- const exitCode = this.exitCode === undefined ? "" : ` with exit code ${this.exitCode}`;
- return `\`grok inspect --json\` failed during ${this.stage}${location}${exitCode}.`;
- }
-}
-
-/**
- * Map `grok inspect --json` output onto provider skills. Entries without a
- * name or a filesystem path are skipped; `userInvocable: false` skills are
- * kept but disabled so pickers that filter on `enabled` hide them.
- */
-function decodeGrokInspectSkills(stdout: string): ReadonlyArray | undefined {
- let parsed: unknown;
- try {
- parsed = JSON.parse(stdout);
- } catch {
- return undefined;
- }
- if (typeof parsed !== "object" || parsed === null) {
- return undefined;
- }
- const entries = (parsed as Record).skills;
- if (!Array.isArray(entries)) {
- return undefined;
- }
-
- const skillsByName = new Map();
- for (const entry of entries) {
- if (typeof entry !== "object" || entry === null) {
- continue;
- }
- const record = entry as Record;
- const name = typeof record.name === "string" ? record.name.trim() : "";
- const source =
- typeof record.source === "object" && record.source !== null
- ? (record.source as Record)
- : undefined;
- const path = typeof source?.path === "string" ? source.path.trim() : "";
- if (!name || !path) {
- continue;
- }
- const scope = typeof source?.type === "string" ? source.type.trim() : "";
- const description = typeof record.description === "string" ? record.description.trim() : "";
- skillsByName.set(name, {
- name,
- path,
- enabled: record.userInvocable !== false,
- ...(scope ? { scope } : {}),
- ...(description ? { description } : {}),
- });
- }
-
- return [...skillsByName.values()].sort((left, right) => left.name.localeCompare(right.name));
-}
-
-/**
- * Run `grok inspect --json` and map the reported catalog onto provider
- * skills. Callers that need best-effort discovery can recover this effect to
- * an empty list; workspace callers leave failures typed so they are not cached.
- */
-export const discoverGrokSkills = Effect.fn("discoverGrokSkills")(function* (
- grokSettings: Pick,
- environment: NodeJS.ProcessEnv = process.env,
- cwd?: string,
-) {
- const command = grokSettings.binaryPath || "grok";
- const inspectResult = yield* Effect.gen(function* () {
- const spawnCommand = yield* resolveSpawnCommand(command, ["inspect", "--json"], {
- env: environment,
- });
- return yield* spawnAndCollect(
- command,
- ChildProcess.make(spawnCommand.command, spawnCommand.args, {
- ...(cwd ? { cwd } : {}),
- env: environment,
- shell: spawnCommand.shell,
- }),
- );
- }).pipe(
- Effect.mapError(
- (cause) =>
- new GrokSkillsProbeError({
- stage: "spawn",
- ...(cwd ? { cwd } : {}),
- cause,
- }),
- ),
- Effect.timeoutOption(GROK_SKILLS_PROBE_TIMEOUT_MS),
- );
-
- if (Option.isNone(inspectResult)) {
- return yield* new GrokSkillsProbeError({
- stage: "timeout",
- ...(cwd ? { cwd } : {}),
- });
- }
- const output = inspectResult.value;
- if (output.code !== 0) {
- return yield* new GrokSkillsProbeError({
- stage: "exit",
- ...(cwd ? { cwd } : {}),
- exitCode: output.code,
- });
- }
- const skills = decodeGrokInspectSkills(output.stdout);
- if (!skills) {
- return yield* new GrokSkillsProbeError({
- stage: "decode",
- ...(cwd ? { cwd } : {}),
- });
- }
- return skills;
-});
diff --git a/apps/server/src/provider/GrokProvider.test.ts b/apps/server/src/provider/GrokProvider.test.ts
index 000176e9bb2318e68142cd72a5f5ec954ab9657c..73f56a3558db36e535969aa0444190f5617f9be7 100644
--- a/apps/server/src/provider/GrokProvider.test.ts
+++ b/apps/server/src/provider/GrokProvider.test.ts
@@ -8,7 +8,7 @@ import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Schema from "effect/Schema";
import { HttpClient, HttpClientResponse } from "effect/http";
-import { GrokSettings } from "@t3tools/contracts";
+import { GrokSettings } from "@t3tools/provider-grok/settings";
import {
buildGrokModelCapabilities,
@@ -17,9 +17,9 @@ import {
checkGrokProviderStatus,
grokSlashCommandsFromInitialize,
parseGrokModelsCliOutput,
-} from "./GrokProvider.ts";
+} from "@t3tools/provider-grok/testing";
import { execScriptSource, writeFakeCli } from "@t3tools/provider-testing/fakeCli";
-import { grokUsageResponseToLimits, readGrokAccount } from "./grokUsageLimits.ts";
+import { grokUsageResponseToLimits, readGrokAccount } from "@t3tools/provider-grok/testing";
const decodeGrokSettings = Schema.decodeSync(GrokSettings);
const __dirname = NodePath.dirname(NodeURL.fileURLToPath(import.meta.url));
diff --git a/apps/server/src/provider/GrokProvider.ts b/apps/server/src/provider/GrokProvider.ts
deleted file mode 100644
index 2899583a20c3df03095b72c8069b5a95745218c2..0000000000000000000000000000000000000000
--- a/apps/server/src/provider/GrokProvider.ts
+++ /dev/null
@@ -1,586 +0,0 @@
-import {
- type CustomModelSetting,
- type GrokSettings,
- type ModelCapabilities,
- type ServerProvider,
- type ServerProviderAuth,
- type ServerProviderModel,
- type ServerProviderSlashCommand,
-} from "@t3tools/contracts";
-import type * as EffectAcpSchema from "effect-acp/compat";
-import { causeErrorTag } from "@t3tools/shared/observability";
-import * as Crypto from "effect/Crypto";
-import * as DateTime from "effect/DateTime";
-import * as Effect from "effect/Effect";
-import * as Exit from "effect/Exit";
-import * as Option from "effect/Option";
-import * as Result from "effect/Result";
-import * as Schema from "effect/Schema";
-import { HttpClient } from "effect/http";
-import { ChildProcess, ChildProcessSpawner } from "effect/process";
-import { createModelCapabilities } from "@t3tools/shared/model";
-import { resolveSpawnCommand } from "@t3tools/shared/shell";
-
-import {
- AUTH_PROBE_TIMEOUT_MS,
- buildServerProvider,
- COMPACT_SLASH_COMMAND,
- isCommandMissingCause,
- parseGenericCliVersion,
- providerModelsFromSettings,
- spawnAndCollect,
- type ServerProviderDraft,
-} from "@t3tools/provider-core/server/snapshotProbe";
-import {
- enrichProviderSnapshotWithVersionAdvisory,
- type ProviderMaintenanceCapabilities,
-} from "@t3tools/provider-core/server/maintenanceResolver";
-import {
- GROK_DEFAULT_MODEL_SLUG,
- GROK_SUPPORTED_RUNTIME_MODES,
- isValidGrokReasoningEffortToken,
- makeGrokAcpRuntime,
- resolveGrokAcpBaseModelId,
-} from "./acp/GrokAcpSupport.ts";
-import { sessionModelStateFromInitialize } from "@t3tools/provider-acp/server/runtimeModel";
-import { discoverGrokSkills } from "./Drivers/GrokSkills.ts";
-
-const GROK_PRESENTATION = {
- displayName: "Grok",
- supportsConversationRollback: false,
- showInteractionModeToggle: false,
- supportedRuntimeModes: GROK_SUPPORTED_RUNTIME_MODES,
-} as const;
-const EMPTY_CAPABILITIES: ModelCapabilities = createModelCapabilities({
- optionDescriptors: [],
-});
-
-const VERSION_PROBE_TIMEOUT_MS = 4_000;
-// `initialize` is a single local round trip, so this is generous even on slow machines.
-const GROK_ACP_INITIALIZE_TIMEOUT_MS = 8_000;
-const GROK_API_KEY_ENV = "XAI_API_KEY";
-
-const GROK_BUILT_IN_MODELS: ReadonlyArray = [
- {
- slug: GROK_DEFAULT_MODEL_SLUG,
- name: "Grok Build",
- isCustom: false,
- capabilities: EMPTY_CAPABILITIES,
- },
-];
-
-export function buildInitialGrokProviderSnapshot(
- grokSettings: GrokSettings,
-): Effect.Effect {
- return Effect.gen(function* () {
- const checkedAt = yield* Effect.map(DateTime.now, DateTime.formatIso);
- const models = grokModelsFromSettings(grokSettings.customModels);
-
- if (!grokSettings.enabled) {
- return buildServerProvider({
- presentation: GROK_PRESENTATION,
- enabled: false,
- checkedAt,
- models,
- probe: {
- installed: false,
- version: null,
- status: "warning",
- auth: { status: "unknown" },
- message: "Grok is disabled in T3 Code settings.",
- },
- });
- }
-
- return buildServerProvider({
- presentation: GROK_PRESENTATION,
- enabled: true,
- checkedAt,
- models,
- probe: {
- installed: true,
- version: null,
- status: "warning",
- auth: { status: "unknown" },
- message: "Checking Grok CLI availability...",
- },
- });
- });
-}
-
-function grokModelsFromSettings(
- customModels: ReadonlyArray | undefined,
- builtInModels: ReadonlyArray = GROK_BUILT_IN_MODELS,
-): ReadonlyArray {
- return providerModelsFromSettings(builtInModels, customModels ?? [], EMPTY_CAPABILITIES);
-}
-
-function nonEmptyString(value: unknown): string | undefined {
- return typeof value === "string" ? value.trim() || undefined : undefined;
-}
-
-function isRecord(value: unknown): value is Record {
- return typeof value === "object" && value !== null && !Array.isArray(value);
-}
-
-function grokReasoningOptionsFromModel(model: EffectAcpSchema.ModelInfo): {
- readonly options: ReadonlyArray<{
- value: string;
- label: string;
- description?: string;
- isDefault?: boolean;
- }>;
- readonly currentValue: string | undefined;
-} {
- const meta = model._meta;
- if (!meta || meta.supportsReasoningEffort === false) {
- return { options: [], currentValue: undefined };
- }
-
- const currentEffort = nonEmptyString(meta.reasoningEffort);
- const advertisedOptions = Array.isArray(meta.reasoningEfforts) ? meta.reasoningEfforts : [];
- const seen = new Set();
- const options: Array<{
- value: string;
- label: string;
- description?: string;
- advertisedDefault: boolean;
- }> = [];
-
- for (const entry of advertisedOptions) {
- if (!isRecord(entry)) {
- continue;
- }
- const rawValue = nonEmptyString(entry.value);
- const rawId = nonEmptyString(entry.id);
- const value =
- rawValue && isValidGrokReasoningEffortToken(rawValue)
- ? rawValue
- : rawId && isValidGrokReasoningEffortToken(rawId)
- ? rawId
- : undefined;
- if (value === undefined || seen.has(value)) {
- continue;
- }
- seen.add(value);
- const description = nonEmptyString(entry.description);
- options.push({
- value,
- label: nonEmptyString(entry.label) ?? value,
- ...(description ? { description } : {}),
- advertisedDefault: entry.default === true || entry.isDefault === true,
- });
- }
-
- const currentValue =
- currentEffort && options.some((option) => option.value === currentEffort)
- ? currentEffort
- : undefined;
- const advertisedDefaults = options.filter((option) => option.advertisedDefault);
- const selectedDefault =
- advertisedDefaults.find((option) => option.value === currentValue)?.value ??
- advertisedDefaults[0]?.value;
- return {
- options: options.map(({ value, label, description }) => ({
- value,
- label,
- ...(description ? { description } : {}),
- ...(value === selectedDefault ? { isDefault: true } : {}),
- })),
- currentValue: currentValue ?? selectedDefault,
- };
-}
-
-export function buildGrokModelCapabilities(model: EffectAcpSchema.ModelInfo): ModelCapabilities {
- const reasoning = grokReasoningOptionsFromModel(model);
- return reasoning.options.length > 0
- ? createModelCapabilities({
- optionDescriptors: [
- {
- id: "reasoningEffort",
- label: "Reasoning",
- type: "select",
- options: reasoning.options.map((option) => ({
- id: option.value,
- label: option.label,
- ...(option.description ? { description: option.description } : {}),
- ...(option.isDefault ? { isDefault: true } : {}),
- })),
- ...(reasoning.currentValue ? { currentValue: reasoning.currentValue } : {}),
- },
- ],
- })
- : EMPTY_CAPABILITIES;
-}
-
-/** Models advertised by the ACP agent, with the session's current model marked as default. */
-export function buildGrokModelsFromSessionModelState(
- modelState: EffectAcpSchema.SessionModelState | null | undefined,
-): ReadonlyArray {
- if (!modelState || modelState.availableModels.length === 0) {
- return [];
- }
- const currentModelId = modelState.currentModelId.trim();
- const seen = new Set();
- return modelState.availableModels.flatMap((model): ServerProviderModel[] => {
- const slug = resolveGrokAcpBaseModelId(model.modelId);
- if (!slug || seen.has(slug)) {
- return [];
- }
- seen.add(slug);
- return [
- {
- slug,
- name: model.name.trim() || slug,
- isCustom: false,
- ...(model.modelId.trim() === currentModelId ? { isDefault: true } : {}),
- capabilities: buildGrokModelCapabilities(model),
- },
- ];
- });
-}
-
-export interface GrokModelsCliOutput {
- /** True or false when the CLI printed a login line, null when it printed neither. */
- readonly authenticated: boolean | null;
- readonly models: ReadonlyArray;
-}
-
-/**
- * Parses `grok models`. The command exits 0 whether or not the user is logged in, so the
- * text is the only signal. Current output looks like:
- *
- * You are logged in with grok.com.
- * Default model: grok-4.6
- * Available models:
- * * grok-4.6 (default)
- * - grok-4.5
- */
-export function parseGrokModelsCliOutput(output: string): GrokModelsCliOutput {
- const authenticated = /you are logged in/i.test(output)
- ? true
- : /not authenticated|not logged in/i.test(output)
- ? false
- : null;
-
- const seen = new Set();
- const models: ServerProviderModel[] = [];
- for (const line of output.split(/\r?\n/)) {
- const bullet = line.match(/^\s*[*-]\s+(\S+)(.*)$/);
- if (!bullet?.[1]) {
- continue;
- }
- const slug = resolveGrokAcpBaseModelId(bullet[1]);
- if (seen.has(slug)) {
- continue;
- }
- seen.add(slug);
- models.push({
- slug,
- name: displayNameFromGrokModelSlug(slug),
- isCustom: false,
- ...(/\(default\)/i.test(bullet[2] ?? "") ? { isDefault: true } : {}),
- capabilities: EMPTY_CAPABILITIES,
- });
- }
- return { authenticated, models };
-}
-
-function displayNameFromGrokModelSlug(slug: string): string {
- return slug
- .split(/[-_]/g)
- .map((part) => (part.toLowerCase() === "grok" ? "Grok" : part))
- .join(" ");
-}
-
-const runGrokCliCommand = (
- grokSettings: GrokSettings,
- args: ReadonlyArray,
- environment: NodeJS.ProcessEnv,
-) =>
- Effect.gen(function* () {
- const command = grokSettings.binaryPath || "grok";
- const spawnCommand = yield* resolveSpawnCommand(command, args, { env: environment });
- return yield* spawnAndCollect(
- command,
- ChildProcess.make(spawnCommand.command, spawnCommand.args, {
- env: environment,
- shell: spawnCommand.shell,
- }),
- );
- });
-
-const decodeAvailableCommands = Schema.decodeUnknownOption(Schema.Array(Schema.Unknown));
-const decodeAvailableCommand = Schema.decodeUnknownOption(
- Schema.Struct({
- name: Schema.String,
- description: Schema.String,
- input: Schema.optional(Schema.NullOr(Schema.Struct({ hint: Schema.String }))),
- }),
-);
-
-export function grokSlashCommandsFromInitialize(
- initialized: EffectAcpSchema.InitializeResponse,
-): ReadonlyArray {
- const commands = decodeAvailableCommands(initialized._meta?.availableCommands);
- const byName = new Map([
- [COMPACT_SLASH_COMMAND.name, COMPACT_SLASH_COMMAND],
- ]);
- for (const entry of Option.getOrElse(commands, () => [])) {
- const decoded = decodeAvailableCommand(entry);
- if (Option.isNone(decoded)) continue;
- const command = decoded.value;
- const name = command.name.trim();
- // Permission changes must go through T3 so the client and provider agree.
- if (!name || name.toLowerCase() === "always-approve") continue;
- // Grok advertises /context, but its ACP handler completes without emitting output.
- if (name.toLowerCase() === "context") continue;
- const description = command.description.trim();
- const hint = command.input?.hint.trim();
- byName.set(name, {
- name,
- ...(description ? { description } : {}),
- ...(hint ? { input: { hint } } : {}),
- });
- }
- return [...byName.values()];
-}
-
-/**
- * Reads model and command metadata from `initialize._meta`. This never calls `authenticate`
- * or `session/new`, so it cannot open a browser login or boot the workspace's MCP servers.
- */
-const discoverGrokMetadataViaAcpInitialize = (
- grokSettings: GrokSettings,
- environment: NodeJS.ProcessEnv,
-) =>
- Effect.gen(function* () {
- const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;
- const acp = yield* makeGrokAcpRuntime({
- grokSettings,
- environment,
- childProcessSpawner,
- cwd: process.cwd(),
- clientInfo: { name: "t3-code-provider-probe", version: "0.0.0" },
- });
- const initialized = yield* acp.initialize();
- return {
- models: buildGrokModelsFromSessionModelState(sessionModelStateFromInitialize(initialized)),
- slashCommands: grokSlashCommandsFromInitialize(initialized),
- };
- }).pipe(Effect.scoped);
-
-export const checkGrokProviderStatus = Effect.fn("checkGrokProviderStatus")(function* (
- grokSettings: GrokSettings,
- environment: NodeJS.ProcessEnv = process.env,
- cwd?: string,
-): Effect.fn.Return<
- ServerProviderDraft,
- never,
- ChildProcessSpawner.ChildProcessSpawner | Crypto.Crypto
-> {
- const checkedAt = DateTime.formatIso(yield* DateTime.now);
- const fallbackModels = grokModelsFromSettings(grokSettings.customModels);
-
- if (!grokSettings.enabled) {
- return buildServerProvider({
- presentation: GROK_PRESENTATION,
- enabled: false,
- checkedAt,
- models: fallbackModels,
- probe: {
- installed: false,
- version: null,
- status: "warning",
- auth: { status: "unknown" },
- message: "Grok is disabled in T3 Code settings.",
- },
- });
- }
-
- const versionResult = yield* runGrokCliCommand(grokSettings, ["--version"], environment).pipe(
- Effect.timeoutOption(VERSION_PROBE_TIMEOUT_MS),
- Effect.result,
- );
-
- if (Result.isFailure(versionResult)) {
- const error = versionResult.failure;
- yield* Effect.logWarning("Grok CLI health check failed.", {
- errorTag: error._tag,
- });
- return buildServerProvider({
- presentation: GROK_PRESENTATION,
- enabled: grokSettings.enabled,
- checkedAt,
- models: fallbackModels,
- probe: {
- installed: !isCommandMissingCause(error),
- version: null,
- status: "error",
- auth: { status: "unknown" },
- message: isCommandMissingCause(error)
- ? "Grok CLI (`grok`) is not installed or not on PATH."
- : "Failed to execute Grok CLI health check.",
- },
- });
- }
-
- if (Option.isNone(versionResult.success)) {
- return buildServerProvider({
- presentation: GROK_PRESENTATION,
- enabled: grokSettings.enabled,
- checkedAt,
- models: fallbackModels,
- probe: {
- installed: true,
- version: null,
- status: "error",
- auth: { status: "unknown" },
- message: "Grok CLI is installed but timed out while running `grok --version`.",
- },
- });
- }
-
- const versionOutput = versionResult.success.value;
- const version = parseGenericCliVersion(`${versionOutput.stdout}\n${versionOutput.stderr}`);
- if (versionOutput.code !== 0) {
- yield* Effect.logWarning("Grok CLI version probe exited with a non-zero status.", {
- exitCode: versionOutput.code,
- stdoutLength: versionOutput.stdout.length,
- stderrLength: versionOutput.stderr.length,
- });
- return buildServerProvider({
- presentation: GROK_PRESENTATION,
- enabled: grokSettings.enabled,
- checkedAt,
- models: fallbackModels,
- probe: {
- installed: true,
- version,
- status: "error",
- auth: { status: "unknown" },
- message: "Grok CLI is installed but failed to run.",
- },
- });
- }
-
- // `grok models` reports login state and model slugs without starting the agent.
- const modelsResult = yield* runGrokCliCommand(grokSettings, ["models"], environment).pipe(
- Effect.timeoutOption(AUTH_PROBE_TIMEOUT_MS),
- Effect.result,
- );
- // Only a clean exit is parsed. Failed invocations print help or error text that
- // must not be read as model slugs or as a login verdict.
- const modelsOutput =
- Result.isSuccess(modelsResult) &&
- Option.isSome(modelsResult.success) &&
- modelsResult.success.value.code === 0
- ? modelsResult.success.value
- : undefined;
- const cliModels: GrokModelsCliOutput = modelsOutput
- ? parseGrokModelsCliOutput(`${modelsOutput.stdout}\n${modelsOutput.stderr}`)
- : { authenticated: null, models: [] };
- if (!modelsOutput) {
- yield* Effect.logWarning("Grok CLI model listing failed or timed out.", {
- errorTag: Result.isFailure(modelsResult)
- ? modelsResult.failure._tag
- : Option.isNone(modelsResult.success)
- ? "Timeout"
- : `ExitCode${modelsResult.success.value.code}`,
- });
- }
-
- const auth: ServerProviderAuth = environment[GROK_API_KEY_ENV]?.trim()
- ? { status: "authenticated", type: "api_key", label: "xAI API key" }
- : cliModels.authenticated === true
- ? { status: "authenticated", type: "cached_token", label: "Grok account" }
- : cliModels.authenticated === false
- ? { status: "unauthenticated" }
- : { status: "unknown" };
-
- const skills = yield* discoverGrokSkills(grokSettings, environment, cwd).pipe(
- Effect.tapError((cause) => Effect.logDebug("Grok skill discovery failed.", { cause })),
- Effect.orElseSucceed(() => []),
- );
-
- const acpExit = yield* discoverGrokMetadataViaAcpInitialize(grokSettings, environment).pipe(
- Effect.timeoutOption(GROK_ACP_INITIALIZE_TIMEOUT_MS),
- Effect.exit,
- );
- const acpMetadata = Exit.isSuccess(acpExit) ? Option.getOrUndefined(acpExit.value) : undefined;
- const acpModels = acpMetadata?.models ?? [];
- const acpFailed = Exit.isFailure(acpExit) || Option.isNone(acpExit.value);
- if (acpFailed) {
- yield* Effect.logWarning("Grok ACP initialize probe failed or timed out.", {
- errorTag: Exit.isFailure(acpExit) ? causeErrorTag(acpExit.cause) : "Timeout",
- });
- }
-
- const discoveredModels = acpModels.length > 0 ? acpModels : cliModels.models;
- const models =
- discoveredModels.length > 0
- ? grokModelsFromSettings(grokSettings.customModels, discoveredModels)
- : fallbackModels;
-
- if (auth.status === "unauthenticated") {
- return buildServerProvider({
- presentation: GROK_PRESENTATION,
- enabled: grokSettings.enabled,
- checkedAt,
- models,
- skills,
- probe: {
- installed: true,
- version,
- status: "error",
- auth,
- message: "Grok CLI is installed but not logged in. Run `grok login`.",
- },
- });
- }
-
- return buildServerProvider({
- presentation: GROK_PRESENTATION,
- enabled: grokSettings.enabled,
- checkedAt,
- models,
- skills,
- slashCommands: acpMetadata?.slashCommands ?? [COMPACT_SLASH_COMMAND],
- probe: {
- installed: true,
- version,
- // A failed metadata probe degrades the model picker, it does not make chats fail.
- status: acpFailed ? "warning" : "ready",
- auth,
- ...(acpFailed
- ? {
- message:
- "Grok CLI is installed but ACP initialize failed. Model options may be incomplete.",
- }
- : {}),
- },
- });
-});
-
-export const enrichGrokSnapshot = (input: {
- readonly snapshot: ServerProvider;
- readonly maintenanceCapabilities: ProviderMaintenanceCapabilities;
- readonly enableProviderUpdateChecks?: boolean;
- readonly publishSnapshot: (snapshot: ServerProvider) => Effect.Effect;
- readonly httpClient: HttpClient.HttpClient;
-}): Effect.Effect => {
- const { snapshot, publishSnapshot } = input;
-
- return enrichProviderSnapshotWithVersionAdvisory(snapshot, input.maintenanceCapabilities, {
- enableProviderUpdateChecks: input.enableProviderUpdateChecks,
- }).pipe(
- Effect.provideService(HttpClient.HttpClient, input.httpClient),
- Effect.flatMap((enrichedSnapshot) => publishSnapshot(enrichedSnapshot)),
- Effect.catchCause((cause) =>
- Effect.logWarning("Grok version advisory enrichment failed", {
- errorTag: causeErrorTag(cause),
- }),
- ),
- Effect.asVoid,
- );
-};
diff --git a/apps/server/src/provider/ProviderInstanceRegistry.test.ts b/apps/server/src/provider/ProviderInstanceRegistry.test.ts
index 353c29fb90daaee7a7692b2478d6b6caec71ef7f..933751bdd39b73da00f56970e83c301915c032eb 100644
--- a/apps/server/src/provider/ProviderInstanceRegistry.test.ts
+++ b/apps/server/src/provider/ProviderInstanceRegistry.test.ts
@@ -31,11 +31,11 @@ import {
EnvironmentId,
type ClaudeSettings,
type CodexSettings,
- type GrokSettings,
ProviderDriverKind,
type ProviderInstanceConfigMap,
ProviderInstanceId,
} from "@t3tools/contracts";
+import type { GrokSettings } from "@t3tools/provider-grok/settings";
import type { CursorSettings } from "@t3tools/provider-cursor/settings";
import { HostProcessPlatform, isHostWindows } from "@t3tools/shared/hostProcess";
import * as DateTime from "effect/DateTime";
@@ -54,7 +54,7 @@ import * as ServerSettings from "../serverSettings.ts";
import { ClaudeDriver, type ClaudeDriverEnv } from "./Drivers/ClaudeDriver.ts";
import { CodexDriver, type CodexDriverEnv } from "./Drivers/CodexDriver.ts";
import { CursorDriver, type CursorDriverEnv } from "@t3tools/provider-cursor/server";
-import { GrokDriver, type GrokDriverEnv } from "./Drivers/GrokDriver.ts";
+import { GrokDriver, type GrokDriverEnv } from "@t3tools/provider-grok/server";
import { OpenCodeDriver, type OpenCodeDriverEnv } from "@t3tools/provider-opencode/server";
import type { OpenCodeSettings } from "@t3tools/provider-opencode/settings";
import * as ModelManifest from "./ModelManifest.ts";
diff --git a/apps/server/src/provider/acp/GrokAcpCliProbe.test.ts b/apps/server/src/provider/acp/GrokAcpCliProbe.test.ts
deleted file mode 100644
index b5c8177d61f0f07aebb38cfc25da1a978042cac9..0000000000000000000000000000000000000000
--- a/apps/server/src/provider/acp/GrokAcpCliProbe.test.ts
+++ /dev/null
@@ -1,110 +0,0 @@
-/**
- * Optional integration check against a real `grok agent stdio` install.
- * Enable with: T3_GROK_ACP_PROBE=1 vp test run GrokAcpCliProbe
- * Set T3_GROK_LIVE_TURN=1 to also send a small prompt to the real model.
- *
- * The probe assumes either `XAI_API_KEY` is set in the environment or
- * the user has previously run `grok login`. Without credentials the
- * agent's `authenticate` request will fail and the test will surface
- * the error.
- */
-import * as NodeServices from "@effect/platform-node/NodeServices";
-import { it } from "@effect/vitest";
-import * as Effect from "effect/Effect";
-import * as Deferred from "effect/Deferred";
-import * as Fiber from "effect/Fiber";
-import * as FileSystem from "effect/FileSystem";
-import * as Stream from "effect/Stream";
-import { ChildProcessSpawner } from "effect/process";
-import { describe, expect } from "vite-plus/test";
-
-import { makeGrokAcpRuntime } from "./GrokAcpSupport.ts";
-
-const makeProbeRuntime = Effect.gen(function* () {
- const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;
- return yield* makeGrokAcpRuntime({
- grokSettings: { binaryPath: "grok" },
- environment: process.env,
- childProcessSpawner,
- cwd: process.cwd(),
- clientInfo: { name: "t3-grok-probe", version: "0.0.0" },
- });
-});
-
-describe.runIf(process.env.T3_GROK_ACP_PROBE === "1")("Grok ACP CLI probe", () => {
- it.effect("initialize and authenticate against real grok agent stdio", () =>
- Effect.gen(function* () {
- const runtime = yield* makeProbeRuntime;
- const started = yield* runtime.start();
- expect(started.initializeResult).toBeDefined();
- }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
- );
-
- it.effect("completes a prompt on the agent's default model", () =>
- Effect.gen(function* () {
- const runtime = yield* makeProbeRuntime;
- const started = yield* runtime.start();
- expect(typeof started.sessionId).toBe("string");
-
- const result = yield* runtime
- .prompt({ prompt: [{ type: "text", text: "Respond with exactly: grok switch ok" }] })
- .pipe(Effect.timeout("60 seconds"));
- expect(result.stopReason).toBe("end_turn");
- }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
- );
- it.effect("session/set_model accepts advertised reasoning effort metadata", () =>
- Effect.gen(function* () {
- const runtime = yield* makeProbeRuntime;
- const started = yield* runtime.start();
- const modelState = started.sessionSetupResult.models;
- const currentModelId = modelState?.currentModelId.trim();
- expect(currentModelId).toBeDefined();
- if (!currentModelId) return;
-
- const currentModel = modelState?.availableModels.find(
- (model) => model.modelId.trim() === currentModelId,
- );
- const reasoningEffort = currentModel?._meta?.reasoningEffort;
- expect(typeof reasoningEffort).toBe("string");
- if (typeof reasoningEffort !== "string") return;
-
- yield* runtime.setSessionModel(currentModelId, { reasoningEffort });
- }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
- );
-
- it.effect.skipIf(process.env.T3_GROK_LIVE_TURN !== "1")(
- "finishes a real Grok turn and streams its answer",
- () =>
- Effect.gen(function* () {
- const fileSystem = yield* FileSystem.FileSystem;
- const cwd = yield* fileSystem.makeTempDirectoryScoped();
- const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;
- const runtime = yield* makeGrokAcpRuntime({
- grokSettings: { binaryPath: "grok" },
- environment: process.env,
- childProcessSpawner,
- cwd,
- runtimeMode: "approval-required",
- clientInfo: { name: "t3-grok-probe", version: "0.0.0" },
- });
- yield* runtime.start();
- const chunks: string[] = [];
- const events = yield* Stream.runForEach(runtime.getEvents(), (event) => {
- if (event._tag === "EventStreamBarrier") {
- return Deferred.succeed(event.acknowledge, undefined);
- }
- if (event._tag === "ContentDelta") {
- chunks.push(event.text);
- }
- return Effect.void;
- }).pipe(Effect.forkChild);
- const result = yield* runtime.prompt({
- prompt: [{ type: "text", text: "Reply exactly GROK_T3_OK. Do not use any tools." }],
- });
- yield* runtime.drainEvents;
- expect(result.stopReason).toBe("end_turn");
- expect(chunks.join("")).toContain("GROK_T3_OK");
- yield* Fiber.interrupt(events);
- }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
- );
-});
diff --git a/apps/server/src/provider/acp/GrokAcpSupport.test.ts b/apps/server/src/provider/acp/GrokAcpSupport.test.ts
deleted file mode 100644
index d15b12fbc2dad20a1392b4cd03cd2f323749862c..0000000000000000000000000000000000000000
--- a/apps/server/src/provider/acp/GrokAcpSupport.test.ts
+++ /dev/null
@@ -1,265 +0,0 @@
-import { describe, expect, it } from "@effect/vitest";
-
-import * as Effect from "effect/Effect";
-import * as EffectAcpErrors from "effect-acp/errors";
-
-import {
- applyGrokAcpModelSelection,
- buildGrokAcpSpawnInput,
- grokAcpRuntimeProcessOwnership,
- grokAcpSpawnArgs,
- isValidGrokReasoningEffortToken,
- resolveGrokAcpBaseModelId,
-} from "./GrokAcpSupport.ts";
-
-describe("grokAcpRuntimeProcessOwnership", () => {
- it("opts Grok into detached process-tree ownership on the injected host platform", () => {
- expect(grokAcpRuntimeProcessOwnership("linux")).toEqual({
- ownDescendantProcessGroups: true,
- ownDetachedProcessGroup: true,
- processGroupPlatform: "linux",
- });
- });
-
- it("uses the prior provider-group path on Darwin and Windows", () => {
- expect(grokAcpRuntimeProcessOwnership("darwin")).toEqual({
- ownDescendantProcessGroups: false,
- ownDetachedProcessGroup: true,
- processGroupPlatform: "darwin",
- });
- expect(grokAcpRuntimeProcessOwnership("win32")).toEqual({
- ownDescendantProcessGroups: false,
- ownDetachedProcessGroup: true,
- processGroupPlatform: "win32",
- });
- });
-});
-
-describe("resolveGrokAcpBaseModelId", () => {
- it("normalizes empty and custom Grok model ids", () => {
- expect(resolveGrokAcpBaseModelId(undefined)).toBe("grok-build");
- expect(resolveGrokAcpBaseModelId(" ")).toBe("grok-build");
- expect(resolveGrokAcpBaseModelId(" grok-test-custom-model ")).toBe("grok-test-custom-model");
- });
-});
-
-describe("grokAcpSpawnArgs", () => {
- it("inherits the Grok CLI config when no T3 runtime mode is set", () => {
- expect(grokAcpSpawnArgs()).toEqual(["agent", "stdio"]);
- });
-
- it("forces Grok to ask when T3 is Supervised", () => {
- expect(grokAcpSpawnArgs("approval-required")).toEqual([
- "--permission-mode",
- "default",
- "agent",
- "stdio",
- ]);
- });
-
- it("maps Full access to Grok always-approve", () => {
- expect(grokAcpSpawnArgs("full-access")).toEqual(["agent", "--always-approve", "stdio"]);
- });
-
- it("launches Auto on Grok's classifier and a mode Grok does not offer asking", () => {
- expect(grokAcpSpawnArgs("auto")).toEqual(["--permission-mode", "auto", "agent", "stdio"]);
- expect(grokAcpSpawnArgs("auto-accept-edits")).toEqual([
- "--permission-mode",
- "default",
- "agent",
- "stdio",
- ]);
- });
-});
-
-describe("buildGrokAcpSpawnInput", () => {
- it("passes the T3 Code referrer through Grok OAuth env", () => {
- const spawn = buildGrokAcpSpawnInput({ binaryPath: "/usr/local/bin/grok" }, "/tmp/project", {
- XAI_API_KEY: "secret",
- GROK_OAUTH2_REFERRER: "other-client",
- });
-
- expect(spawn).toEqual({
- command: "/usr/local/bin/grok",
- args: ["agent", "stdio"],
- cwd: "/tmp/project",
- env: {
- XAI_API_KEY: "secret",
- GROK_OAUTH2_REFERRER: "t3code",
- },
- });
- });
-
- it("puts Supervised on the Grok argv so config always-approve cannot win", () => {
- const spawn = buildGrokAcpSpawnInput(
- { binaryPath: "/usr/local/bin/grok" },
- "/tmp/project",
- undefined,
- "approval-required",
- );
- expect(spawn.args).toEqual(["--permission-mode", "default", "agent", "stdio"]);
- });
-});
-
-describe("isValidGrokReasoningEffortToken", () => {
- it("accepts future ACP tokens and rejects malformed metadata values", () => {
- expect(isValidGrokReasoningEffortToken("xhigh")).toBe(true);
- expect(isValidGrokReasoningEffortToken("turbo_v2")).toBe(true);
- expect(isValidGrokReasoningEffortToken("not a token")).toBe(false);
- expect(isValidGrokReasoningEffortToken("-leading-dash")).toBe(false);
- expect(isValidGrokReasoningEffortToken("x".repeat(33))).toBe(false);
- });
-});
-
-describe("applyGrokAcpModelSelection", () => {
- const makeRecordingRuntime = (failure?: EffectAcpErrors.AcpError) => {
- const modelCalls: Array<{
- modelId: string;
- meta?: { readonly [key: string]: unknown } | null;
- }> = [];
- const runtime = {
- setSessionModel: (modelId: string, meta?: { readonly [key: string]: unknown } | null) =>
- Effect.gen(function* () {
- modelCalls.push(meta === undefined ? { modelId } : { modelId, meta });
- if (failure) return yield* failure;
- return {};
- }),
- };
- return { runtime, modelCalls };
- };
-
- it.effect("calls session/set_model when the requested model differs from current", () =>
- Effect.gen(function* () {
- const { runtime, modelCalls } = makeRecordingRuntime();
- const result = yield* applyGrokAcpModelSelection({
- runtime,
- currentModelId: "grok-build",
- requestedModelId: "grok-mock-alt",
- mapError: (cause) => cause.message,
- });
- expect(modelCalls).toEqual([{ modelId: "grok-mock-alt" }]);
- expect(result).toBe("grok-mock-alt");
- }),
- );
-
- it.effect("applies reasoning effort through session/set_model metadata", () =>
- Effect.gen(function* () {
- const { runtime, modelCalls } = makeRecordingRuntime();
- const result = yield* applyGrokAcpModelSelection({
- runtime,
- currentModelId: "grok-4.6",
- currentReasoningEffort: "high",
- requestedModelId: "grok-4.6",
- requestedReasoningEffort: "xhigh",
- mapError: (cause) => cause.message,
- });
- expect(modelCalls).toEqual([{ modelId: "grok-4.6", meta: { reasoningEffort: "xhigh" } }]);
- expect(result).toBe("grok-4.6");
- }),
- );
-
- it.effect("does not clear reasoning when same-model selection omits effort", () =>
- Effect.gen(function* () {
- const { runtime, modelCalls } = makeRecordingRuntime();
- const result = yield* applyGrokAcpModelSelection({
- runtime,
- currentModelId: "grok-4.6",
- currentReasoningEffort: "high",
- requestedModelId: "grok-4.6",
- requestedReasoningEffort: undefined,
- mapError: (cause) => cause.message,
- });
- expect(modelCalls).toEqual([]);
- expect(result).toBe("grok-4.6");
- }),
- );
-
- it.effect("drops malformed effort metadata instead of sending it", () =>
- Effect.gen(function* () {
- const { runtime, modelCalls } = makeRecordingRuntime();
- yield* applyGrokAcpModelSelection({
- runtime,
- currentModelId: "grok-4.6",
- currentReasoningEffort: "high",
- requestedModelId: "grok-4.6",
- requestedReasoningEffort: "not a token",
- mapError: (cause) => cause.message,
- });
- expect(modelCalls).toEqual([{ modelId: "grok-4.6" }]);
- }),
- );
-
- it.effect("skips set_model when requested matches current", () =>
- Effect.gen(function* () {
- const { runtime, modelCalls } = makeRecordingRuntime();
- const result = yield* applyGrokAcpModelSelection({
- runtime,
- currentModelId: "grok-build",
- requestedModelId: "grok-build",
- mapError: (cause) => cause.message,
- });
- expect(modelCalls).toEqual([]);
- expect(result).toBe("grok-build");
- }),
- );
-
- it.effect("keeps the session's current model when the product slug is requested", () =>
- Effect.gen(function* () {
- const { runtime, modelCalls } = makeRecordingRuntime();
- const result = yield* applyGrokAcpModelSelection({
- runtime,
- currentModelId: "grok-4.6",
- requestedModelId: "grok-build",
- mapError: (cause) => cause.message,
- });
- expect(modelCalls).toEqual([]);
- expect(result).toBe("grok-4.6");
- }),
- );
-
- it.effect("applies reasoning to the current model when the product slug is requested", () =>
- Effect.gen(function* () {
- const { runtime, modelCalls } = makeRecordingRuntime();
- const result = yield* applyGrokAcpModelSelection({
- runtime,
- currentModelId: "grok-4.6",
- currentReasoningEffort: "high",
- requestedModelId: "grok-build",
- requestedReasoningEffort: "xhigh",
- mapError: (cause) => cause.message,
- });
- expect(modelCalls).toEqual([{ modelId: "grok-4.6", meta: { reasoningEffort: "xhigh" } }]);
- expect(result).toBe("grok-4.6");
- }),
- );
-
- it.effect("skips set_model when no model is requested", () =>
- Effect.gen(function* () {
- const { runtime, modelCalls } = makeRecordingRuntime();
- const result = yield* applyGrokAcpModelSelection({
- runtime,
- currentModelId: "grok-build",
- requestedModelId: undefined,
- mapError: (cause) => cause.message,
- });
- expect(modelCalls).toEqual([]);
- expect(result).toBe("grok-build");
- }),
- );
-
- it.effect("propagates session/set_model failures via mapError", () =>
- Effect.gen(function* () {
- const failure = EffectAcpErrors.AcpRequestError.invalidParams("session id not known");
- const { runtime } = makeRecordingRuntime(failure);
- const error = yield* Effect.flip(
- applyGrokAcpModelSelection({
- runtime,
- currentModelId: "grok-build",
- requestedModelId: "grok-mock-alt",
- mapError: (cause) => cause.message,
- }),
- );
- expect(error).toBe(failure.message);
- }),
- );
-});
diff --git a/apps/server/src/provider/acp/GrokAcpSupport.ts b/apps/server/src/provider/acp/GrokAcpSupport.ts
deleted file mode 100644
index 57b4c8fb9d421145613af2f2041ad4f1142960fb..0000000000000000000000000000000000000000
--- a/apps/server/src/provider/acp/GrokAcpSupport.ts
+++ /dev/null
@@ -1,278 +0,0 @@
-import type * as EffectAcpSchema from "effect-acp/compat";
-import * as NodeServices from "@effect/platform-node/NodeServices";
-import {
- type GrokSettings,
- type ProviderApprovalOption,
- ProviderDriverKind,
- type RuntimeMode,
-} from "@t3tools/contracts";
-import * as Crypto from "effect/Crypto";
-import * as Effect from "effect/Effect";
-import * as Layer from "effect/Layer";
-import * as Scope from "effect/Scope";
-import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
-import * as EffectAcpErrors from "effect-acp/errors";
-import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
-import { normalizeModelSlug } from "@t3tools/shared/model";
-
-import * as AcpSessionRuntime from "@t3tools/provider-acp/server/AcpSessionRuntime";
-import { makeXAiPromptCompletionRuntime } from "./XAiAcpExtension.ts";
-
-const GROK_API_KEY_ENV = "XAI_API_KEY";
-const GROK_OAUTH2_REFERRER_ENV = "GROK_OAUTH2_REFERRER";
-const T3_CODE_OAUTH_REFERRER = "t3code";
-const GROK_AUTH_METHOD_API_KEY = "xai.api_key";
-const GROK_AUTH_METHOD_CACHED_TOKEN = "cached_token";
-const GROK_DRIVER_KIND = ProviderDriverKind.make("grok");
-
-type GrokAcpRuntimeGrokSettings = Pick;
-
-interface GrokAcpRuntimeInput extends Omit<
- AcpSessionRuntime.AcpSessionRuntimeOptions,
- "authMethodId" | "clientCapabilities" | "spawn"
-> {
- readonly childProcessSpawner: ChildProcessSpawner.ChildProcessSpawner["Service"];
- readonly grokSettings: GrokAcpRuntimeGrokSettings | null | undefined;
- readonly environment?: NodeJS.ProcessEnv;
- readonly runtimeMode?: RuntimeMode;
-}
-
-/**
- * The runtime modes `grok agent` can launch in: ask, its auto classifier, and
- * always-approve. It has no Auto-accept edits: `acceptEdits` only exists as a
- * settings-file `permissions.defaultMode`, and `grok agent` treats it as ask.
- */
-export const GROK_SUPPORTED_RUNTIME_MODES = [
- "approval-required",
- "auto",
- "full-access",
-] as const satisfies ReadonlyArray;
-
-/**
- * Launch argv for a runtime mode. `--permission-mode` on the argv beats the
- * user's Grok config, so Supervised cannot inherit a configured always-approve.
- * A mode Grok does not offer launches asking.
- */
-export function grokAcpSpawnArgs(runtimeMode?: RuntimeMode): ReadonlyArray {
- switch (runtimeMode) {
- case undefined:
- return ["agent", "stdio"];
- case "auto":
- return ["--permission-mode", "auto", "agent", "stdio"];
- case "full-access":
- return ["agent", "--always-approve", "stdio"];
- default:
- return ["--permission-mode", "default", "agent", "stdio"];
- }
-}
-
-export function buildGrokAcpSpawnInput(
- grokSettings: GrokAcpRuntimeGrokSettings | null | undefined,
- cwd: string,
- environment?: NodeJS.ProcessEnv,
- runtimeMode?: RuntimeMode,
-): AcpSessionRuntime.AcpSpawnInput {
- return {
- command: grokSettings?.binaryPath || "grok",
- args: [...grokAcpSpawnArgs(runtimeMode)],
- cwd,
- env: {
- ...environment,
- [GROK_OAUTH2_REFERRER_ENV]: T3_CODE_OAUTH_REFERRER,
- },
- };
-}
-
-function resolveGrokAuthMethodId(environment: NodeJS.ProcessEnv | undefined): string {
- return environment?.[GROK_API_KEY_ENV]?.trim()
- ? GROK_AUTH_METHOD_API_KEY
- : GROK_AUTH_METHOD_CACHED_TOKEN;
-}
-
-export function grokAcpRuntimeProcessOwnership(
- processGroupPlatform: NodeJS.Platform,
-): Pick<
- AcpSessionRuntime.AcpSessionRuntimeOptions,
- "ownDescendantProcessGroups" | "ownDetachedProcessGroup" | "processGroupPlatform"
-> {
- return {
- // macOS keeps the prior provider-group teardown until a stable libproc
- // identity provider can cover Grok's nested detached tool groups.
- ownDescendantProcessGroups: processGroupPlatform === "linux",
- ownDetachedProcessGroup: true,
- processGroupPlatform,
- };
-}
-
-/**
- * Current Grok treats Ctrl+C cancellation as a barrier against stale
- * background-task wake prompts until the next genuine user turn. Replay sends
- * the same metadata so recorded cancels match.
- */
-export const GROK_ACP_CANCEL_META = { cancelTrigger: "ctrl_c" } as const;
-
-/**
- * Grok's Auto mode asks the client about an action its classifier blocks only
- * when the client declares a type that can show a prompt; the default
- * (`generic`) gets a silent denial instead. `extension` is the prompting type
- * that keeps the permission options T3 already maps (no always-approve row,
- * no per-command persistent grants).
- */
-export const GROK_ACP_INITIALIZE_META = { clientType: "extension" } as const;
-
-/**
- * Grok's only session-scoped `allow_always` answer: "Yes, allow all edits
- * during this session" on an edit prompt. Its bash, monitor and MCP
- * `always-allow` rows instead save a grant for the whole project that outlives
- * the session (grok-build `crates/codegen/xai-grok-workspace/src/permission/`
- * `prompter.rs` `ALLOW_EDITS_SESSION_OPTION_ID`, `grants.rs`
- * `record_prompt_outcome`).
- */
-const GROK_ALLOW_EDITS_SESSION_OPTION_ID = "allow-edits-session";
-
-/**
- * The approval choices a Grok permission prompt can honor. The session choice
- * appears only where Grok's answer lasts for the session.
- */
-export function grokApprovalOptions(
- request: EffectAcpSchema.RequestPermissionRequest,
-): ReadonlyArray {
- const has = (kind: EffectAcpSchema.PermissionOption["kind"], optionId?: string) =>
- request.options.some(
- (option) =>
- option.kind === kind && (optionId === undefined || option.optionId.trim() === optionId),
- );
- return [
- { decision: "cancel", label: "Cancel" },
- ...(has("reject_once") ? [{ decision: "decline", label: "Decline" } as const] : []),
- ...(has("allow_always", GROK_ALLOW_EDITS_SESSION_OPTION_ID)
- ? [{ decision: "acceptForSession", label: "Allow all edits this session" } as const]
- : []),
- ...(has("allow_once") ? [{ decision: "accept", label: "Approve" } as const] : []),
- ];
-}
-
-export const makeGrokAcpRuntime = (
- input: GrokAcpRuntimeInput,
-): Effect.Effect<
- AcpSessionRuntime.AcpSessionRuntime["Service"],
- EffectAcpErrors.AcpError,
- Crypto.Crypto | Scope.Scope
-> =>
- Effect.gen(function* () {
- const processGroupPlatform = yield* HostProcessPlatform.pipe(
- Effect.provide(NodeServices.layer),
- );
- const acpContext = yield* Layer.build(
- AcpSessionRuntime.layer({
- ...input,
- spawn: buildGrokAcpSpawnInput(
- input.grokSettings,
- input.cwd,
- input.environment,
- input.runtimeMode,
- ),
- authMethodId: resolveGrokAuthMethodId(input.environment),
- cancelMeta: { ...input.cancelMeta, ...GROK_ACP_CANCEL_META },
- initializeMeta: GROK_ACP_INITIALIZE_META,
- ...grokAcpRuntimeProcessOwnership(processGroupPlatform),
- }).pipe(
- Layer.provide(
- Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, input.childProcessSpawner),
- ),
- ),
- );
- const runtime = yield* Effect.service(AcpSessionRuntime.AcpSessionRuntime).pipe(
- Effect.provide(acpContext),
- );
- return yield* makeXAiPromptCompletionRuntime(runtime);
- });
-
-/**
- * T3's built-in Grok slug. It is the CLI's product name, not a model id the ACP accepts,
- * so selecting it means "use whatever model the Grok session currently runs on".
- */
-export const GROK_DEFAULT_MODEL_SLUG = "grok-build";
-
-export function resolveGrokAcpBaseModelId(model: string | null | undefined): string {
- const trimmed = model?.trim();
- const base = trimmed && trimmed.length > 0 ? trimmed : GROK_DEFAULT_MODEL_SLUG;
- return normalizeModelSlug(base, GROK_DRIVER_KIND) ?? GROK_DEFAULT_MODEL_SLUG;
-}
-
-const GROK_REASONING_EFFORT_TOKEN = /^[a-z0-9][a-z0-9._-]{0,31}$/i;
-
-export function isValidGrokReasoningEffortToken(value: string): boolean {
- return GROK_REASONING_EFFORT_TOKEN.test(value);
-}
-
-function normalizeGrokReasoningEffort(value: string | undefined): string | undefined {
- const effort = value?.trim();
- return effort && isValidGrokReasoningEffortToken(effort) ? effort : undefined;
-}
-
-export function currentGrokModelIdFromSessionSetup(
- sessionSetupResult:
- | EffectAcpSchema.LoadSessionResponse
- | EffectAcpSchema.NewSessionResponse
- | EffectAcpSchema.ResumeSessionResponse,
-): string | undefined {
- return sessionSetupResult.models?.currentModelId?.trim() || undefined;
-}
-
-export function currentGrokReasoningEffortFromSessionSetup(
- sessionSetupResult:
- | EffectAcpSchema.LoadSessionResponse
- | EffectAcpSchema.NewSessionResponse
- | EffectAcpSchema.ResumeSessionResponse,
-): string | undefined {
- const modelState = sessionSetupResult.models;
- if (!modelState) {
- return undefined;
- }
- const currentModelId = modelState.currentModelId.trim();
- if (currentModelId.length === 0) {
- return undefined;
- }
- const currentModel = modelState.availableModels.find(
- (model) => model.modelId.trim() === currentModelId,
- );
- const reasoningEffort = currentModel?._meta?.reasoningEffort;
- return typeof reasoningEffort === "string"
- ? normalizeGrokReasoningEffort(reasoningEffort)
- : undefined;
-}
-
-export function applyGrokAcpModelSelection(input: {
- readonly runtime: Pick;
- readonly currentModelId: string | undefined;
- readonly currentReasoningEffort?: string | undefined;
- readonly requestedModelId: string | undefined;
- readonly requestedReasoningEffort?: string | undefined;
- readonly mapError: (cause: EffectAcpErrors.AcpError) => E;
-}): Effect.Effect {
- // The product slug is never sent over the wire; it keeps the session's current model.
- const requestedModelId =
- input.requestedModelId === GROK_DEFAULT_MODEL_SLUG ? undefined : input.requestedModelId;
- const modelChanged = requestedModelId !== undefined && requestedModelId !== input.currentModelId;
- const reasoningProvided = input.requestedReasoningEffort !== undefined;
- const reasoningEffort = reasoningProvided
- ? normalizeGrokReasoningEffort(input.requestedReasoningEffort)
- : undefined;
- const reasoningEffortChanged =
- reasoningProvided && reasoningEffort !== input.currentReasoningEffort;
- const targetModelId = requestedModelId ?? input.currentModelId;
- if ((!modelChanged && !reasoningEffortChanged) || targetModelId === undefined) {
- return Effect.succeed(input.currentModelId);
- }
- const reasoningMeta =
- reasoningProvided && reasoningEffort !== undefined ? { reasoningEffort } : undefined;
- // When reasoning was explicitly provided but invalid (normalize => undefined), we deliberately
- // send no meta so the invalid value is dropped rather than forwarded. When reasoning was not
- // provided at all, we also send no meta, but we only reach this call when the model itself
- // changed - an omitted reasoning preference must not be treated as an explicit clear of the
- // CLI-advertised default (e.g. Extra High) on same-model reselections.
- return input.runtime
- .setSessionModel(targetModelId, reasoningMeta)
- .pipe(Effect.mapError(input.mapError), Effect.as(targetModelId));
-}
diff --git a/apps/server/src/provider/acp/XAiAcpExtension.test.ts b/apps/server/src/provider/acp/XAiAcpExtension.test.ts
deleted file mode 100644
index 84e5511785b40de5c52abd1b1d32fdcfadb174eb..0000000000000000000000000000000000000000
--- a/apps/server/src/provider/acp/XAiAcpExtension.test.ts
+++ /dev/null
@@ -1,2039 +0,0 @@
-// @effect-diagnostics nodeBuiltinImport:off -- plan-path tests resolve the OS home dir.
-import * as NodeOS from "node:os";
-import { it } from "@effect/vitest";
-import * as Deferred from "effect/Deferred";
-import * as Effect from "effect/Effect";
-import * as Fiber from "effect/Fiber";
-import * as Schema from "effect/Schema";
-import { describe, expect } from "vite-plus/test";
-
-import {
- XAiExitPlanModeRequest,
- XAI_EMPTY_PLAN_MARKDOWN,
- isGrokPlanMarkdownPath,
- extractXAiExitPlanMarkdown,
- extractGrokPlanMarkdownFromToolCallData,
- extractXAiAcpBackgroundToolMutation,
- extractXAiAcpSubagentEndNotice,
- extractXAiAcpSubagentUpdate,
- extractXAiAskUserQuestions,
- extractXAiBackgroundTaskCompletion,
- extractXAiKilledBackgroundTasks,
- extractXAiMonitorTaskId,
- isGenericAcpToolTitle,
- isXAiMonitorTool,
- isXAiPersistentMonitor,
- makeXAiAskUserQuestionResponse,
- makeXAiPromptCompletionRuntime,
- normalizeXAiAcpToolCallState,
- registerXAiBackgroundTaskTracking,
- registerXAiSubagentFinished,
- resolveXAiAcpToolTitle,
- xAiBackgroundTaskLifecycleMutation,
- xAiPromptCompleteFromSessionUpdate,
- xAiSubagentFinishedNotice,
- XAiAskUserQuestionRequest,
-} from "./XAiAcpExtension.ts";
-import * as AcpSessionRuntime from "@t3tools/provider-acp/server/AcpSessionRuntime";
-import {
- type AcpToolCallState,
- mergeToolCallState,
- parseSessionUpdateEvent,
-} from "@t3tools/provider-acp/server/runtimeModel";
-
-const decodeXAiAskUserQuestionRequest = Schema.decodeUnknownSync(XAiAskUserQuestionRequest);
-
-describe("xAiPromptCompleteFromSessionUpdate", () => {
- it("maps live turn_completed snake_case payloads", () => {
- expect(
- xAiPromptCompleteFromSessionUpdate({
- sessionId: "019f4428-4bf1-7e52-b7c6-c29b506543b1",
- update: {
- sessionUpdate: "turn_completed",
- prompt_id: "t3-xai-prompt-1",
- stop_reason: "end_turn",
- },
- }),
- ).toEqual({
- sessionId: "019f4428-4bf1-7e52-b7c6-c29b506543b1",
- promptId: "t3-xai-prompt-1",
- stopReason: "end_turn",
- });
- });
-
- it("ignores non-turn updates and task-completed prompt ids", () => {
- expect(
- xAiPromptCompleteFromSessionUpdate({
- sessionId: "root",
- update: { sessionUpdate: "hook_execution", prompt_id: "t3-xai-prompt-1" },
- }),
- ).toBeNull();
- expect(
- xAiPromptCompleteFromSessionUpdate({
- sessionId: "root",
- update: {
- sessionUpdate: "turn_completed",
- prompt_id: "task-completed-call-abc",
- stop_reason: "end_turn",
- },
- }),
- ).toBeNull();
- expect(
- xAiPromptCompleteFromSessionUpdate({
- sessionId: "root",
- update: { sessionUpdate: "turn_completed", stop_reason: "end_turn" },
- }),
- ).toBeNull();
- });
-});
-
-// Runs a recorded Grok tool_call_update through the adapter's parse path.
-function parseRecordedToolCallUpdate(update: Record): AcpToolCallState {
- const [event] = parseSessionUpdateEvent({
- sessionId: "01a0d660-c1bb-7842-91a3-02d91dc8b0d2",
- update,
- } as Parameters[0]).events;
- if (event?._tag !== "ToolCallUpdated") throw new Error("expected a tool call update");
- return event.toolCall;
-}
-
-describe("XAiAcpExtension", () => {
- it("recognizes Grok Task starts as native subagents", () => {
- expect(
- extractXAiAcpSubagentUpdate({
- toolCallId: "task-1",
- title: "Task",
- status: "inProgress",
- data: {
- rawInput: {
- description: "Explore server architecture",
- prompt: "Audit apps/server.",
- subagent_type: "generalPurpose",
- model: "composer-2.5-fast",
- },
- },
- }),
- ).toEqual({
- nativeTaskId: "task-1",
- prompt: "Audit apps/server.",
- title: "Explore server architecture",
- model: "composer-2.5-fast",
- status: "running",
- childSessionId: null,
- result: null,
- suppressNormalTool: true,
- });
- });
-
- it("extracts Grok child session lineage from completed Task output", () => {
- expect(
- extractXAiAcpSubagentUpdate({
- toolCallId: "task-1",
- title: "Task",
- status: "completed",
- data: {
- rawInput: {
- description: "Explore server architecture",
- prompt: "Audit apps/server.",
- subagent_type: "generalPurpose",
- },
- rawOutput: {
- type: "Text",
- text: [
- "Server audit complete.",
- "",
- "Agent ID: 019f0220-e192-7c41-9e9d-b406bc3459c8 (resume supported)",
- ].join("\n"),
- },
- },
- }),
- ).toEqual({
- nativeTaskId: "task-1",
- prompt: "Audit apps/server.",
- title: "Explore server architecture",
- model: null,
- status: "completed",
- childSessionId: "019f0220-e192-7c41-9e9d-b406bc3459c8",
- result: "Server audit complete.",
- suppressNormalTool: true,
- });
- });
-
- it("keeps async spawn_subagent ACKs running and binds subagent_id", () => {
- expect(
- extractXAiAcpSubagentUpdate({
- toolCallId: "call-spawn-1",
- title: "spawn_subagent",
- status: "completed",
- data: {
- rawInput: {
- description: "Demo subagent wait and ls",
- prompt: "Wait then ls.",
- subagent_type: "general-purpose",
- },
- rawOutput: {
- type: "Text",
- text: [
- "Subagent started in background.",
- "subagent_id: 019f44a6-4820-7402-925d-bc862ee711dd",
- "type: general-purpose",
- "description: Demo subagent wait and ls",
- "",
- 'Use get_command_or_subagent_output with task_ids=["019f44a6-4820-7402-925d-bc862ee711dd"] and timeout_ms to wait for results.',
- ].join("\n"),
- },
- },
- }),
- ).toEqual({
- nativeTaskId: "call-spawn-1",
- prompt: "Wait then ls.",
- title: "Demo subagent wait and ls",
- model: null,
- status: "running",
- childSessionId: "019f44a6-4820-7402-925d-bc862ee711dd",
- result: null,
- suppressNormalTool: true,
- });
- });
-
- it("hydrates subagent completion from get_command_or_subagent_output", () => {
- expect(
- extractXAiAcpSubagentUpdate({
- toolCallId: "call-get-1",
- title: "get_command_or_subagent_output",
- status: "completed",
- data: {
- rawInput: {
- task_ids: ["019f44a6-4820-7402-925d-bc862ee711dd"],
- timeout_ms: 30000,
- },
- rawOutput: {
- type: "Text",
- text: [
- "=== Task 019f44a6-4820-7402-925d-bc862ee711dd ===",
- "Status: completed",
- "Exit Code: 0",
- "",
- "=== Output ===",
- "SUBAGENT_MARKER: 35 entries",
- ].join("\n"),
- },
- },
- }),
- ).toEqual({
- nativeTaskId: "call-get-1",
- prompt: "",
- title: null,
- model: null,
- status: "completed",
- childSessionId: "019f44a6-4820-7402-925d-bc862ee711dd",
- result: "SUBAGENT_MARKER: 35 entries",
- suppressNormalTool: true,
- });
- });
-
- it("attributes multi-task get_output envelopes to the result task_id", () => {
- expect(
- extractXAiAcpSubagentUpdate({
- toolCallId: "call-get-multi",
- title: "get_command_or_subagent_output",
- status: "completed",
- data: {
- rawInput: {
- task_ids: [
- "019f44a6-4820-7402-925d-bc862ee711dd",
- "019f44b0-1b73-7f32-bb1b-1ff696f536e3",
- ],
- timeout_ms: 30000,
- },
- rawOutput: {
- type: "TaskOutput",
- Result: {
- task_id: "019f44b0-1b73-7f32-bb1b-1ff696f536e3",
- status: "completed",
- exit_code: 0,
- output: "SECOND_SUB_DONE\n",
- },
- },
- },
- }),
- ).toMatchObject({
- childSessionId: "019f44b0-1b73-7f32-bb1b-1ff696f536e3",
- status: "completed",
- result: "SECOND_SUB_DONE",
- });
- });
-
- it("does not treat a subagent_id mentioned in result text as the completed identity", () => {
- const requestedTaskId = "019f44a6-4820-7402-925d-bc862ee711dd";
- const mentionedTaskId = "019f44b0-1b73-7f32-bb1b-1ff696f536e3";
- const toolCall = {
- toolCallId: "call-get-mentioned-id",
- title: "get_command_or_subagent_output",
- status: "completed" as const,
- data: {
- rawInput: {
- variant: "TaskOutput",
- task_ids: [requestedTaskId],
- },
- rawOutput: {
- type: "TaskOutput",
- Result: {
- status: "completed",
- exit_code: 0,
- output: `Audited subagent_id: ${mentionedTaskId}; no issues found.`,
- },
- },
- },
- };
-
- expect(extractXAiAcpSubagentUpdate(toolCall)).toMatchObject({
- childSessionId: requestedTaskId,
- status: "completed",
- });
- expect(extractXAiBackgroundTaskCompletion(toolCall)).toEqual([
- {
- taskId: requestedTaskId,
- status: "completed",
- appendOutput: `Audited subagent_id: ${mentionedTaskId}; no issues found.`,
- },
- ]);
- });
-
- it("hydrates structured ACP TaskOutput tool envelopes", () => {
- expect(
- extractXAiAcpSubagentUpdate({
- toolCallId: "call-get-2",
- title: "[subagent:general-purpose] Sleep then return SUBAGENT_DONE (019f44b0)",
- status: "completed",
- data: {
- rawInput: {
- variant: "TaskOutput",
- task_ids: ["019f44b0-1b73-7f32-bb1b-1ff696f536e3"],
- timeout_ms: 20000,
- },
- rawOutput: {
- type: "TaskOutput",
- Result: {
- task_id: "019f44b0-1b73-7f32-bb1b-1ff696f536e3",
- status: "completed",
- exit_code: 0,
- output:
- "SUBAGENT_DONE\n\nid=019f44b0-1b73-7f32-bb1b-1ff696f536e3\n",
- },
- },
- },
- }),
- ).toEqual({
- nativeTaskId: "call-get-2",
- prompt: "",
- title: null,
- model: null,
- status: "completed",
- childSessionId: "019f44b0-1b73-7f32-bb1b-1ff696f536e3",
- result: "SUBAGENT_DONE",
- suppressNormalTool: true,
- });
- });
-
- it("does not leak raw machine tags when get_output is only meta/result blocks", () => {
- const metaOnlyToolCall = {
- toolCallId: "call-get-meta-only",
- title: "get_command_or_subagent_output",
- status: "completed" as const,
- data: {
- rawInput: {
- variant: "TaskOutput",
- task_ids: ["019f44b0-1b73-7f32-bb1b-1ff696f536e3"],
- timeout_ms: 20000,
- },
- rawOutput: {
- type: "TaskOutput",
- Result: {
- task_id: "019f44b0-1b73-7f32-bb1b-1ff696f536e3",
- status: "completed",
- exit_code: 0,
- output:
- "id=019f44b0-1b73-7f32-bb1b-1ff696f536e3\ndone\n",
- },
- },
- },
- };
- expect(extractXAiAcpSubagentUpdate(metaOnlyToolCall)).toEqual({
- nativeTaskId: "call-get-meta-only",
- prompt: "",
- title: null,
- model: null,
- status: "completed",
- childSessionId: "019f44b0-1b73-7f32-bb1b-1ff696f536e3",
- result: null,
- suppressNormalTool: true,
- });
- expect(extractXAiBackgroundTaskCompletion(metaOnlyToolCall)).toEqual([
- {
- taskId: "019f44b0-1b73-7f32-bb1b-1ff696f536e3",
- status: "completed",
- appendOutput: "",
- },
- ]);
- });
-
- it("keeps monitor start ACKs running and extracts task ids", () => {
- const toolCall = {
- toolCallId: "call-mon-1",
- title: "monitor",
- status: "completed" as const,
- data: {
- rawInput: {
- command: "echo mon_line_1",
- description: "Demo monitor",
- },
- rawOutput: {
- type: "Text",
- text: "Monitor started (task 019f44a5-87d1-7640-8e35-6a4667ffc873, timeout 36000000ms).\nYou will be notified on each event.",
- },
- },
- };
- expect(normalizeXAiAcpToolCallState(toolCall).status).toBe("inProgress");
- expect(extractXAiMonitorTaskId(toolCall)).toBe("019f44a5-87d1-7640-8e35-6a4667ffc873");
- });
-
- it("tracks text Monitor ACKs with generic titles and description-only input", () => {
- const toolCall = {
- toolCallId: "call-mon-generic-text",
- title: "Tool",
- status: "completed" as const,
- data: {
- rawInput: {
- command: "echo mon_line_1",
- description: "Demo monitor without variant",
- },
- rawOutput: {
- type: "Text",
- text: "Monitor started (task 019f44a5-87d1-7640-8e35-6a4667ffc873, timeout 36000000ms).\nYou will be notified on each event.",
- },
- },
- };
-
- const normalized = normalizeXAiAcpToolCallState(toolCall);
- expect(isXAiMonitorTool(normalized)).toBe(true);
- expect(normalized.status).toBe("inProgress");
- expect(extractXAiMonitorTaskId(normalized)).toBe("019f44a5-87d1-7640-8e35-6a4667ffc873");
- });
-
- it("keeps Monitor ticks running while Grok reports them in progress", () => {
- // Recorded from Grok 1.0.41: every tick is an in_progress update whose
- // Bash-shaped rawOutput already carries exit_code 0. The monitor ends only
- // through `_x.ai/task_completed`, never through a completed status.
- const command = "for i in 1 2 3; do sleep 8; echo tick $i; done";
- const started = parseRecordedToolCallUpdate({
- sessionUpdate: "tool_call_update",
- toolCallId: "call-6b025a4e-621c-4526-a579-ba86ada2669c-0",
- kind: "other",
- title: "Start monitor: Watch three ticks eight seconds apart",
- locations: [],
- rawInput: {
- variant: "Monitor",
- command,
- description: "Watch three ticks eight seconds apart",
- timeout_ms: 36000000,
- persistent: false,
- },
- });
- const tick = parseRecordedToolCallUpdate({
- sessionUpdate: "tool_call_update",
- toolCallId: "call-6b025a4e-621c-4526-a579-ba86ada2669c-0",
- status: "in_progress",
- content: [{ type: "content", content: { type: "text", text: "tick 1\n" } }],
- rawOutput: {
- type: "Bash",
- output: [116, 105, 99, 107, 32, 49, 10],
- output_for_prompt: "tick 1\n",
- exit_code: 0,
- command,
- truncated: false,
- signal: null,
- timed_out: false,
- description: null,
- current_dir: "",
- output_file: "",
- total_bytes: 7,
- },
- });
- const toolCall = normalizeXAiAcpToolCallState(mergeToolCallState(started, tick));
- expect(isXAiMonitorTool(toolCall)).toBe(true);
- expect(toolCall.status).toBe("inProgress");
- });
-
- it("finishes monitors from the recorded task_completed snapshot", () => {
- // Fields from the grok_monitor recording (Grok 1.0.41).
- const completed = {
- sessionId: "01a0d660-c1bb-7842-91a3-02d91dc8b0d2",
- update: {
- sessionUpdate: "task_completed",
- task_snapshot: {
- task_id: "01a0d660-f98d-7ef3-a291-97af1c2b6455",
- command: "for i in 1 2 3; do sleep 8; echo tick $i; done",
- display_command: "[monitor] Watch three tick echoes",
- output: "tick 1\ntick 2\ntick 3\n",
- exit_code: 0,
- kind: "monitor",
- description: "Watch three tick echoes",
- },
- },
- };
- expect(xAiBackgroundTaskLifecycleMutation(completed, "completed")).toEqual({
- sessionId: "01a0d660-c1bb-7842-91a3-02d91dc8b0d2",
- taskId: "01a0d660-f98d-7ef3-a291-97af1c2b6455",
- status: "completed",
- output: "tick 1\ntick 2\ntick 3\n",
- report: { kind: "monitor", label: "Watch three tick echoes" },
- });
- expect(
- xAiBackgroundTaskLifecycleMutation(
- {
- ...completed,
- update: { ...completed.update, task_snapshot: { task_id: "t", exit_code: 2 } },
- },
- "completed",
- )?.status,
- ).toBe("failed");
- });
-
- it("detects Monitor start ACKs from structured rawOutput when title is generic", () => {
- const toolCall = {
- toolCallId: "call-mon-generic-title",
- title: "Tool",
- status: "completed" as const,
- data: {
- rawInput: {
- command: "echo mon_line",
- description: "Demo monitor without variant",
- },
- rawOutput: {
- type: "Monitor",
- taskId: "019f44a5-87d1-7640-8e35-6a4667ffc873",
- timeoutMs: 36000000,
- persistent: false,
- },
- },
- };
- expect(isXAiMonitorTool(toolCall)).toBe(true);
- expect(normalizeXAiAcpToolCallState(toolCall).status).toBe("inProgress");
- expect(extractXAiMonitorTaskId(toolCall)).toBe("019f44a5-87d1-7640-8e35-6a4667ffc873");
- });
-
- it("completes status-less Bash results despite generic titles", () => {
- // A Bash result that carries no status (empty rawInput, generic title) is
- // terminalized from its exit_code. An explicit in_progress status wins:
- // that is how Grok streams a still-running monitor.
- const toolCall = {
- toolCallId: "call-wake-1",
- title: "Tool",
- data: {
- rawInput: {},
- rawOutput: {
- type: "Bash",
- output: Array.from(new TextEncoder().encode("STREAM_1\nSTREAM_DONE\n")),
- output_for_prompt: "STREAM_1\nSTREAM_DONE\n",
- exit_code: 0,
- command: "for i in 1 2; do echo STREAM_$i; done; echo STREAM_DONE",
- },
- },
- };
- expect(isXAiMonitorTool(toolCall)).toBe(false);
- const normalized = normalizeXAiAcpToolCallState(toolCall);
- expect(normalized.status).toBe("completed");
- expect(normalized.title).toBe("for i in 1 2; do echo STREAM_$i; done; echo STREAM_DONE");
- expect(
- normalizeXAiAcpToolCallState({
- ...toolCall,
- data: {
- ...toolCall.data,
- rawOutput: { ...toolCall.data.rawOutput, exit_code: 2 },
- },
- }).status,
- ).toBe("failed");
- });
-
- it("keeps empty successful Bash updates running until native output or turn settlement", () => {
- const normalized = normalizeXAiAcpToolCallState({
- toolCallId: "call-bash-permission-ack",
- title: "Ran command",
- status: "completed",
- data: {
- rawInput: { command: "sleep 40" },
- rawOutput: { type: "Bash", exit_code: 0 },
- },
- });
- expect(normalized.status).toBe("inProgress");
- });
-
- it("keeps structured Monitor start ACKs running and extracts taskId", () => {
- const toolCall = {
- toolCallId: "call-mon-3",
- title: "Start monitor: Wait 30s then list directory",
- status: "completed" as const,
- data: {
- rawInput: {
- variant: "Monitor",
- command: "sleep 30 && ls",
- description: "Wait 30s then list directory",
- },
- rawOutput: {
- type: "Monitor",
- taskId: "019f44b8-8e98-7c80-a40e-df1e26a5f9e3",
- timeoutMs: 36000000,
- persistent: false,
- },
- },
- };
- expect(normalizeXAiAcpToolCallState(toolCall).status).toBe("inProgress");
- expect(extractXAiMonitorTaskId(toolCall)).toBe("019f44b8-8e98-7c80-a40e-df1e26a5f9e3");
- });
-
- it("replaces generic ACP titles with description / Monitor labels", () => {
- expect(isGenericAcpToolTitle("Tool")).toBe(true);
- expect(isGenericAcpToolTitle("Read package.json")).toBe(false);
- expect(
- resolveXAiAcpToolTitle({
- toolCallId: "call-mon-title",
- title: "Tool",
- status: "completed",
- data: {
- rawInput: {
- variant: "Monitor",
- command: "sleep 30 && ls",
- description: "Wait 30s then list directory",
- },
- },
- }),
- ).toBe("Monitor: Wait 30s then list directory");
- const normalized = normalizeXAiAcpToolCallState({
- toolCallId: "call-mon-title",
- title: "Tool",
- status: "completed",
- data: {
- rawInput: {
- variant: "Monitor",
- command: "sleep 30 && ls",
- description: "Wait 30s then list directory",
- },
- rawOutput: {
- type: "Monitor",
- taskId: "019f44b8-8e98-7c80-a40e-df1e26a5f9e3",
- timeoutMs: 36000000,
- persistent: false,
- },
- },
- });
- expect(normalized.title).toBe("Monitor: Wait 30s then list directory");
- expect(normalized.status).toBe("inProgress");
- // Non-generic titles from the CLI are preserved.
- expect(
- resolveXAiAcpToolTitle({
- toolCallId: "call-read",
- title: "Read package.json",
- status: "inProgress",
- data: { rawInput: { path: "package.json" } },
- }),
- ).toBe("Read package.json");
- });
-
- it("hydrates monitor completion from TaskOutput get_command envelopes", () => {
- expect(
- extractXAiBackgroundTaskCompletion({
- toolCallId: "call-get-mon",
- title: "Wait 30s then list directory",
- status: "completed",
- data: {
- rawInput: {
- variant: "TaskOutput",
- task_ids: ["019f44b8-8e98-7c80-a40e-df1e26a5f9e3"],
- },
- rawOutput: {
- type: "TaskOutput",
- Result: {
- task_id: "019f44b8-8e98-7c80-a40e-df1e26a5f9e3",
- command: "[monitor] Wait 30s then list directory",
- status: "completed",
- exit_code: 0,
- output: "agents\nAGENTS.md\nnotes\n",
- },
- },
- },
- }),
- ).toEqual([
- {
- taskId: "019f44b8-8e98-7c80-a40e-df1e26a5f9e3",
- status: "completed",
- appendOutput: "agents\nAGENTS.md\nnotes",
- },
- ]);
- });
-
- it("hydrates monitor completion from a standalone TaskOutput frame without rawInput", () => {
- // Post-settle shape observed live (2026-07-12): the final
- // get_command_or_subagent_output update carries only rawOutput; the frame
- // is parsed in isolation (no in-turn merge), and this completion is the
- // ONLY end signal when the agent consumed the output itself (no "Monitor
- // ended" reminder follows).
- expect(
- extractXAiBackgroundTaskCompletion({
- toolCallId: "call-c29e64dd-ce5d-4eac-a6ca-c20513fefac3-1",
- title: "[monitor] Stream numbered markers every 3s (019f54a0)",
- status: "completed",
- data: {
- rawOutput: {
- type: "TaskOutput",
- Result: {
- task_id: "019f54a0-06a8-77f2-8214-e24937cad564",
- command: "[monitor] Stream numbered markers every 3s",
- status: "completed",
- exit_code: 0,
- output: "STREAM_1\nSTREAM_DONE\n",
- },
- },
- },
- }),
- ).toMatchObject([
- {
- taskId: "019f54a0-06a8-77f2-8214-e24937cad564",
- status: "completed",
- },
- ]);
- });
-
- it("hydrates every requested monitor when TaskOutput has no result task_id", () => {
- expect(
- extractXAiBackgroundTaskCompletion({
- toolCallId: "call-get-multi-mon",
- title: "get_command_or_subagent_output",
- status: "completed",
- data: {
- rawInput: {
- variant: "TaskOutput",
- task_ids: [
- "019f44a6-4820-7402-925d-bc862ee711dd",
- "019f44b0-1b73-7f32-bb1b-1ff696f536e3",
- ],
- },
- rawOutput: {
- type: "TaskOutput",
- Result: {
- status: "completed",
- exit_code: 0,
- output: "BOTH_DONE\n",
- },
- },
- },
- }),
- ).toEqual([
- {
- taskId: "019f44a6-4820-7402-925d-bc862ee711dd",
- status: "completed",
- appendOutput: "BOTH_DONE",
- },
- {
- taskId: "019f44b0-1b73-7f32-bb1b-1ff696f536e3",
- status: "completed",
- appendOutput: "BOTH_DONE",
- },
- ]);
- });
-
- it("keeps every requested monitor running when a completed poll has no terminal task status", () => {
- expect(
- extractXAiBackgroundTaskCompletion({
- toolCallId: "call-get-running-monitors",
- title: "get_command_or_subagent_output",
- status: "completed",
- data: {
- rawInput: {
- variant: "TaskOutput",
- task_ids: [
- "019f44a6-4820-7402-925d-bc862ee711dd",
- "019f44b0-1b73-7f32-bb1b-1ff696f536e3",
- ],
- },
- rawOutput: {
- type: "TaskOutput",
- Result: {
- output: "No task has reached a terminal state.",
- },
- },
- },
- }),
- ).toEqual([
- {
- taskId: "019f44a6-4820-7402-925d-bc862ee711dd",
- status: "running",
- appendOutput: "No task has reached a terminal state.",
- },
- {
- taskId: "019f44b0-1b73-7f32-bb1b-1ff696f536e3",
- status: "running",
- appendOutput: "No task has reached a terminal state.",
- },
- ]);
- });
-
- it("treats Exit Code: -1 text envelopes as failed", () => {
- expect(
- extractXAiBackgroundTaskCompletion({
- toolCallId: "call-get-neg",
- title: "get_command_or_subagent_output",
- status: "completed",
- data: {
- rawInput: {
- variant: "TaskOutput",
- task_ids: ["019f44b8-8e98-7c80-a40e-df1e26a5f9e3"],
- },
- rawOutput: {
- type: "Text",
- text: ["=== Task 019f44b8-8e98-7c80-a40e-df1e26a5f9e3 ===", "Exit Code: -1"].join("\n"),
- },
- },
- }),
- ).toMatchObject([
- {
- taskId: "019f44b8-8e98-7c80-a40e-df1e26a5f9e3",
- status: "failed",
- },
- ]);
- });
-
- it("tombstones confirmed kills and ignores failed kill calls", () => {
- expect(
- extractXAiKilledBackgroundTasks({
- toolCallId: "call-kill-1",
- title: "kill_command_or_subagent",
- status: "completed",
- data: {
- rawInput: {
- variant: "Kill",
- task_ids: ["call-bg-1", "call-bg-2", "call-bg-1"],
- },
- },
- }),
- ).toEqual([
- { taskId: "call-bg-1", status: "completed", appendOutput: "" },
- { taskId: "call-bg-2", status: "completed", appendOutput: "" },
- ]);
- // Singular task_id shape, matched by variant when the title is generic.
- expect(
- extractXAiKilledBackgroundTasks({
- toolCallId: "call-kill-2",
- title: "Tool",
- status: "failed",
- data: { rawInput: { variant: "kill", task_id: "call-bg-3" } },
- }),
- ).toEqual([]);
- // In-flight kill calls and unrelated tools contribute nothing.
- expect(
- extractXAiKilledBackgroundTasks({
- toolCallId: "call-kill-3",
- title: "kill_command_or_subagent",
- status: "inProgress",
- data: { rawInput: { task_ids: ["call-bg-4"] } },
- }),
- ).toEqual([]);
- expect(
- extractXAiKilledBackgroundTasks({
- toolCallId: "call-get",
- title: "get_command_or_subagent_output",
- status: "completed",
- data: { rawInput: { variant: "TaskOutput", task_ids: ["call-bg-5"] } },
- }),
- ).toEqual([]);
- });
-
- it("maps task lifecycle notifications to background mutations", () => {
- // task_backgrounded carries the cancelled call's tool_call_id as task_id.
- expect(
- xAiBackgroundTaskLifecycleMutation(
- {
- sessionId: "session-1",
- update: {
- sessionUpdate: "task_backgrounded",
- tool_call_id: "call-bg-1",
- task_id: "call-bg-1",
- },
- },
- "running",
- ),
- ).toEqual({ sessionId: "session-1", taskId: "call-bg-1", status: "running" });
- // task_completed nests the id inside task_snapshot.
- expect(
- xAiBackgroundTaskLifecycleMutation(
- {
- sessionId: "session-1",
- update: {
- sessionUpdate: "task_completed",
- task_snapshot: { task_id: "call-bg-1" },
- },
- },
- "completed",
- ),
- ).toEqual({ sessionId: "session-1", taskId: "call-bg-1", status: "completed" });
- expect(
- xAiBackgroundTaskLifecycleMutation(
- { sessionId: "session-1", update: { sessionUpdate: "task_completed" } },
- "completed",
- ),
- ).toBeNull();
- });
-
- it.effect("tracks canonical Grok task lifecycle notifications", () =>
- Effect.gen(function* () {
- const handlers = new Map Effect.Effect>();
- const mutations: Array<{
- readonly sessionId: string;
- readonly taskId: string;
- readonly status: "running" | "completed" | "failed";
- }> = [];
- const runtime = {
- handleExtNotification: (
- method: string,
- _schema: unknown,
- handler: (notification: unknown) => Effect.Effect,
- ) => {
- handlers.set(method, handler);
- return Effect.void;
- },
- } as unknown as Pick;
-
- yield* registerXAiBackgroundTaskTracking(runtime, (mutation) =>
- Effect.sync(() => mutations.push(mutation)),
- );
-
- const backgrounded = handlers.get("x.ai/task_backgrounded");
- const completed = handlers.get("x.ai/task_completed");
- expect(backgrounded).toBeDefined();
- expect(completed).toBeDefined();
- yield* backgrounded!({
- sessionId: "session-1",
- update: {
- sessionUpdate: "task_backgrounded",
- tool_call_id: "call-bg-1",
- task_id: "task-bg-1",
- },
- });
- yield* completed!({
- sessionId: "session-1",
- update: {
- sessionUpdate: "task_completed",
- task_snapshot: { task_id: "task-bg-1" },
- },
- });
- expect(mutations).toEqual([
- { sessionId: "session-1", taskId: "task-bg-1", status: "running" },
- { sessionId: "session-1", taskId: "task-bg-1", status: "completed" },
- ]);
- expect(handlers.has("_x.ai/task_backgrounded")).toBe(true);
- expect(handlers.has("_x.ai/task_completed")).toBe(true);
- }),
- );
-
- it("detects persistent Monitor start ACKs", () => {
- expect(
- isXAiPersistentMonitor({
- toolCallId: "call-mon-persist",
- title: "Monitor",
- status: "completed",
- data: {
- rawInput: { variant: "Monitor" },
- rawOutput: {
- type: "Monitor",
- taskId: "019f44b8-8e98-7c80-a40e-df1e26a5f9e3",
- timeoutMs: 36000000,
- persistent: true,
- },
- },
- }),
- ).toBe(true);
- expect(
- isXAiPersistentMonitor({
- toolCallId: "call-mon-ephemeral",
- title: "Monitor",
- status: "completed",
- data: {
- rawInput: { variant: "Monitor" },
- rawOutput: {
- type: "Monitor",
- taskId: "019f44b8-8e98-7c80-a40e-df1e26a5f9e3",
- timeoutMs: 36000000,
- persistent: false,
- },
- },
- }),
- ).toBe(false);
- });
-
- it("keeps a standalone TaskOutput completion terminal through normalize", () => {
- // Must remain offer evidence post-settle: normalize must not demote it to
- // inProgress the way monitor start ACKs are.
- expect(
- normalizeXAiAcpToolCallState({
- toolCallId: "call-c29e64dd-ce5d-4eac-a6ca-c20513fefac3-1",
- title: "[monitor] Stream numbered markers every 3s (019f54a0)",
- status: "completed",
- data: {
- rawOutput: {
- type: "TaskOutput",
- Result: {
- task_id: "019f54a0-06a8-77f2-8214-e24937cad564",
- status: "completed",
- exit_code: 0,
- output: "STREAM_DONE\n",
- },
- },
- },
- }).status,
- ).toBe("completed");
- });
-
- it("parses monitor event lines and end reminders", () => {
- expect(
- extractXAiAcpBackgroundToolMutation(
- '\n[Demo] mon_line_1\n',
- ),
- ).toEqual([
- {
- taskId: "019f44a5-87d1-7640-8e35-6a4667ffc873",
- status: "running",
- appendOutput: "[Demo] mon_line_1\n",
- },
- ]);
- expect(
- extractXAiAcpBackgroundToolMutation(
- [
- "",
- 'Monitor "019f44a5-87d1-7640-8e35-6a4667ffc873" ended: [monitor ended: exited (code 0)].',
- "Description: Demo monitor",
- "",
- ].join("\n"),
- ),
- ).toMatchObject([
- {
- taskId: "019f44a5-87d1-7640-8e35-6a4667ffc873",
- status: "completed",
- },
- ]);
- // Description/output may contain "error" without meaning the monitor failed.
- expect(
- extractXAiAcpBackgroundToolMutation(
- [
- "",
- 'Monitor "019f44a5-87d1-7640-8e35-6a4667ffc873" ended: [monitor ended: exited (code 0)].',
- "Description: watch error logs for failed deploys",
- "error: nothing found",
- "",
- ].join("\n"),
- ),
- ).toMatchObject([
- {
- taskId: "019f44a5-87d1-7640-8e35-6a4667ffc873",
- status: "completed",
- },
- ]);
- });
-
- it("parses every monitor-event and trailing end notice in one chunk", () => {
- const mutations = extractXAiAcpBackgroundToolMutation(
- [
- '',
- "[Demo] mon_line_1",
- "",
- '',
- "[Demo] mon_line_2",
- "",
- 'Monitor "019f44a5-87d1-7640-8e35-6a4667ffc873" ended: [monitor ended: exited (code 0)].',
- ].join("\n"),
- );
- expect(mutations).toHaveLength(3);
- expect(mutations[0]).toEqual({
- taskId: "019f44a5-87d1-7640-8e35-6a4667ffc873",
- status: "running",
- appendOutput: "[Demo] mon_line_1\n",
- });
- expect(mutations[1]).toEqual({
- taskId: "019f44a5-87d1-7640-8e35-6a4667ffc873",
- status: "running",
- appendOutput: "[Demo] mon_line_2\n",
- });
- expect(mutations[2]).toMatchObject({
- taskId: "019f44a5-87d1-7640-8e35-6a4667ffc873",
- status: "completed",
- });
- });
-
- it("parses batched monitor event blocks as running mutations", () => {
- expect(
- extractXAiAcpBackgroundToolMutation(
- [
- "3 monitor events from 1 monitor (use get_command_or_subagent_output to identify each monitor):",
- "",
- '',
- "[1] STREAM_7",
- "[2] STREAM_8",
- "[3] STREAM_9",
- "",
- ].join("\n"),
- ),
- ).toEqual([
- {
- taskId: "019f545d-4d39-7001-b1c8-c7744c448ec1",
- status: "running",
- appendOutput: "[1] STREAM_7\n[2] STREAM_8\n[3] STREAM_9\n",
- },
- ]);
- });
-
- it("parses the subagent completed reminder as an end notice", () => {
- expect(
- extractXAiAcpSubagentEndNotice(
- [
- "",
- 'Background subagent "019f5470-bf92-7a90-afb3-5a6cea5b34a3" (general-purpose: "Run sleep then echo token") completed successfully.',
- "Duration: 28.8s | Tool calls: 1 | Turns: 1",
- 'Use get_task_output("019f5470-bf92-7a90-afb3-5a6cea5b34a3") to see the full output.',
- "",
- ].join("\n"),
- ),
- ).toEqual({
- childSessionId: "019f5470-bf92-7a90-afb3-5a6cea5b34a3",
- status: "completed",
- });
- });
-
- it("parses a failed subagent reminder as a failed end notice", () => {
- expect(
- extractXAiAcpSubagentEndNotice(
- 'Background subagent "019f5470-bf92-7a90-afb3-5a6cea5b34a3" (general-purpose: "Run sleep then echo token") failed.',
- ),
- ).toEqual({
- childSessionId: "019f5470-bf92-7a90-afb3-5a6cea5b34a3",
- status: "failed",
- });
- });
-
- it("ignores prompt text that merely mentions subagents", () => {
- expect(
- extractXAiAcpSubagentEndNotice("Live-test post-settle subagent completion."),
- ).toBeUndefined();
- expect(
- extractXAiAcpSubagentEndNotice(
- 'Background subagent "019f5470-bf92-7a90-afb3-5a6cea5b34a3" (general-purpose: "Run sleep then echo token") is still running.',
- ),
- ).toBeUndefined();
- });
-
- it("does not treat nested parentheses in the title as the outcome verb", () => {
- expect(
- extractXAiAcpSubagentEndNotice(
- 'Background subagent "019f5470-bf92-7a90-afb3-5a6cea5b34a3" (general-purpose: "Check (backend) failed tests") is still running.',
- ),
- ).toBeUndefined();
- expect(
- extractXAiAcpSubagentEndNotice(
- 'Background subagent "019f5470-bf92-7a90-afb3-5a6cea5b34a3" (general-purpose: "Check (backend) failed tests") completed successfully.',
- ),
- ).toEqual({
- childSessionId: "019f5470-bf92-7a90-afb3-5a6cea5b34a3",
- status: "completed",
- });
- });
-
- it("extracts questions from the real xAI ask_user_question payload shape", () => {
- const questions = extractXAiAskUserQuestions({
- sessionId: "session-1",
- toolCallId: "tool-call-1",
- mode: "default",
- questions: [
- {
- id: "scope",
- question: "Which scope should Grok use?",
- options: [
- { label: "Workspace", description: "Use the current workspace" },
- { label: "Session", description: "Only use this session" },
- ],
- },
- ],
- });
-
- expect(questions).toEqual([
- {
- id: "scope",
- header: "Question",
- question: "Which scope should Grok use?",
- multiSelect: false,
- options: [
- { label: "Workspace", description: "Use the current workspace" },
- { label: "Session", description: "Only use this session" },
- ],
- },
- ]);
- });
-
- it("extracts questions from wrapped _x.ai extension payloads", () => {
- const payload = {
- method: "_x.ai/ask_user_question",
- params: {
- sessionId: "session-1",
- toolCallId: "tool-call-1",
- mode: "plan",
- questions: [
- {
- question: "Which changes should be included?",
- multiSelect: true,
- options: [{ label: "Tests" }, { label: "Docs" }],
- },
- ],
- },
- };
- const decoded = decodeXAiAskUserQuestionRequest(payload);
- const questions = extractXAiAskUserQuestions(decoded);
-
- expect(questions).toEqual([
- {
- id: "Which changes should be included?",
- header: "Question",
- question: "Which changes should be included?",
- multiSelect: true,
- options: [
- { label: "Tests", description: "Tests" },
- { label: "Docs", description: "Docs" },
- ],
- },
- ]);
- });
-
- it("treats nullable multiSelect from Grok as single-select", () => {
- const questions = extractXAiAskUserQuestions({
- sessionId: "session-1",
- toolCallId: "tool-call-1",
- mode: "default",
- questions: [
- {
- question: "Which label should Grok use?",
- multiSelect: null,
- options: [
- { label: "Alpha", description: "Use the Alpha label" },
- { label: "Beta", description: "Use the Beta label" },
- { label: "Other", description: "Use the Other label" },
- ],
- },
- ],
- });
-
- expect(questions).toEqual([
- {
- id: "Which label should Grok use?",
- header: "Question",
- question: "Which label should Grok use?",
- multiSelect: false,
- options: [
- { label: "Alpha", description: "Use the Alpha label" },
- { label: "Beta", description: "Use the Beta label" },
- { label: "Other", description: "Use the Other label" },
- ],
- },
- ]);
- });
-
- it("maps UI question ids back to xAI question text in accepted responses", () => {
- const response = makeXAiAskUserQuestionResponse(
- {
- sessionId: "session-1",
- toolCallId: "tool-call-1",
- mode: "default",
- questions: [
- {
- id: "scope",
- question: "Which scope should Grok use?",
- options: [
- { label: "workspace", description: "Use the current workspace" },
- { label: "session", description: "Only use this session" },
- ],
- },
- ],
- },
- { scope: "workspace" },
- );
-
- expect(response).toEqual({
- outcome: "accepted",
- answers: {
- "Which scope should Grok use?": ["workspace"],
- },
- });
- });
-
- it("orders accepted answers by the original xAI question order", () => {
- const response = makeXAiAskUserQuestionResponse(
- {
- sessionId: "session-1",
- toolCallId: "tool-call-1",
- mode: "default",
- questions: [
- {
- id: "first",
- question: "First question?",
- options: [{ label: "A", description: "A" }],
- },
- {
- id: "second",
- question: "Second question?",
- options: [{ label: "B", description: "B" }],
- },
- ],
- },
- {
- second: "B",
- first: "A",
- },
- );
-
- expect(Object.keys(response.answers)).toEqual(["First question?", "Second question?"]);
- expect(response).toMatchObject({
- outcome: "accepted",
- answers: {
- "First question?": ["A"],
- "Second question?": ["B"],
- },
- });
- });
-
- it("encodes typed custom answers as xAI Other annotations", () => {
- const response = makeXAiAskUserQuestionResponse(
- {
- method: "x.ai/ask_user_question",
- params: {
- sessionId: "session-1",
- toolCallId: "tool-call-1",
- mode: "default",
- questions: [
- {
- question: "Which ice cream flavor?",
- options: [
- { label: "vanilla", description: "Vanilla flavor" },
- { label: "chocolate", description: "Chocolate flavor" },
- ],
- },
- ],
- },
- },
- { "Which ice cream flavor?": "pistachio" },
- );
-
- expect(response).toEqual({
- outcome: "accepted",
- answers: {
- "Which ice cream flavor?": ["Other"],
- },
- annotations: {
- "Which ice cream flavor?": {
- notes: "pistachio",
- },
- },
- });
- });
-
- it("does not echo preview annotations for multi-select answers", () => {
- const response = makeXAiAskUserQuestionResponse(
- {
- sessionId: "session-1",
- toolCallId: "tool-call-1",
- mode: "default",
- questions: [
- {
- question: "Which files should Grok touch?",
- multiSelect: true,
- options: [
- {
- label: "Tests",
- description: "Update tests",
- preview: "test preview",
- },
- {
- label: "Docs",
- description: "Update docs",
- preview: "docs preview",
- },
- ],
- },
- ],
- },
- { "Which files should Grok touch?": ["Tests", "Docs"] },
- );
-
- expect(response).toEqual({
- outcome: "accepted",
- answers: {
- "Which files should Grok touch?": ["Tests", "Docs"],
- },
- });
- });
-
- it.effect("settles a hung prompt from a root-session prompt_complete notification", () =>
- Effect.gen(function* () {
- const handlers = new Map Effect.Effect>();
- const hungPrompt = yield* Deferred.make();
- const baseRuntime = {
- start: () =>
- Effect.succeed({
- sessionId: "root-session",
- initializeResult: {},
- sessionSetupResult: {},
- modelConfigId: undefined,
- }),
- prompt: () => Deferred.await(hungPrompt),
- cancel: Effect.void,
- handleExtNotification: (
- method: string,
- _schema: unknown,
- handler: (notification: unknown) => Effect.Effect,
- ) => {
- handlers.set(method, handler);
- return Effect.void;
- },
- handleExtRequest: () => Effect.void,
- } as unknown as AcpSessionRuntime.AcpSessionRuntime["Service"];
-
- const runtime = yield* makeXAiPromptCompletionRuntime(baseRuntime);
- const promptFiber = yield* runtime
- .prompt({ prompt: [{ type: "text", text: "hi" }] })
- .pipe(Effect.forkChild);
- yield* Effect.yieldNow;
- const promptCompleteHandler = handlers.get("x.ai/session/prompt_complete");
- expect(promptCompleteHandler).toBeDefined();
- yield* promptCompleteHandler!({
- sessionId: "root-session",
- stopReason: "end_turn",
- });
- const response = yield* Fiber.join(promptFiber);
- expect(response.stopReason).toBe("end_turn");
- expect(handlers.has("_x.ai/session/prompt_complete")).toBe(true);
- }),
- );
-
- it.effect("fails a hung prompt from an xAI rate-limit completion (#8358)", () =>
- Effect.gen(function* () {
- const handlers = new Map Effect.Effect>();
- const hungPrompt = yield* Deferred.make();
- const baseRuntime = {
- start: () =>
- Effect.succeed({
- sessionId: "root-session",
- initializeResult: {},
- sessionSetupResult: {},
- modelConfigId: undefined,
- }),
- prompt: () => Deferred.await(hungPrompt),
- cancel: Effect.void,
- handleExtNotification: (
- method: string,
- _schema: unknown,
- handler: (notification: unknown) => Effect.Effect,
- ) => {
- handlers.set(method, handler);
- return Effect.void;
- },
- handleExtRequest: () => Effect.void,
- } as unknown as AcpSessionRuntime.AcpSessionRuntime["Service"];
-
- const runtime = yield* makeXAiPromptCompletionRuntime(baseRuntime);
- const promptFiber = yield* runtime
- .prompt({ prompt: [{ type: "text", text: "hi" }] })
- .pipe(Effect.forkChild);
- yield* Effect.yieldNow;
-
- const promptCompleteHandler = handlers.get("x.ai/session/prompt_complete");
- expect(promptCompleteHandler).toBeDefined();
- yield* promptCompleteHandler!({
- sessionId: "root-session",
- stopReason: "rate_limit",
- });
- const error = yield* Fiber.join(promptFiber).pipe(Effect.flip);
- expect(error).toMatchObject({
- _tag: "AcpRequestError",
- code: -32003,
- errorMessage: "Grok usage limit reached. Try again later.",
- });
- }),
- );
-
- it("maps subagent_finished to a notice only for statuses Grok defines", () => {
- // Recorded from Grok 1.0.41 (ids normalized); statuses per grok-build
- // xai-grok-tools/.../task/types.rs `status()`.
- const finished = (update: Record) =>
- xAiSubagentFinishedNotice({
- sessionId: "root-session",
- update: {
- sessionUpdate: "subagent_finished",
- subagent_id: "child-session",
- child_session_id: "child-session",
- tool_calls: 2,
- turns: 1,
- duration_ms: 23734,
- ...update,
- } as never,
- });
- expect(finished({ status: "completed", output: "SUBAGENT_DONE", will_wake: true })).toEqual({
- sessionId: "root-session",
- childSessionId: "child-session",
- status: "completed",
- result: "SUBAGENT_DONE",
- });
- expect(finished({ status: "failed", error: "tool crashed" })).toMatchObject({
- status: "failed",
- result: "tool crashed",
- });
- expect(
- finished({ status: "cancelled", error: "interrupted by process restart" }),
- ).toMatchObject({ status: "cancelled", result: "interrupted by process restart" });
- // A failed subagent's error is its result; `output` only accompanies success.
- expect(finished({ status: "failed", output: "partial" })).toMatchObject({ result: null });
- expect(finished({ status: "timed_out" })).toBeNull();
- expect(finished({})).toBeNull();
- expect(finished({ status: "completed", child_session_id: undefined })).toBeNull();
- });
-
- it.effect("settles prompts and finishes subagents from the same session notifications", () =>
- Effect.gen(function* () {
- const handlers = new Map Effect.Effect>();
- let capturedMeta: Record | null | undefined;
- const hungPrompt = yield* Deferred.make();
- const baseRuntime = {
- start: () =>
- Effect.succeed({
- sessionId: "root-session",
- initializeResult: {},
- sessionSetupResult: {},
- modelConfigId: undefined,
- }),
- prompt: (payload: { readonly _meta?: Record | null }) => {
- capturedMeta = payload._meta ?? null;
- return Deferred.await(hungPrompt);
- },
- cancel: Effect.void,
- handleExtNotification: (
- method: string,
- _schema: unknown,
- handler: (notification: unknown) => Effect.Effect,
- ) => {
- handlers.set(method, handler);
- return Effect.void;
- },
- } as unknown as AcpSessionRuntime.AcpSessionRuntime["Service"];
- const runtime = yield* makeXAiPromptCompletionRuntime(baseRuntime);
- const notices: Array = [];
- // Registered after the wrapper, as the adapter does: it must not replace
- // the wrapper's prompt completion on the shared method.
- yield* registerXAiSubagentFinished(runtime, (notice) =>
- Effect.sync(() => notices.push(notice)),
- );
- const sessionNotification = handlers.get("_x.ai/session_notification")!;
- yield* sessionNotification({
- sessionId: "root-session",
- update: {
- sessionUpdate: "subagent_finished",
- child_session_id: "child-session",
- status: "completed",
- output: "SUBAGENT_DONE",
- },
- });
- expect(notices).toEqual([
- {
- sessionId: "root-session",
- childSessionId: "child-session",
- status: "completed",
- result: "SUBAGENT_DONE",
- },
- ]);
-
- const promptFiber = yield* runtime
- .prompt({ prompt: [{ type: "text", text: "hi" }] })
- .pipe(Effect.forkChild);
- yield* Effect.yieldNow;
- yield* sessionNotification({
- sessionId: "root-session",
- update: {
- sessionUpdate: "turn_completed",
- prompt_id: capturedMeta?.promptId,
- stop_reason: "end_turn",
- },
- });
- const response = yield* Fiber.join(promptFiber);
- expect(response.stopReason).toBe("end_turn");
- expect(notices).toHaveLength(1);
- }),
- );
-
- it.effect("settles a hung prompt from _x.ai/session/update turn_completed", () =>
- Effect.gen(function* () {
- const handlers = new Map Effect.Effect>();
- let capturedMeta: Record | null | undefined;
- const hungPrompt = yield* Deferred.make();
- const baseRuntime = {
- start: () =>
- Effect.succeed({
- sessionId: "root-session",
- initializeResult: {},
- sessionSetupResult: {},
- modelConfigId: undefined,
- }),
- prompt: (payload: { readonly _meta?: Record | null }) => {
- capturedMeta = payload._meta ?? null;
- return Deferred.await(hungPrompt);
- },
- cancel: Effect.void,
- handleExtNotification: (
- method: string,
- _schema: unknown,
- handler: (notification: unknown) => Effect.Effect,
- ) => {
- handlers.set(method, handler);
- return Effect.void;
- },
- handleExtRequest: () => Effect.void,
- } as unknown as AcpSessionRuntime.AcpSessionRuntime["Service"];
-
- const runtime = yield* makeXAiPromptCompletionRuntime(baseRuntime);
- const promptFiber = yield* runtime
- .prompt({ prompt: [{ type: "text", text: "hi" }] })
- .pipe(Effect.forkChild);
- yield* Effect.yieldNow;
- const promptId = capturedMeta?.promptId;
- expect(typeof promptId).toBe("string");
- const sessionUpdateHandler = handlers.get("_x.ai/session/update");
- expect(sessionUpdateHandler).toBeDefined();
- yield* sessionUpdateHandler!({
- sessionId: "root-session",
- update: {
- sessionUpdate: "turn_completed",
- prompt_id: promptId,
- stop_reason: "end_turn",
- },
- });
- const response = yield* Fiber.join(promptFiber);
- expect(response.stopReason).toBe("end_turn");
- }),
- );
-
- it.effect("settles a hung prompt from released _x.ai/session_notification turns", () =>
- Effect.gen(function* () {
- const handlers = new Map Effect.Effect>();
- const hungPrompt = yield* Deferred.make();
- const baseRuntime = {
- start: () =>
- Effect.succeed({
- sessionId: "root-session",
- initializeResult: {},
- sessionSetupResult: {},
- modelConfigId: undefined,
- }),
- prompt: () => Deferred.await(hungPrompt),
- cancel: Effect.void,
- handleExtNotification: (
- method: string,
- _schema: unknown,
- handler: (notification: unknown) => Effect.Effect,
- ) => {
- handlers.set(method, handler);
- return Effect.void;
- },
- handleExtRequest: () => Effect.void,
- } as unknown as AcpSessionRuntime.AcpSessionRuntime["Service"];
-
- const runtime = yield* makeXAiPromptCompletionRuntime(baseRuntime);
- const promptFiber = yield* runtime
- .prompt({ prompt: [{ type: "text", text: "hi" }] })
- .pipe(Effect.forkChild);
- yield* Effect.yieldNow;
- const sessionNotificationHandler = handlers.get("_x.ai/session_notification");
- expect(sessionNotificationHandler).toBeDefined();
- yield* sessionNotificationHandler!({
- sessionId: "root-session",
- update: {
- sessionUpdate: "turn_completed",
- prompt_id: "t3-xai-prompt-1",
- stop_reason: "end_turn",
- },
- });
- const response = yield* Fiber.join(promptFiber);
- expect(response.stopReason).toBe("end_turn");
- expect(handlers.has("_x.ai/session/update")).toBe(true);
- }),
- );
-
- it.effect("ignores turn_completed for non-pending prompt ids and task completions", () =>
- Effect.gen(function* () {
- const handlers = new Map Effect.Effect>();
- const hungPrompt = yield* Deferred.make();
- const baseRuntime = {
- start: () =>
- Effect.succeed({
- sessionId: "root-session",
- initializeResult: {},
- sessionSetupResult: {},
- modelConfigId: undefined,
- }),
- prompt: () => Deferred.await(hungPrompt),
- cancel: Effect.void,
- handleExtNotification: (
- method: string,
- _schema: unknown,
- handler: (notification: unknown) => Effect.Effect,
- ) => {
- handlers.set(method, handler);
- return Effect.void;
- },
- handleExtRequest: () => Effect.void,
- } as unknown as AcpSessionRuntime.AcpSessionRuntime["Service"];
-
- const runtime = yield* makeXAiPromptCompletionRuntime(baseRuntime);
- const promptFiber = yield* runtime
- .prompt({ prompt: [{ type: "text", text: "hi" }] })
- .pipe(Effect.forkChild);
- yield* Effect.yieldNow;
- const sessionUpdateHandler = handlers.get("_x.ai/session/update");
- expect(sessionUpdateHandler).toBeDefined();
- yield* sessionUpdateHandler!({
- sessionId: "root-session",
- update: {
- sessionUpdate: "turn_completed",
- prompt_id: "task-completed-call-abc",
- stop_reason: "end_turn",
- },
- });
- yield* sessionUpdateHandler!({
- sessionId: "root-session",
- update: {
- sessionUpdate: "turn_completed",
- prompt_id: "some-other-cli-turn",
- stop_reason: "end_turn",
- },
- });
- yield* Effect.yieldNow;
- expect(promptFiber.pollUnsafe()).toBeUndefined();
- yield* Fiber.interrupt(promptFiber);
- }),
- );
-
- it.effect("ignores prompt_complete notifications for foreign session ids", () =>
- Effect.gen(function* () {
- const handlers = new Map Effect.Effect>();
- const hungPrompt = yield* Deferred.make();
- const baseRuntime = {
- start: () =>
- Effect.succeed({
- sessionId: "root-session",
- initializeResult: {},
- sessionSetupResult: {},
- modelConfigId: undefined,
- }),
- prompt: () => Deferred.await(hungPrompt),
- cancel: Effect.void,
- handleExtNotification: (
- method: string,
- _schema: unknown,
- handler: (notification: unknown) => Effect.Effect,
- ) => {
- handlers.set(method, handler);
- return Effect.void;
- },
- handleExtRequest: () => Effect.void,
- } as unknown as AcpSessionRuntime.AcpSessionRuntime["Service"];
-
- const runtime = yield* makeXAiPromptCompletionRuntime(baseRuntime);
- const promptFiber = yield* runtime
- .prompt({ prompt: [{ type: "text", text: "hi" }] })
- .pipe(Effect.forkChild);
- yield* Effect.yieldNow;
- const promptCompleteHandler = handlers.get("_x.ai/session/prompt_complete");
- expect(promptCompleteHandler).toBeDefined();
- yield* promptCompleteHandler!({
- sessionId: "child-session",
- });
- yield* Effect.yieldNow;
- expect(promptFiber.pollUnsafe()).toBeUndefined();
- yield* Fiber.interrupt(promptFiber);
- }),
- );
-
- it.effect(
- "ignores promptId-less prompt_complete when multiple prompts are pending on the same session",
- () =>
- Effect.gen(function* () {
- const handlers = new Map Effect.Effect>();
- const hungPrompt = yield* Deferred.make();
- const baseRuntime = {
- start: () =>
- Effect.succeed({
- sessionId: "root-session",
- initializeResult: {},
- sessionSetupResult: {},
- modelConfigId: undefined,
- }),
- prompt: () => Deferred.await(hungPrompt),
- cancel: Effect.void,
- handleExtNotification: (
- method: string,
- _schema: unknown,
- handler: (notification: unknown) => Effect.Effect,
- ) => {
- handlers.set(method, handler);
- return Effect.void;
- },
- handleExtRequest: () => Effect.void,
- } as unknown as AcpSessionRuntime.AcpSessionRuntime["Service"];
-
- const runtime = yield* makeXAiPromptCompletionRuntime(baseRuntime);
- const firstPromptFiber = yield* runtime
- .prompt({ prompt: [{ type: "text", text: "first" }] })
- .pipe(Effect.forkChild);
- yield* Effect.yieldNow;
- const secondPromptFiber = yield* runtime
- .prompt({ prompt: [{ type: "text", text: "second" }] })
- .pipe(Effect.forkChild);
- yield* Effect.yieldNow;
- const promptCompleteHandler = handlers.get("_x.ai/session/prompt_complete");
- expect(promptCompleteHandler).toBeDefined();
- yield* promptCompleteHandler!({
- sessionId: "root-session",
- stopReason: "end_turn",
- });
- yield* Effect.yieldNow;
- expect(firstPromptFiber.pollUnsafe()).toBeUndefined();
- expect(secondPromptFiber.pollUnsafe()).toBeUndefined();
- yield* Fiber.interrupt(firstPromptFiber);
- yield* Fiber.interrupt(secondPromptFiber);
- }),
- );
-
- it.effect("injects promptId and requestId into prompt _meta", () =>
- Effect.gen(function* () {
- let capturedMeta: Record | null | undefined;
- const baseRuntime = {
- start: () => Effect.succeed({ sessionId: "session-1" }),
- prompt: (payload: { readonly _meta?: Record | null }) => {
- capturedMeta = payload._meta ?? null;
- return Effect.succeed({ stopReason: "end_turn" as const });
- },
- cancel: Effect.void,
- handleExtNotification: () => Effect.void,
- handleExtRequest: () => Effect.void,
- } as unknown as AcpSessionRuntime.AcpSessionRuntime["Service"];
-
- const runtime = yield* makeXAiPromptCompletionRuntime(baseRuntime);
- yield* runtime.prompt({ prompt: [{ type: "text", text: "hi" }] });
-
- expect(typeof capturedMeta?.promptId).toBe("string");
- expect(capturedMeta).toMatchObject({
- promptId: capturedMeta?.promptId,
- requestId: capturedMeta?.promptId,
- });
- }),
- );
-
- it.effect("cancels pending completions without restarting a stalled runtime", () =>
- Effect.gen(function* () {
- const hungStart = yield* Deferred.make();
- const hungPrompt = yield* Deferred.make();
- let startCount = 0;
- let cancelCalled = false;
- const baseRuntime = {
- start: () => {
- startCount += 1;
- return startCount === 1
- ? Effect.succeed({
- sessionId: "root-session",
- initializeResult: {},
- sessionSetupResult: {},
- modelConfigId: undefined,
- })
- : Deferred.await(hungStart);
- },
- prompt: () => Deferred.await(hungPrompt),
- cancel: Effect.sync(() => {
- cancelCalled = true;
- }),
- handleExtNotification: () => Effect.void,
- handleExtRequest: () => Effect.void,
- } as unknown as AcpSessionRuntime.AcpSessionRuntime["Service"];
-
- const runtime = yield* makeXAiPromptCompletionRuntime(baseRuntime);
- const promptFiber = yield* runtime
- .prompt({ prompt: [{ type: "text", text: "hi" }] })
- .pipe(Effect.forkChild);
- yield* Effect.yieldNow;
-
- yield* runtime.cancel;
-
- expect(cancelCalled).toBe(true);
- expect(startCount).toBe(1);
- expect((yield* Fiber.join(promptFiber)).stopReason).toBe("cancelled");
- }),
- );
-});
-
-describe("Grok exit_plan_mode capture (#8358)", () => {
- it("extracts plan markdown from exit_plan_mode payloads", () => {
- const decode = Schema.decodeUnknownSync(XAiExitPlanModeRequest);
- const direct = decode({
- sessionId: "session-1",
- toolCallId: "exit-1",
- planContent: "# Plan\n\n- do the thing\n",
- });
- expect(extractXAiExitPlanMarkdown(direct)).toBe("# Plan\n\n- do the thing");
-
- const wrapped = decode({
- method: "_x.ai/exit_plan_mode",
- params: {
- sessionId: "session-1",
- toolCallId: "exit-1",
- planContent: null,
- },
- });
- expect(extractXAiExitPlanMarkdown(wrapped, " # fallback plan ")).toBe("# fallback plan");
- expect(extractXAiExitPlanMarkdown(wrapped, "")).toBe(XAI_EMPTY_PLAN_MARKDOWN);
- expect(extractXAiExitPlanMarkdown(wrapped)).toBe(XAI_EMPTY_PLAN_MARKDOWN);
- });
- it("identifies Grok plan.md paths and extracts markdown from tool call data", () => {
- const linuxHost = { platform: "linux" as const, environment: {} };
- const windowsHost = { platform: "win32" as const, environment: {} };
- const grokHomeHost = {
- platform: "linux" as const,
- environment: { GROK_HOME: "/opt/grok-data" },
- };
- const home = NodeOS.homedir().replace(/\\/g, "/");
- const sessionPlan = `${home}/.grok/sessions/abc/plan.md`;
- const nestedSessionPlan = `${home}/.grok/sessions/%2Fhome%2Fproj/019fd20e-c563-70a0-b801-a6bc51815a9b/plan.md`;
- expect(isGrokPlanMarkdownPath(sessionPlan, linuxHost)).toBe(true);
- expect(isGrokPlanMarkdownPath(nestedSessionPlan, linuxHost)).toBe(true);
- expect(isGrokPlanMarkdownPath("~/.grok/sessions/abc/plan.md", linuxHost)).toBe(true);
- expect(isGrokPlanMarkdownPath("/tmp/mock-home/.grok/sessions/sess/plan.md", linuxHost)).toBe(
- true,
- );
- expect(isGrokPlanMarkdownPath("/home/other/.grok/sessions/sess/plan.md", linuxHost)).toBe(true);
- expect(isGrokPlanMarkdownPath("/HOME/other/.grok/sessions/sess/plan.md", linuxHost)).toBe(
- false,
- );
- expect(isGrokPlanMarkdownPath("C:/Users/other/.grok/sessions/id/plan.md", windowsHost)).toBe(
- true,
- );
- expect(isGrokPlanMarkdownPath("c:/users/OTHER/.GROK/SESSIONS/id/PLAN.MD", windowsHost)).toBe(
- true,
- );
- expect(
- isGrokPlanMarkdownPath("C:\\Users\\other\\.grok\\sessions\\id\\plan.md", windowsHost),
- ).toBe(true);
- expect(isGrokPlanMarkdownPath("/opt/grok-data/sessions/sess/plan.md", grokHomeHost)).toBe(true);
- expect(
- isGrokPlanMarkdownPath("/OPT/GROK-DATA/sessions/sess/plan.md", {
- platform: "win32",
- environment: { GROK_HOME: "/opt/grok-data" },
- }),
- ).toBe(true);
- expect(isGrokPlanMarkdownPath("/OPT/GROK-DATA/sessions/sess/plan.md", grokHomeHost)).toBe(
- false,
- );
- // Workspace plan.md must not be treated as the session plan file.
- expect(isGrokPlanMarkdownPath("plan.md", linuxHost)).toBe(false);
- expect(isGrokPlanMarkdownPath("/repo/docs/plan.md", linuxHost)).toBe(false);
- expect(isGrokPlanMarkdownPath("/tmp/other.md", linuxHost)).toBe(false);
- expect(isGrokPlanMarkdownPath("/repo/.grok/sessions/example/plan.md", linuxHost)).toBe(false);
- expect(
- isGrokPlanMarkdownPath(`${home}/project/.grok/sessions/example/plan.md`, linuxHost),
- ).toBe(false);
- expect(
- isGrokPlanMarkdownPath("/home/other/.grok/sessions/../../project/plan.md", linuxHost),
- ).toBe(false);
- expect(
- isGrokPlanMarkdownPath("/home/other/.grok/sessions/foo/../../../project/plan.md", linuxHost),
- ).toBe(false);
-
- expect(
- extractGrokPlanMarkdownFromToolCallData(
- {
- rawInput: {
- file_path: sessionPlan,
- content: "# From rawInput\n\n- a\n",
- },
- },
- linuxHost,
- ),
- ).toBe("# From rawInput\n\n- a");
-
- expect(
- extractGrokPlanMarkdownFromToolCallData(
- {
- content: [
- {
- type: "diff",
- path: sessionPlan,
- oldText: "",
- newText: "# From diff\n\n- b\n",
- },
- ],
- },
- linuxHost,
- ),
- ).toBe("# From diff\n\n- b");
-
- expect(
- extractGrokPlanMarkdownFromToolCallData(
- {
- rawInput: { file_path: sessionPlan, content: "" },
- content: [
- {
- type: "diff",
- path: sessionPlan,
- oldText: "",
- newText: "# From diff after empty rawInput\n",
- },
- ],
- },
- linuxHost,
- ),
- ).toBe("# From diff after empty rawInput");
-
- expect(
- extractGrokPlanMarkdownFromToolCallData(
- {
- rawInput: { file_path: sessionPlan, content: "" },
- },
- linuxHost,
- ),
- ).toBe("");
-
- expect(
- extractGrokPlanMarkdownFromToolCallData(
- {
- content: [{ type: "diff", path: sessionPlan, oldText: "# old", newText: "" }],
- },
- linuxHost,
- ),
- ).toBe("");
-
- expect(
- extractGrokPlanMarkdownFromToolCallData(
- {
- rawInput: { file_path: "/tmp/readme.md", content: "nope" },
- },
- linuxHost,
- ),
- ).toBeUndefined();
-
- expect(
- extractGrokPlanMarkdownFromToolCallData(
- {
- rawInput: { file_path: "/repo/docs/plan.md", content: "# Project plan\n" },
- },
- linuxHost,
- ),
- ).toBeUndefined();
- });
-});
diff --git a/apps/server/src/provider/acp/XAiAcpExtension.ts b/apps/server/src/provider/acp/XAiAcpExtension.ts
deleted file mode 100644
index 4c424a664d903f54ff19132bccccd0cef55f8f06..0000000000000000000000000000000000000000
--- a/apps/server/src/provider/acp/XAiAcpExtension.ts
+++ /dev/null
@@ -1,1713 +0,0 @@
-// @effect-diagnostics nodeBuiltinImport:off -- Grok's plan file lives under the OS home dir.
-import * as NodeOS from "node:os";
-
-import type { ProviderUserInputAnswers, UserInputQuestion } from "@t3tools/contracts";
-import * as Cause from "effect/Cause";
-import * as Deferred from "effect/Deferred";
-import * as Effect from "effect/Effect";
-import * as Fiber from "effect/Fiber";
-import * as Ref from "effect/Ref";
-import * as Schema from "effect/Schema";
-import * as EffectAcpErrors from "effect-acp/errors";
-import type * as EffectAcpSchema from "effect-acp/compat";
-
-import type * as AcpSessionRuntime from "@t3tools/provider-acp/server/AcpSessionRuntime";
-import type { AcpToolCallState } from "@t3tools/provider-acp/server/runtimeModel";
-
-const xAiStopReasonMissingMetaKey = "xAiStopReasonMissing";
-export const xAiRateLimitedErrorCode = -32003;
-const completedXAiPromptIdLimit = 128;
-
-const XAiPromptCompleteNotification = Schema.Struct({
- sessionId: Schema.String,
- promptId: Schema.optional(Schema.String),
- stopReason: Schema.optional(Schema.String),
- agentResult: Schema.optional(Schema.NullOr(Schema.Unknown)),
-});
-
-type XAiPromptCompleteNotification = typeof XAiPromptCompleteNotification.Type;
-
-/**
- * Grok persists root turn completion as an extension session update:
- * `{ params: { sessionId, update: {
- * sessionUpdate: "turn_completed", prompt_id, stop_reason } } }`.
- * The open-source runtime forwards it as `x.ai/session_notification` and
- * stores/replays it as `_x.ai/session/update`; released 0.2.106 forwards the
- * underscore alias `_x.ai/session_notification`. Current source also emits
- * the fire-and-forget canonical `x.ai/session/prompt_complete`, while released
- * builds use the underscore alias.
- */
-const XAiSessionUpdateNotification = Schema.Struct({
- sessionId: Schema.String,
- update: Schema.Struct({
- sessionUpdate: Schema.String,
- prompt_id: Schema.optional(Schema.String),
- promptId: Schema.optional(Schema.String),
- stop_reason: Schema.optional(Schema.String),
- stopReason: Schema.optional(Schema.String),
- agent_result: Schema.optional(Schema.NullOr(Schema.Unknown)),
- // subagent_finished
- child_session_id: Schema.optional(Schema.String),
- status: Schema.optional(Schema.String),
- output: Schema.optional(Schema.NullOr(Schema.String)),
- error: Schema.optional(Schema.NullOr(Schema.String)),
- }),
- _meta: Schema.optional(Schema.Unknown),
-});
-
-type XAiSessionUpdateNotification = typeof XAiSessionUpdateNotification.Type;
-
-const decodeXAiSessionUpdateNotification = Schema.decodeUnknownEffect(XAiSessionUpdateNotification);
-
-const xAiSessionNotificationMethods = [
- "x.ai/session_notification",
- "_x.ai/session_notification",
- "_x.ai/session/update",
-] as const;
-
-const XAI_TASK_COMPLETED_PROMPT_ID_PREFIX = "task-completed-";
-
-/**
- * Map a Grok `_x.ai/session/update` payload to a prompt-complete shape when it
- * is a root `turn_completed` for a real prompt id. Returns null for hooks,
- * background task completions, and other update kinds.
- */
-export function xAiPromptCompleteFromSessionUpdate(
- notification: XAiSessionUpdateNotification,
-): XAiPromptCompleteNotification | null {
- const update = notification.update;
- if (update.sessionUpdate !== "turn_completed") {
- return null;
- }
- const promptId = nonEmptyString(update.prompt_id) ?? nonEmptyString(update.promptId);
- // Require prompt id so we never session-fallback match a background task
- // completion (`task-completed-*`) or an unrelated CLI turn.
- if (promptId === undefined || promptId.startsWith(XAI_TASK_COMPLETED_PROMPT_ID_PREFIX)) {
- return null;
- }
- const stopReason = nonEmptyString(update.stop_reason) ?? nonEmptyString(update.stopReason);
- return {
- sessionId: notification.sessionId,
- promptId,
- ...(stopReason === undefined ? {} : { stopReason }),
- ...(update.agent_result === undefined ? {} : { agentResult: update.agent_result }),
- };
-}
-
-/**
- * Grok answers a finished background command in its own turn, tagging every
- * frame with a `task-completed-*` prompt id instead of the one T3 sent.
- */
-export function isXAiTaskCompletedWakeNotification(
- notification: EffectAcpSchema.SessionNotification,
-): boolean {
- const promptId = notification._meta?.promptId;
- return typeof promptId === "string" && promptId.startsWith(XAI_TASK_COMPLETED_PROMPT_ID_PREFIX);
-}
-
-interface PendingXAiPromptCompletion {
- readonly sessionId: string;
- readonly promptId: string;
- readonly deferred: Deferred.Deferred<
- EffectAcpSchema.PromptResponse,
- EffectAcpErrors.AcpRequestError
- >;
-}
-
-export interface XAiAcpSubagentUpdate {
- readonly nativeTaskId: string;
- readonly prompt: string;
- readonly title: string | null;
- readonly model: string | null;
- readonly status: "running" | "completed" | "failed";
- readonly childSessionId: string | null;
- readonly result: string | null;
- /**
- * When false, the ACP adapter also projects a normal tool turn item for this
- * tool call (used for get_command_or_subagent_output hydration). Default true.
- */
- readonly suppressNormalTool?: boolean;
-}
-
-export interface XAiAcpBackgroundToolMutation {
- readonly taskId: string;
- readonly status: "running" | "completed" | "failed";
- readonly appendOutput: string;
-}
-
-export interface XAiAcpSubagentEndNotice {
- readonly childSessionId: string;
- readonly status: "completed" | "failed";
-}
-
-const XAI_UUID_RE = "[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}";
-
-function unknownRecord(value: unknown): Record | undefined {
- return typeof value === "object" && value !== null && !Array.isArray(value)
- ? (value as Record)
- : undefined;
-}
-
-function nonEmptyString(value: unknown): string | undefined {
- return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
-}
-
-function titleKey(toolCall: AcpToolCallState): string {
- return (toolCall.title ?? "").trim().toLowerCase();
-}
-
-function decodeByteText(value: unknown): string | undefined {
- if (typeof value === "string") return nonEmptyString(value);
- if (!Array.isArray(value)) return undefined;
- if (value.length === 0) return undefined;
- if (!value.every((entry) => typeof entry === "number")) return undefined;
- try {
- return nonEmptyString(new TextDecoder().decode(Uint8Array.from(value as number[])));
- } catch {
- return undefined;
- }
-}
-
-function xAiToolOutputText(toolCall: AcpToolCallState): string | undefined {
- const rawOutput = unknownRecord(toolCall.data.rawOutput);
- if (rawOutput !== undefined) {
- const direct =
- nonEmptyString(rawOutput.text) ??
- nonEmptyString(rawOutput.output_for_prompt) ??
- decodeByteText(rawOutput.output);
- if (direct !== undefined) return direct;
- const result = unknownRecord(rawOutput.Result) ?? unknownRecord(rawOutput.result);
- if (result !== undefined) {
- const nested =
- nonEmptyString(result.output) ??
- nonEmptyString(result.text) ??
- decodeByteText(result.output);
- if (nested !== undefined) return nested;
- }
- }
- if (typeof toolCall.data.rawOutput === "string") {
- return nonEmptyString(toolCall.data.rawOutput);
- }
- const content = toolCall.data.content;
- if (!Array.isArray(content)) return undefined;
- const text = content
- .flatMap((entry) => {
- const record = unknownRecord(entry);
- const nested = unknownRecord(record?.content);
- return nonEmptyString(nested?.text) ?? nonEmptyString(record?.text) ?? [];
- })
- .join("\n")
- .trim();
- return text.length > 0 ? text : undefined;
-}
-
-function firstUuidMatch(text: string, pattern: RegExp): string | undefined {
- const match = pattern.exec(text);
- return match?.[1];
-}
-
-function extractXAiChildSessionId(output: string | undefined): string | null {
- if (output === undefined) return null;
- return (
- firstUuidMatch(output, new RegExp(`(?:^|\\n)\\s*Agent ID:\\s*(${XAI_UUID_RE})\\b`, "i")) ??
- firstUuidMatch(output, new RegExp(`(?:^|\\n)\\s*subagent_id:\\s*(${XAI_UUID_RE})\\b`, "i")) ??
- firstUuidMatch(output, new RegExp(`===\\s*Task\\s+(${XAI_UUID_RE})\\s*===`, "i")) ??
- null
- );
-}
-
-function isXAiAsyncSpawnAck(output: string | undefined): boolean {
- if (output === undefined) return false;
- if (/subagent started in background/i.test(output)) return true;
- return (
- new RegExp(`subagent_id:\\s*${XAI_UUID_RE}`, "i").test(output) &&
- /get_command_or_subagent_output/i.test(output)
- );
-}
-
-function isXAiMonitorStartAck(output: string | undefined): boolean {
- if (output === undefined) return false;
- return /monitor started\s*\(\s*task\s+/i.test(output);
-}
-
-function isXAiSpawnOrTaskTool(
- toolCall: AcpToolCallState,
- rawInput: Record | undefined,
-): boolean {
- const title = titleKey(toolCall);
- if (title === "task" || title === "spawn_subagent" || title.includes("spawn subagent")) {
- return true;
- }
- const variant = nonEmptyString(rawInput?.variant)?.toLowerCase();
- if (variant === "cursortask" || variant === "task" || variant === "spawn_subagent") {
- return true;
- }
- if (nonEmptyString(rawInput?.subagent_type) !== undefined) return true;
- if (nonEmptyString(rawInput?.subagentType) !== undefined) return true;
- return false;
-}
-
-function isXAiGetSubagentOutputTool(
- toolCall: AcpToolCallState,
- rawInput: Record | undefined,
-): boolean {
- const title = titleKey(toolCall);
- if (
- title === "get_command_or_subagent_output" ||
- title.includes("get_command_or_subagent_output") ||
- title === "wait_commands_or_subagents"
- ) {
- return true;
- }
- const variant = nonEmptyString(rawInput?.variant)?.toLowerCase();
- if (variant === "taskoutput" || variant === "task_output") return true;
- const rawOutput = unknownRecord(toolCall.data.rawOutput);
- const outputType = nonEmptyString(rawOutput?.type)?.toLowerCase();
- if (outputType === "taskoutput" || outputType === "task_output") return true;
- // ACP often titles this tool with the finished subagent label.
- if (title.startsWith("[subagent:") && Array.isArray(rawInput?.task_ids)) return true;
- return false;
-}
-
-export function isXAiMonitorTool(toolCall: AcpToolCallState): boolean {
- const title = titleKey(toolCall);
- if (title === "monitor" || title.startsWith("monitor ")) return true;
- const rawInput = unknownRecord(toolCall.data.rawInput);
- const variant = nonEmptyString(rawInput?.variant)?.toLowerCase();
- if (variant === "monitor") return true;
- // Live start ACKs often keep a generic title ("Tool") and only mark the
- // structured envelope as Monitor; title rewrite to description must not
- // prevent monitor registration / inProgress forcing.
- const rawOutput = unknownRecord(toolCall.data.rawOutput);
- const outputType = nonEmptyString(rawOutput?.type)?.toLowerCase();
- if (outputType === "monitor") return true;
- // Released clients can wrap the start ACK as plain Text and omit the Monitor
- // variant. The ACK is still unambiguous and contains the task id required for
- // background tracking, even after a generic title is rewritten from
- // rawInput.description.
- return isXAiMonitorStartAck(xAiToolOutputText(toolCall));
-}
-
-/**
- * A shell command Grok runs in the background (asked for, or moved there after
- * its auto-background timeout) acknowledges with
- * `{ type: "BackgroundTaskStarted", task_id }` and completes its tool call
- * while the process still runs. Like a monitor, it ends only with
- * `x.ai/task_completed` for that task id.
- */
-function xAiBackgroundShellTaskId(toolCall: AcpToolCallState): string | undefined {
- const rawOutput = unknownRecord(toolCall.data.rawOutput);
- return nonEmptyString(rawOutput?.type) === "BackgroundTaskStarted"
- ? nonEmptyString(rawOutput?.task_id)
- : undefined;
-}
-
-export function extractXAiMonitorTaskId(toolCall: AcpToolCallState): string | undefined {
- const backgroundShellTaskId = xAiBackgroundShellTaskId(toolCall);
- if (backgroundShellTaskId !== undefined) return backgroundShellTaskId;
- if (!isXAiMonitorTool(toolCall)) return undefined;
- const rawOutput = unknownRecord(toolCall.data.rawOutput);
- // Live ACP start ACK: { type: "Monitor", taskId, timeoutMs, persistent }
- if (rawOutput !== undefined && nonEmptyString(rawOutput.type)?.toLowerCase() === "monitor") {
- const structured = nonEmptyString(rawOutput.taskId) ?? nonEmptyString(rawOutput.task_id);
- if (structured !== undefined) return structured;
- }
- const output = xAiToolOutputText(toolCall);
- if (output !== undefined) {
- const fromAck = firstUuidMatch(
- output,
- new RegExp(`monitor started\\s*\\(\\s*task\\s+(${XAI_UUID_RE})\\b`, "i"),
- );
- if (fromAck !== undefined) return fromAck;
- }
- const rawInput = unknownRecord(toolCall.data.rawInput);
- return (
- nonEmptyString(rawInput?.task_id) ??
- nonEmptyString(rawInput?.taskId) ??
- (Array.isArray(rawInput?.task_ids) ? nonEmptyString(rawInput.task_ids[0]) : undefined)
- );
-}
-
-/**
- * Live Monitor start ACK `{ type: "Monitor", persistent: true }` (or matching
- * rawInput). Persistent monitors should not hold root-turn deferred finalize.
- */
-export function isXAiPersistentMonitor(toolCall: AcpToolCallState): boolean {
- if (!isXAiMonitorTool(toolCall)) return false;
- const rawOutput = unknownRecord(toolCall.data.rawOutput);
- if (rawOutput !== undefined && nonEmptyString(rawOutput.type)?.toLowerCase() === "monitor") {
- return rawOutput.persistent === true;
- }
- const rawInput = unknownRecord(toolCall.data.rawInput);
- return rawInput?.persistent === true;
-}
-
-/**
- * When get_command_or_subagent_output / TaskOutput completes registered monitor
- * task(s), return hydration for each completed background tool id.
- */
-export function extractXAiBackgroundTaskCompletion(toolCall: AcpToolCallState): ReadonlyArray<{
- readonly taskId: string;
- readonly status: "running" | "completed" | "failed";
- readonly appendOutput: string;
-}> {
- const rawInput = unknownRecord(toolCall.data.rawInput);
- if (!isXAiGetSubagentOutputTool(toolCall, rawInput)) return [];
- // Caller matches taskId against registered background tools (monitors).
- // Subagent hydration stays on extractXAiAcpSubagentUpdate.
- const text = xAiToolOutputText(toolCall);
- const status = statusFromGetOutputTool(toolCall, text);
- const appendOutput = resultFromGetOutputTool(toolCall, text) ?? "";
- // Prefer the completed identity from the result/header over the request list
- // (one call can poll multiple task_ids).
- const resultTaskId = resultTaskIdFromGetOutputTool(toolCall, text);
- if (resultTaskId !== undefined) {
- return [{ taskId: resultTaskId, status, appendOutput }];
- }
- const requestIds: string[] = [];
- const push = (value: unknown) => {
- const id = nonEmptyString(value);
- if (id !== undefined && new RegExp(`^${XAI_UUID_RE}$`, "i").test(id)) {
- requestIds.push(id);
- }
- };
- if (Array.isArray(rawInput?.task_ids)) {
- for (const entry of rawInput.task_ids) push(entry);
- }
- push(rawInput?.task_id);
- push(rawInput?.taskId);
- const unique = [...new Set(requestIds)];
- return unique.map((taskId) => ({ taskId, status, appendOutput }));
-}
-
-/**
- * A finished kill_command_or_subagent call is the genuine end signal for every
- * task id it names: the Grok CLI emits no `x.ai/task_completed` for a killed
- * task. A failed kill is not proof the target stopped, so leave the existing
- * task state unchanged until a genuine terminal signal arrives.
- */
-export function extractXAiKilledBackgroundTasks(toolCall: AcpToolCallState): ReadonlyArray<{
- readonly taskId: string;
- readonly status: "running" | "completed" | "failed";
- readonly appendOutput: string;
-}> {
- const title = titleKey(toolCall);
- const rawInput = unknownRecord(toolCall.data.rawInput);
- const variant = nonEmptyString(rawInput?.variant)?.toLowerCase();
- if (!title.includes("kill_command_or_subagent") && variant !== "kill") return [];
- if (toolCall.status !== "completed") return [];
- const taskIds: string[] = [];
- const push = (value: unknown) => {
- const id = nonEmptyString(value);
- if (id !== undefined) taskIds.push(id);
- };
- if (Array.isArray(rawInput?.task_ids)) {
- for (const entry of rawInput.task_ids) push(entry);
- }
- push(rawInput?.task_id);
- push(rawInput?.taskId);
- return [...new Set(taskIds)].map((taskId) => ({
- taskId,
- status: "completed" as const,
- appendOutput: "",
- }));
-}
-
-const XAiTaskLifecycleNotification = Schema.Struct({
- sessionId: Schema.String,
- update: Schema.Struct({
- sessionUpdate: Schema.String,
- task_id: Schema.optional(Schema.String),
- tool_call_id: Schema.optional(Schema.String),
- command: Schema.optional(Schema.String),
- description: Schema.optional(Schema.NullOr(Schema.String)),
- monitor_description: Schema.optional(Schema.NullOr(Schema.String)),
- task_snapshot: Schema.optional(
- Schema.Struct({
- task_id: Schema.optional(Schema.String),
- output: Schema.optional(Schema.String),
- exit_code: Schema.optional(Schema.NullOr(Schema.Number)),
- command: Schema.optional(Schema.String),
- display_command: Schema.optional(Schema.NullOr(Schema.String)),
- description: Schema.optional(Schema.NullOr(Schema.String)),
- // `TaskKind` in grok-build crates/common/xai-tool-runtime/src/notification.rs.
- kind: Schema.optional(Schema.String),
- }),
- ),
- }),
- _meta: Schema.optional(Schema.Unknown),
-});
-
-type XAiTaskLifecycleNotification = typeof XAiTaskLifecycleNotification.Type;
-
-export interface XAiBackgroundTaskLifecycleMutation {
- readonly sessionId: string;
- readonly taskId: string;
- readonly status: "running" | "completed" | "failed";
- /** Final output from `task_completed.task_snapshot`, when Grok sent one. */
- readonly output?: string;
- /** What the task is and how to name it, when Grok said. */
- readonly report?:
- | { readonly kind: "command"; readonly label?: string; readonly exitCode?: number }
- | { readonly kind: "monitor"; readonly label?: string };
-}
-
-function xAiBackgroundTaskReport(
- update: XAiTaskLifecycleNotification["update"],
-): XAiBackgroundTaskLifecycleMutation["report"] {
- const snapshot = update.task_snapshot;
- const monitorDescription = nonEmptyString(update.monitor_description ?? undefined);
- const displayCommand = nonEmptyString(snapshot?.display_command ?? undefined);
- const isMonitor =
- snapshot?.kind === "monitor" ||
- monitorDescription !== undefined ||
- displayCommand?.startsWith("[monitor]") === true;
- // Grok's snapshot defaults `kind` to bash; anything that carries a command is one.
- const isCommand =
- snapshot?.kind === "bash" || snapshot?.command !== undefined || update.command !== undefined;
- if (!isMonitor && !isCommand) return undefined;
- const label =
- monitorDescription ??
- nonEmptyString(snapshot?.description ?? undefined) ??
- nonEmptyString(update.description ?? undefined) ??
- displayCommand?.replace(/^\[monitor\]\s*/, "") ??
- nonEmptyString(snapshot?.command) ??
- nonEmptyString(update.command);
- const named = label === undefined ? {} : { label };
- if (isMonitor) return { kind: "monitor", ...named };
- const exitCode = snapshot?.exit_code;
- return { kind: "command", ...named, ...(typeof exitCode === "number" ? { exitCode } : {}) };
-}
-
-export function xAiBackgroundTaskLifecycleMutation(
- notification: XAiTaskLifecycleNotification,
- status: "running" | "completed",
-): XAiBackgroundTaskLifecycleMutation | null {
- const update = notification.update;
- const taskId =
- nonEmptyString(update.task_snapshot?.task_id) ??
- nonEmptyString(update.task_id) ??
- nonEmptyString(update.tool_call_id);
- if (taskId === undefined) return null;
- const exitCode = update.task_snapshot?.exit_code;
- const output = update.task_snapshot?.output;
- const report = xAiBackgroundTaskReport(update);
- return {
- sessionId: notification.sessionId,
- taskId,
- status:
- status === "completed" && typeof exitCode === "number" && exitCode !== 0 ? "failed" : status,
- ...(output === undefined ? {} : { output }),
- ...(report === undefined ? {} : { report }),
- };
-}
-
-/**
- * Grok task lifecycle notifications use canonical `x.ai/task_*` methods in
- * current builds; older 0.2.x builds used `_x.ai/task_*`. Interactive cancel
- * preserves tasks that were already backgrounded (and some older builds could
- * background the cancelled foreground command), so both spellings must be
- * tracked. Residual frames then buffer instead of waking synthetic
- * continuations, and pending-background-work stays truthful until the task
- * ends (or the model kills it via kill_command_or_subagent).
- */
-export const registerXAiBackgroundTaskTracking = (
- runtime: Pick,
- apply: (mutation: XAiBackgroundTaskLifecycleMutation) => Effect.Effect,
-): Effect.Effect =>
- Effect.forEach(
- [
- ["x.ai/task_backgrounded", "running"],
- ["_x.ai/task_backgrounded", "running"],
- ["x.ai/task_completed", "completed"],
- ["_x.ai/task_completed", "completed"],
- ] as const,
- ([method, status]) =>
- runtime.handleExtNotification(method, XAiTaskLifecycleNotification, (notification) => {
- const mutation = xAiBackgroundTaskLifecycleMutation(notification, status);
- return mutation === null ? Effect.void : apply(mutation);
- }),
- { discard: true },
- );
-
-/**
- * A background subagent's end, sent on its PARENT session as
- * `{ sessionUpdate: "subagent_finished", child_session_id, status, output, error }`
- * (grok-build `crates/codegen/xai-grok-shell/src/extensions/notification.rs`
- * `SessionUpdate::SubagentFinished`). `status` is exactly "completed",
- * "failed" or "cancelled" (`SubagentResult::status()` in
- * `crates/codegen/xai-grok-tools/src/implementations/grok_build/task/types.rs`);
- * `output` is the final text of a completed subagent and `error` the message
- * of a failed one. Grok follows it with its own `subagent-completed-` wake
- * turn when `will_wake` is true.
- */
-export interface XAiSubagentFinishedNotice {
- readonly sessionId: string;
- readonly childSessionId: string;
- readonly status: "completed" | "failed" | "cancelled";
- readonly result: string | null;
-}
-
-/** Null for other session updates, and for a status Grok does not define. */
-export function xAiSubagentFinishedNotice(
- notification: XAiSessionUpdateNotification,
-): XAiSubagentFinishedNotice | null {
- const update = notification.update;
- const childSessionId = nonEmptyString(update.child_session_id);
- if (update.sessionUpdate !== "subagent_finished" || childSessionId === undefined) return null;
- const status = update.status;
- if (status !== "completed" && status !== "failed" && status !== "cancelled") return null;
- const text = status === "completed" ? update.output : update.error;
- return {
- sessionId: notification.sessionId,
- childSessionId,
- status,
- result: nonEmptyString(text ?? undefined) ?? null,
- };
-}
-
-/**
- * Finishes background subagents from Grok's `subagent_finished`. These arrive
- * on the session notification methods that also carry turn completion; a
- * runtime from {@link makeXAiPromptCompletionRuntime} settles its prompt from
- * each notification before passing it to this handler.
- */
-export const registerXAiSubagentFinished = (
- runtime: Pick,
- finish: (notice: XAiSubagentFinishedNotice) => Effect.Effect,
-): Effect.Effect =>
- Effect.forEach(
- xAiSessionNotificationMethods,
- (method) =>
- runtime.handleExtNotification(method, XAiSessionUpdateNotification, (notification) => {
- const notice = xAiSubagentFinishedNotice(notification);
- return notice === null ? Effect.void : finish(notice);
- }),
- { discard: true },
- );
-
-/**
- * Grok ACP often sends `title: "Tool"` even when rawInput has a useful
- * description or variant (especially Monitor). Prefer description/variant so
- * the timeline matches Claude-style tool names rather than a generic label.
- */
-export function isGenericAcpToolTitle(title: string | undefined): boolean {
- const key = (title ?? "").trim().toLowerCase();
- return key.length === 0 || key === "tool" || key === "tool call" || key === "terminal";
-}
-
-export function resolveXAiAcpToolTitle(toolCall: AcpToolCallState): string | undefined {
- if (!isGenericAcpToolTitle(toolCall.title)) {
- return nonEmptyString(toolCall.title);
- }
- const rawInput = unknownRecord(toolCall.data.rawInput);
- const description = nonEmptyString(rawInput?.description);
- const variant = nonEmptyString(rawInput?.variant)?.toLowerCase();
- if (description !== undefined) {
- if (variant === "monitor" && !description.toLowerCase().startsWith("monitor")) {
- return `Monitor: ${description}`;
- }
- return description;
- }
- if (variant === "monitor") return "Monitor";
- if (variant === "task" || variant === "cursortask" || variant === "spawn_subagent") {
- return "Task";
- }
- if (variant === "taskoutput" || variant === "task_output") return "Task output";
- if (variant !== undefined) {
- return variant;
- }
- const command =
- nonEmptyString(rawInput?.command) ??
- nonEmptyString(unknownRecord(toolCall.data.rawOutput)?.command);
- if (command !== undefined) {
- return command.length > 80 ? `${command.slice(0, 77)}...` : command;
- }
- return nonEmptyString(toolCall.title);
-}
-
-/**
- * Normalize Grok ACP tool presentation and monitor lifecycle:
- * - replace generic titles ("Tool") with description / variant labels
- * - structured Monitor start ACK stays running
- * - text start ACK stays running
- * - structured Bash results with exit_code are terminal, unless Grok itself
- * still reports the tool running. Grok 1.0.41 streams every monitor tick as
- * `status: "in_progress"` with a Bash-shaped rawOutput whose exit_code is
- * already 0, and never sends a completed status for the monitor; its end
- * arrives as `_x.ai/task_completed`.
- */
-export function normalizeXAiAcpToolCallState(toolCall: AcpToolCallState): AcpToolCallState {
- const resolvedTitle = resolveXAiAcpToolTitle(toolCall);
- const withTitle =
- resolvedTitle !== undefined && resolvedTitle !== toolCall.title
- ? { ...toolCall, title: resolvedTitle }
- : toolCall;
-
- const rawOutput = unknownRecord(withTitle.data.rawOutput);
- const outputType = nonEmptyString(rawOutput?.type)?.toLowerCase();
- const reportedRunning = withTitle.status === "inProgress" || withTitle.status === "pending";
- if (outputType === "bash" && !reportedRunning) {
- // Same key set as the adapter's command projection (commandExitCode).
- const exitCode = ["exit_code", "exitCode", "code"]
- .map((key) => rawOutput?.[key])
- .find((value) => typeof value === "number" && Number.isInteger(value));
- if (typeof exitCode === "number") {
- if (exitCode === 0 && (xAiToolOutputText(withTitle)?.trim().length ?? 0) === 0) {
- return { ...withTitle, status: "inProgress" };
- }
- return {
- ...withTitle,
- status: exitCode === 0 ? "completed" : "failed",
- };
- }
- }
- if (xAiBackgroundShellTaskId(withTitle) !== undefined) {
- // Start acknowledgement only; the command still runs in the background.
- return { ...withTitle, status: "inProgress" };
- }
- if (!isXAiMonitorTool(withTitle)) {
- return withTitle;
- }
- if (outputType === "monitor") {
- // Start registration only; process still running in the background.
- return { ...withTitle, status: "inProgress" };
- }
- const output = xAiToolOutputText(withTitle);
- if (withTitle.status === "completed" && isXAiMonitorStartAck(output)) {
- return { ...withTitle, status: "inProgress" };
- }
- return withTitle;
-}
-
-/**
- * Parse Grok synthetic monitor traffic that arrives as root text chunks
- * (`` lines, batched `` event blocks, and
- * "Monitor ... ended" reminders). Coalesced chunks may contain several events;
- * return every match so later progress / end notices are not dropped.
- */
-export function extractXAiAcpBackgroundToolMutation(
- text: string,
-): ReadonlyArray {
- const mutations: XAiAcpBackgroundToolMutation[] = [];
-
- const eventRe = new RegExp(
- `\\s*([\\s\\S]*?)\\s*`,
- "gi",
- );
- for (const eventMatch of text.matchAll(eventRe)) {
- if (eventMatch[1] === undefined) continue;
- const line = (eventMatch[2] ?? "").trim();
- mutations.push({
- taskId: eventMatch[1],
- status: "running",
- appendOutput: line.length > 0 ? `${line}\n` : "",
- });
- }
-
- // Batched form: "N monitor events from 1 monitor ...:\n\n[1] line\n". Still a running
- // monitor; must track and filter like single-event chatter.
- const batchedRe = new RegExp(
- `]*task_id=["']?(${XAI_UUID_RE})["']?[^>]*>\\s*([\\s\\S]*?)\\s*`,
- "gi",
- );
- for (const batchedMatch of text.matchAll(batchedRe)) {
- if (batchedMatch[1] === undefined) continue;
- const lines = (batchedMatch[2] ?? "").trim();
- mutations.push({
- taskId: batchedMatch[1],
- status: "running",
- appendOutput: lines.length > 0 ? `${lines}\n` : "",
- });
- }
-
- const endedRe = new RegExp(`Monitor\\s+["']?(${XAI_UUID_RE})["']?\\s+ended`, "gi");
- for (const endedMatch of text.matchAll(endedRe)) {
- if (endedMatch[1] === undefined) continue;
- // Only the "Monitor … ended …" clause is outcome text. Description/output
- // after the header can contain words like "error" without meaning failure.
- const outcomeClause = (endedMatch[0] ?? "").trim();
- const afterHeader = text.slice((endedMatch.index ?? 0) + endedMatch[0].length);
- const outcomeTail = afterHeader.split(/\n/)[0] ?? "";
- const outcome = `${outcomeClause}${outcomeTail}`;
- const summary = text.replace(/<\/?system-reminder>/gi, "").trim();
- mutations.push({
- taskId: endedMatch[1],
- status:
- /exited\s*\(\s*code\s*0\s*\)/i.test(outcome) || /ended cleanly/i.test(outcome)
- ? "completed"
- : /exited|failed|error|signal/i.test(outcome)
- ? "failed"
- : "completed",
- appendOutput: summary.length > 0 ? `${summary}\n` : "Monitor ended.\n",
- });
- }
-
- return mutations;
-}
-
-/**
- * Parse Grok's root-session subagent end reminder ("Background subagent
- * "" (general-purpose: "...") completed successfully."). The agent is
- * free to skip get_command_or_subagent_output hydration, so this reminder can
- * be the only terminal signal for the subagent row; a row stuck on running
- * holds deferred finalize (and the whole run) open until harness timeout.
- */
-function stripLeadingBalancedParenGroup(text: string): string {
- const trimmed = text.replace(/^\s+/, "");
- if (!trimmed.startsWith("(")) return trimmed;
- let depth = 0;
- for (let index = 0; index < trimmed.length; index += 1) {
- const char = trimmed[index];
- if (char === "(") depth += 1;
- else if (char === ")") {
- depth -= 1;
- if (depth === 0) return trimmed.slice(index + 1).replace(/^\s+/, "");
- }
- }
- return trimmed;
-}
-
-export function extractXAiAcpSubagentEndNotice(text: string): XAiAcpSubagentEndNotice | undefined {
- const match = text.match(new RegExp(`Background subagent\\s+["']?(${XAI_UUID_RE})["']?`, "i"));
- if (match?.[1] === undefined) return undefined;
- // Skip the parenthesized descriptor (subagent type and title) so its words
- // cannot masquerade as the outcome verb. Balance parens so nested titles
- // like `(general-purpose: "Check (backend) failed")` do not leak.
- const tail = stripLeadingBalancedParenGroup(text.slice((match.index ?? 0) + match[0].length));
- if (/completed successfully/i.test(tail)) {
- return { childSessionId: match[1], status: "completed" };
- }
- if (/\b(failed|errored|error|crashed|cancelled|canceled|interrupted|timed out)\b/i.test(tail)) {
- return { childSessionId: match[1], status: "failed" };
- }
- if (/\b(completed|finished|ended)\b/i.test(tail)) {
- return { childSessionId: match[1], status: "completed" };
- }
- return undefined;
-}
-
-function taskIdsFromGetOutputTool(
- toolCall: AcpToolCallState,
- rawInput: Record | undefined,
-): ReadonlyArray {
- const ids: string[] = [];
- const push = (value: unknown) => {
- const text = nonEmptyString(value);
- if (text !== undefined && new RegExp(`^${XAI_UUID_RE}$`, "i").test(text)) {
- ids.push(text);
- }
- };
- if (Array.isArray(rawInput?.task_ids)) {
- for (const entry of rawInput.task_ids) push(entry);
- }
- push(rawInput?.task_id);
- push(rawInput?.taskId);
- const resultId = resultTaskIdFromGetOutputTool(toolCall, xAiToolOutputText(toolCall));
- if (resultId !== undefined) ids.push(resultId);
- return [...new Set(ids)];
-}
-
-/** Identity of the task the get_output envelope actually reports (not the request list). */
-function resultTaskIdFromGetOutputTool(
- toolCall: AcpToolCallState,
- output: string | undefined,
-): string | undefined {
- const rawOutput = unknownRecord(toolCall.data.rawOutput);
- const result = unknownRecord(rawOutput?.Result) ?? unknownRecord(rawOutput?.result);
- const structured =
- nonEmptyString(result?.task_id) ??
- nonEmptyString(result?.taskId) ??
- nonEmptyString(rawOutput?.task_id) ??
- nonEmptyString(rawOutput?.taskId);
- if (structured !== undefined && new RegExp(`^${XAI_UUID_RE}$`, "i").test(structured)) {
- return structured;
- }
- if (output === undefined) return undefined;
- const taskHeader = firstUuidMatch(
- output,
- new RegExp(`===\\s*Task\\s+(${XAI_UUID_RE})\\s*===`, "i"),
- );
- return taskHeader;
-}
-
-function resultFromGetOutputTool(
- toolCall: AcpToolCallState,
- output: string | undefined,
-): string | null {
- const rawOutput = unknownRecord(toolCall.data.rawOutput);
- const result = unknownRecord(rawOutput?.Result) ?? unknownRecord(rawOutput?.result);
- const structured =
- nonEmptyString(result?.output) ??
- nonEmptyString(result?.text) ??
- decodeByteText(result?.output);
- if (structured !== undefined) {
- // Drop trailing machine meta blocks when present.
- const cleaned = structured
- .replace(/[\s\S]*?<\/subagent_meta>/gi, "")
- .replace(/[\s\S]*?<\/subagent_result>/gi, "")
- .trim();
- if (cleaned.length > 0) return cleaned;
- return null;
- }
- if (output === undefined) return null;
- const marker = output.match(/=== Output ===\s*([\s\S]*)$/i);
- const body = (marker?.[1] ?? output).trim();
- return body.length > 0 ? body : null;
-}
-
-function statusFromGetOutputTool(
- toolCall: AcpToolCallState,
- output: string | undefined,
-): "running" | "completed" | "failed" {
- if (toolCall.status === "failed") return "failed";
- const rawOutput = unknownRecord(toolCall.data.rawOutput);
- const result = unknownRecord(rawOutput?.Result) ?? unknownRecord(rawOutput?.result);
- const structuredStatus = nonEmptyString(result?.status)?.toLowerCase();
- if (structuredStatus === "completed" || structuredStatus === "success") return "completed";
- if (structuredStatus === "failed" || structuredStatus === "error") return "failed";
- if (structuredStatus === "running" || structuredStatus === "pending") return "running";
- if (typeof result?.exit_code === "number") {
- return result.exit_code === 0 ? "completed" : "failed";
- }
- if (output !== undefined) {
- if (/Status:\s*failed/i.test(output) || /Exit Code:\s*-?(?!0\b)\d+/i.test(output)) {
- return "failed";
- }
- if (/Status:\s*completed/i.test(output) || /Status:\s*success/i.test(output)) {
- return "completed";
- }
- if (/Status:\s*running/i.test(output) || /Status:\s*pending/i.test(output)) {
- return "running";
- }
- }
- // A completed get_output tool call only means the poll RPC returned. Without
- // a task-level terminal status or exit code, the requested task is still
- // running and must remain registered for subsequent polling.
- return "running";
-}
-
-/**
- * Recognizes Grok Task / spawn_subagent envelopes and get_command hydration
- * without teaching the generic ACP adapter about xAI tool names.
- *
- * Current Grok CLI returns spawn_subagent immediately with a background ACK
- * (`subagent_id: ...`). That must stay `running` until child work finishes or
- * get_command_or_subagent_output reports a terminal status.
- */
-export function extractXAiAcpSubagentUpdate(
- toolCall: AcpToolCallState,
-): XAiAcpSubagentUpdate | undefined {
- const rawInput = unknownRecord(toolCall.data.rawInput);
- const output = xAiToolOutputText(toolCall);
-
- if (isXAiGetSubagentOutputTool(toolCall, rawInput)) {
- const taskIds = taskIdsFromGetOutputTool(toolCall, rawInput);
- // Prefer the completed task identity from the result/header over the first
- // requested task_ids entry (one call can poll multiple task_ids).
- const childSessionId =
- resultTaskIdFromGetOutputTool(toolCall, output) ??
- taskIds[0] ??
- extractXAiChildSessionId(output);
- if (childSessionId === null) return undefined;
- // Prefer the durable subagent row as the completion surface (Claude/Codex
- // style). Suppress the noisy TaskOutput tool card; monitor hydration is
- // handled separately via extractBackgroundTaskCompletion.
- return {
- nativeTaskId: toolCall.toolCallId,
- prompt: "",
- title: null,
- model: null,
- status: statusFromGetOutputTool(toolCall, output),
- childSessionId,
- result: resultFromGetOutputTool(toolCall, output),
- suppressNormalTool: true,
- };
- }
-
- if (!isXAiSpawnOrTaskTool(toolCall, rawInput)) return undefined;
-
- const childSessionId = extractXAiChildSessionId(output);
- const asyncSpawnAck = isXAiAsyncSpawnAck(output);
- const legacyResult =
- output
- ?.replace(new RegExp(`(?:^|\\n)\\s*Agent ID:\\s*${XAI_UUID_RE}[^\\n]*(?:\\n|$)`, "gi"), "\n")
- .replace(
- new RegExp(`(?:^|\\n)\\s*subagent_id:\\s*${XAI_UUID_RE}[^\\n]*(?:\\n|$)`, "gi"),
- "\n",
- )
- .trim() || null;
-
- let status: "running" | "completed" | "failed";
- if (toolCall.status === "failed") {
- status = "failed";
- } else if (asyncSpawnAck) {
- // Spawn RPC completed, but the child is still running.
- status = "running";
- } else if (toolCall.status === "completed") {
- status = "completed";
- } else {
- status = "running";
- }
-
- return {
- nativeTaskId: toolCall.toolCallId,
- prompt: nonEmptyString(rawInput?.prompt) ?? "",
- title: nonEmptyString(rawInput?.description) ?? null,
- model: nonEmptyString(rawInput?.model) ?? null,
- status,
- childSessionId,
- result: asyncSpawnAck ? null : legacyResult,
- suppressNormalTool: true,
- };
-}
-
-const XAiAskUserQuestionOption = Schema.Struct({
- label: Schema.String,
- description: Schema.optional(Schema.String),
- preview: Schema.optional(Schema.String),
- id: Schema.optional(Schema.String),
-});
-
-const XAiAskUserQuestion = Schema.Struct({
- id: Schema.optional(Schema.String),
- question: Schema.String,
- options: Schema.Array(XAiAskUserQuestionOption),
- multiSelect: Schema.optional(Schema.NullOr(Schema.Boolean)),
-});
-
-const XAiAskUserQuestionParams = Schema.Struct({
- sessionId: Schema.String,
- toolCallId: Schema.String,
- questions: Schema.Array(XAiAskUserQuestion),
- mode: Schema.Literals(["default", "plan"]),
-});
-
-const XAiWrappedAskUserQuestionParams = Schema.Struct({
- method: Schema.Literals(["x.ai/ask_user_question", "_x.ai/ask_user_question"]),
- params: XAiAskUserQuestionParams,
-});
-
-export const XAiAskUserQuestionRequest = Schema.Union([
- XAiAskUserQuestionParams,
- XAiWrappedAskUserQuestionParams,
-]);
-
-type XAiAskUserQuestionRequestParams = typeof XAiAskUserQuestionParams.Type;
-type XAiAskUserQuestionRequest = typeof XAiAskUserQuestionRequest.Type;
-
-// ---------------------------------------------------------------------------
-// x.ai/exit_plan_mode — plan approval gate (mirrors Grok Build TUI plan window)
-// ---------------------------------------------------------------------------
-
-const XAiExitPlanModeParams = Schema.Struct({
- sessionId: Schema.String,
- toolCallId: Schema.String,
- planContent: Schema.optional(Schema.NullOr(Schema.String)),
-});
-
-const XAiWrappedExitPlanModeParams = Schema.Struct({
- method: Schema.Literals(["x.ai/exit_plan_mode", "_x.ai/exit_plan_mode"]),
- params: XAiExitPlanModeParams,
-});
-
-export const XAiExitPlanModeRequest = Schema.Union([
- XAiExitPlanModeParams,
- XAiWrappedExitPlanModeParams,
-]);
-
-type XAiExitPlanModeRequestParams = typeof XAiExitPlanModeParams.Type;
-type XAiExitPlanModeRequest = typeof XAiExitPlanModeRequest.Type;
-
-function unwrapExitPlanModeParams(params: XAiExitPlanModeRequest): XAiExitPlanModeRequestParams {
- return "params" in params ? params.params : params;
-}
-
-/** Empty-state copy when Grok exits plan mode without a plan file. */
-export const XAI_EMPTY_PLAN_MARKDOWN =
- "# No plan written yet\n\n(The agent exited plan mode without writing a plan.)";
-
-export function extractXAiExitPlanMarkdown(
- params: XAiExitPlanModeRequest,
- fallback?: string | null,
-): string {
- const content = unwrapExitPlanModeParams(params).planContent;
- const fromRequest = typeof content === "string" ? trimmed(content) : undefined;
- if (fromRequest) {
- return fromRequest;
- }
- const fromFallback = fallback?.trim();
- if (fromFallback && fromFallback.length > 0) {
- return fromFallback;
- }
- return XAI_EMPTY_PLAN_MARKDOWN;
-}
-
-export type XAiExitPlanModeOutcome = "approved" | "abandoned" | "request_changes";
-
-export interface XAiExitPlanModeResponse {
- readonly outcome: XAiExitPlanModeOutcome;
- readonly feedback?: string;
-}
-
-/**
- * Client captured the plan for T3's proposed-plan card. Abandon the native
- * Grok plan-approval gate so the turn unblocks; the user implements via T3 UI.
- */
-export function makeXAiExitPlanModeCapturedResponse(feedback?: string): XAiExitPlanModeResponse {
- return {
- outcome: "abandoned",
- feedback:
- feedback ??
- "The client captured your proposed plan. Stop here and wait for the user's feedback or implementation request in a later turn.",
- };
-}
-
-function normalizeFsPath(value: string): string {
- return value.trim().replace(/\\/g, "/").replace(/\/+$/, "");
-}
-
-function pathHasTraversalSegment(normalized: string): boolean {
- return normalized.split("/").includes("..");
-}
-
-function addGrokSessionPrefix(
- prefixes: Set,
- homeOrRoot: string,
- nestedGrokDir: boolean,
-): void {
- const root = normalizeFsPath(homeOrRoot);
- if (!root) {
- return;
- }
- prefixes.add(nestedGrokDir ? `${root}/.grok/sessions/` : `${root}/sessions/`);
-}
-
-/** Injected host bits so these helpers stay off `process.platform` / `process.env`. */
-export interface GrokPlanPathHost {
- readonly platform: NodeJS.Platform;
- readonly environment: NodeJS.ProcessEnv;
-}
-
-function grokPlanSessionPrefixes(environment: NodeJS.ProcessEnv): ReadonlySet {
- const prefixes = new Set();
- addGrokSessionPrefix(prefixes, NodeOS.homedir(), true);
- addGrokSessionPrefix(prefixes, "~", true);
- addGrokSessionPrefix(prefixes, environment.HOME ?? "", true);
- addGrokSessionPrefix(prefixes, environment.USERPROFILE ?? "", true);
- // ACP mock and isolated Grok spawns use a HOME that is not the server process home.
- addGrokSessionPrefix(prefixes, "/tmp/mock-home", true);
- const grokHome = environment.GROK_HOME ?? "";
- addGrokSessionPrefix(prefixes, grokHome, false);
- addGrokSessionPrefix(prefixes, grokHome, true);
- return prefixes;
-}
-
-const CANONICAL_HOME_GROK_SESSION_PATH =
- /^(?:\/home\/[^/]+|\/Users\/[^/]+|[a-zA-Z]:\/Users\/[^/]+)\/\.grok\/sessions\/(?:[^/]+\/)+plan\.md$/;
-const CASE_INSENSITIVE_CANONICAL_HOME_GROK_SESSION_PATH = new RegExp(
- CANONICAL_HOME_GROK_SESSION_PATH.source,
- "i",
-);
-
-/**
- * True when a path is Grok's session plan file under a Grok home
- * (`~/.grok/sessions/.../plan.md`, `$HOME/.grok/sessions/...`, or `$GROK_HOME/sessions/...`).
- * Deliberately does not match workspace files named `plan.md` (e.g. docs/plan.md
- * or a repo-local `.grok/sessions/.../plan.md`).
- */
-export function isGrokPlanMarkdownPath(
- path: string | undefined | null,
- host: GrokPlanPathHost,
-): boolean {
- if (typeof path !== "string") {
- return false;
- }
- const normalized = path.trim().replace(/\\/g, "/");
- const win32 = host.platform === "win32";
- const haystack = win32 ? normalized.toLowerCase() : normalized;
- if (
- normalized.length === 0 ||
- !haystack.endsWith("/plan.md") ||
- pathHasTraversalSegment(normalized)
- ) {
- return false;
- }
- for (const prefix of grokPlanSessionPrefixes(host.environment)) {
- const needle = win32 ? prefix.toLowerCase() : prefix;
- if (!haystack.startsWith(needle)) {
- continue;
- }
- const rest = haystack.slice(needle.length);
- // Session layout: /.grok/sessions///plan.md
- if (rest !== "plan.md" && rest.endsWith("plan.md")) {
- return true;
- }
- }
- return (
- win32 ? CASE_INSENSITIVE_CANONICAL_HOME_GROK_SESSION_PATH : CANONICAL_HOME_GROK_SESSION_PATH
- ).test(haystack);
-}
-
-/**
- * Extract plan markdown from a Grok write/edit tool call targeting plan.md.
- * Used so T3 can show the plan while plan mode is still active (before exit).
- */
-export function extractGrokPlanMarkdownFromToolCallData(
- data: Record | undefined,
- host: GrokPlanPathHost,
-): string | undefined {
- if (!data) {
- return undefined;
- }
-
- let sawPlanWrite = false;
- const takePlanText = (
- value: string | undefined,
- filePath: string | undefined,
- ): string | undefined => {
- if (!isGrokPlanMarkdownPath(filePath, host) || value === undefined) {
- return undefined;
- }
- sawPlanWrite = true;
- const trimmed = value.trim();
- return trimmed.length > 0 ? trimmed : undefined;
- };
-
- const rawInput = data.rawInput;
- if (isRecord(rawInput)) {
- const filePath =
- (typeof rawInput.file_path === "string" ? rawInput.file_path : undefined) ??
- (typeof rawInput.path === "string" ? rawInput.path : undefined);
- const content = typeof rawInput.content === "string" ? rawInput.content : undefined;
- const fromRaw = takePlanText(content, filePath);
- if (fromRaw !== undefined) {
- return fromRaw;
- }
- }
-
- const content = data.content;
- if (Array.isArray(content)) {
- for (const block of content) {
- if (!isRecord(block) || block.type !== "diff") {
- continue;
- }
- const path = typeof block.path === "string" ? block.path : undefined;
- const newText = typeof block.newText === "string" ? block.newText : undefined;
- const fromDiff = takePlanText(newText, path);
- if (fromDiff !== undefined) {
- return fromDiff;
- }
- }
- }
-
- return sawPlanWrite ? "" : undefined;
-}
-
-function isRecord(value: unknown): value is Record {
- return typeof value === "object" && value !== null && !Array.isArray(value);
-}
-
-function trimmed(value: string | undefined): string | undefined {
- const text = value?.trim();
- return text && text.length > 0 ? text : undefined;
-}
-
-function unwrapAskUserQuestionParams(
- params: XAiAskUserQuestionRequest,
-): XAiAskUserQuestionRequestParams {
- return "params" in params ? params.params : params;
-}
-
-export function extractXAiAskUserQuestions(
- params: XAiAskUserQuestionRequest,
-): ReadonlyArray {
- return unwrapAskUserQuestionParams(params).questions.map((question) => ({
- id: question.id ?? question.question,
- header: "Question",
- question: question.question,
- multiSelect: question.multiSelect === true,
- options:
- question.options.length > 0
- ? question.options.map((option) => ({
- label: option.label,
- description: option.description ?? option.label,
- }))
- : [{ label: "OK", description: "Continue" }],
- }));
-}
-
-export function extractXAiAskUserQuestionIdentity(params: XAiAskUserQuestionRequest): {
- readonly sessionId: string;
- readonly toolCallId: string;
-} {
- const unwrapped = unwrapAskUserQuestionParams(params);
- return {
- sessionId: unwrapped.sessionId,
- toolCallId: unwrapped.toolCallId,
- };
-}
-
-interface XAiAskUserQuestionAnnotation {
- readonly preview?: string;
- readonly notes?: string;
-}
-
-interface XAiAskUserQuestionAcceptedResponse {
- readonly outcome: "accepted";
- readonly answers: Record>;
- readonly annotations?: Record;
-}
-
-interface XAiAskUserQuestionCancelledResponse {
- readonly outcome: "cancelled";
-}
-
-interface NormalizedXAiAnswer {
- readonly questionText: string;
- readonly selectedLabels: ReadonlyArray;
- readonly annotation?: XAiAskUserQuestionAnnotation;
-}
-
-function answerValues(answer: unknown): ReadonlyArray {
- if (Array.isArray(answer)) {
- return answer.flatMap((entry) => {
- const text = typeof entry === "string" ? trimmed(entry) : undefined;
- return text ? [text] : [];
- });
- }
- const text = typeof answer === "string" ? trimmed(answer) : undefined;
- return text ? [text] : [];
-}
-
-function normalizeAnswerForXAi(
- question: XAiAskUserQuestionRequestParams["questions"][number],
- answer: unknown,
-): NormalizedXAiAnswer | undefined {
- const values = answerValues(answer);
- if (values.length === 0) {
- return undefined;
- }
-
- const optionByLabel = new Map(question.options.map((option) => [option.label, option]));
- const resolvedValues = values.map((value) => ({
- value,
- option: optionByLabel.get(value),
- }));
- const selectedLabels = resolvedValues.flatMap(({ option }) => (option ? [option.label] : []));
- const notes = resolvedValues.flatMap(({ option, value }) => (option ? [] : [value]));
- const preview =
- question.multiSelect === true
- ? undefined
- : resolvedValues.map(({ option }) => trimmed(option?.preview)).find((value) => value);
-
- const annotation =
- preview || notes.length > 0
- ? {
- ...(preview ? { preview } : {}),
- ...(notes.length > 0 ? { notes: notes.join("\n") } : {}),
- }
- : undefined;
-
- return {
- questionText: question.question,
- selectedLabels: selectedLabels.length > 0 ? selectedLabels : ["Other"],
- ...(annotation ? { annotation } : {}),
- };
-}
-
-function findQuestionAnswer(
- answers: ProviderUserInputAnswers,
- question: XAiAskUserQuestionRequestParams["questions"][number],
-): unknown {
- const key = question.id ?? question.question;
- return answers[key] ?? answers[question.question];
-}
-
-export function makeXAiAskUserQuestionResponse(
- params: XAiAskUserQuestionRequest,
- answers: ProviderUserInputAnswers,
-): XAiAskUserQuestionAcceptedResponse {
- const questions = unwrapAskUserQuestionParams(params).questions;
- const normalized = questions.flatMap((question) => {
- const entry = normalizeAnswerForXAi(question, findQuestionAnswer(answers, question));
- return entry ? [entry] : [];
- });
- const annotations = Object.fromEntries(
- normalized.flatMap((entry) =>
- entry.annotation ? [[entry.questionText, entry.annotation] as const] : [],
- ),
- );
-
- return {
- outcome: "accepted",
- answers: Object.fromEntries(
- normalized.map((entry) => [entry.questionText, entry.selectedLabels]),
- ),
- ...(Object.keys(annotations).length > 0 ? { annotations } : {}),
- };
-}
-
-export function makeXAiAskUserQuestionCancelledResponse(): XAiAskUserQuestionCancelledResponse {
- return { outcome: "cancelled" };
-}
-
-function promptIdFromResponse(response: EffectAcpSchema.PromptResponse): string | undefined {
- const meta = response._meta;
- if (meta === null || typeof meta !== "object") {
- return undefined;
- }
- const promptId = meta.promptId ?? meta.requestId;
- return typeof promptId === "string" && promptId.length > 0 ? promptId : undefined;
-}
-
-function normalizeXAiStopReason(value: string | undefined): EffectAcpSchema.StopReason {
- switch (value) {
- case "cancelled":
- case "end_turn":
- case "max_tokens":
- case "max_turn_requests":
- case "refusal":
- return value;
- default:
- return "end_turn";
- }
-}
-
-function promptResponseFromXAi(
- notification: XAiPromptCompleteNotification,
-): EffectAcpSchema.PromptResponse {
- const stopReason = normalizeXAiStopReason(notification.stopReason);
- // Values originate from a decoded JSON-RPC payload, so they are JSON even
- // though the x.ai schema types agentResult as unknown.
- const meta: Record = {
- sessionId: notification.sessionId,
- };
- if (notification.stopReason === undefined) {
- meta[xAiStopReasonMissingMetaKey] = true;
- }
- if (notification.promptId !== undefined) {
- meta.promptId = notification.promptId;
- meta.requestId = notification.promptId;
- }
- if (notification.agentResult !== undefined) {
- meta.agentResult = notification.agentResult as Schema.Json;
- }
- return {
- stopReason,
- _meta: meta,
- };
-}
-
-/**
- * Grok settles a failed prompt with `stopReason: "error"` and the provider's
- * message in `agentResult` before its `session/prompt` RPC error arrives. The
- * completion wins the race, so it must carry the failure itself.
- */
-function xAiPromptFailure(
- notification: XAiPromptCompleteNotification,
-): EffectAcpErrors.AcpRequestError | null {
- if (notification.stopReason === "rate_limit") {
- return new EffectAcpErrors.AcpRequestError({
- code: xAiRateLimitedErrorCode,
- errorMessage: "Grok usage limit reached. Try again later.",
- });
- }
- if (notification.stopReason === "error") {
- // Grok's raw result is unbounded provider text: keep it only as the cause.
- const agentResult = nonEmptyString(notification.agentResult);
- return new EffectAcpErrors.AcpRequestError({
- code: -32603,
- errorMessage: "Grok ended the turn with an error.",
- operation: "receive-response",
- ...(agentResult === undefined ? {} : { cause: new XAiPromptFailureText(agentResult) }),
- });
- }
- return null;
-}
-
-/**
- * Grok's own text for a failed prompt. A plain Error rather than a schema
- * error so the unbounded provider text never becomes a structured attribute;
- * it is read back only at the presentation boundary.
- */
-export class XAiPromptFailureText extends Error {}
-
-const registerXAiPromptCompletionFallback = (
- pendingRef: Ref.Ref>,
- sessionId: string,
- promptId: string,
-) =>
- Deferred.make().pipe(
- Effect.tap((deferred) =>
- Ref.update(pendingRef, (pending) => [...pending, { sessionId, promptId, deferred }]),
- ),
- Effect.map((deferred) => ({ deferred, promptId })),
- );
-
-const unregisterXAiPromptCompletionFallback = (
- pendingRef: Ref.Ref>,
- deferred: Deferred.Deferred,
-) => Ref.update(pendingRef, (pending) => pending.filter((entry) => entry.deferred !== deferred));
-
-const abortPendingPromptCompletions = (
- pendingRef: Ref.Ref>,
- sessionId?: string,
-) =>
- Ref.modify(pendingRef, (pending) => {
- const [toAbort, remaining] = pending.reduce<
- [ReadonlyArray, ReadonlyArray]
- >(
- ([aborting, kept], entry) =>
- sessionId === undefined || entry.sessionId === sessionId
- ? [[...aborting, entry], kept]
- : [aborting, [...kept, entry]],
- [[], []],
- );
- if (toAbort.length === 0) {
- return [Effect.void, pending] as const;
- }
- return [
- Effect.forEach(
- toAbort,
- (entry) =>
- Deferred.succeed(
- entry.deferred,
- promptResponseFromXAi({
- sessionId: entry.sessionId,
- promptId: entry.promptId,
- stopReason: "cancelled",
- agentResult: null,
- }),
- ),
- { concurrency: "unbounded" },
- ).pipe(Effect.asVoid),
- remaining,
- ] as const;
- }).pipe(Effect.flatten);
-
-const resolveXAiPromptCompletionFallback = ({
- pendingRef,
- completedPromptIdsRef,
- notification,
-}: {
- readonly pendingRef: Ref.Ref>;
- readonly completedPromptIdsRef: Ref.Ref>;
- readonly notification: XAiPromptCompleteNotification;
-}) =>
- Ref.get(completedPromptIdsRef).pipe(
- Effect.flatMap((completedPromptIds) => {
- if (
- notification.promptId !== undefined &&
- completedPromptIds.includes(notification.promptId)
- ) {
- return Effect.void;
- }
- return Ref.modify(pendingRef, (pending) => {
- const index =
- notification.promptId !== undefined
- ? pending.findIndex(
- (entry) =>
- entry.sessionId === notification.sessionId &&
- entry.promptId === notification.promptId,
- )
- : (() => {
- const sessionPendingIndexes = pending.flatMap((entry, entryIndex) =>
- entry.sessionId === notification.sessionId ? [entryIndex] : [],
- );
- if (sessionPendingIndexes.length !== 1) {
- return -1;
- }
- return sessionPendingIndexes[0] ?? -1;
- })();
- if (index < 0) {
- return [Effect.void, pending] as const;
- }
- const entry = pending[index];
- if (!entry) {
- return [Effect.void, pending] as const;
- }
- const failure = xAiPromptFailure(notification);
- const settle =
- failure !== null
- ? Deferred.fail(entry.deferred, failure).pipe(Effect.asVoid)
- : Deferred.succeed(entry.deferred, promptResponseFromXAi(notification)).pipe(
- Effect.asVoid,
- );
- return [settle, [...pending.slice(0, index), ...pending.slice(index + 1)]] as const;
- }).pipe(Effect.flatten);
- }),
- );
-
-const rememberCompletedXAiPromptId = (
- completedPromptIdsRef: Ref.Ref>,
- response: EffectAcpSchema.PromptResponse,
- fallbackPromptId: string,
-) => {
- const promptId = promptIdFromResponse(response) ?? fallbackPromptId;
- if (promptId.length === 0) {
- return Effect.void;
- }
- return Ref.update(completedPromptIdsRef, (completedPromptIds) => {
- if (completedPromptIds.includes(promptId)) {
- return completedPromptIds;
- }
- return [...completedPromptIds, promptId].slice(-completedXAiPromptIdLimit);
- });
-};
-
-/**
- * Grok-specific ACP runtime wrapper. Races `session/prompt` against root-matched
- * terminal notifications:
- * - session notification/update + `turn_completed` + matching `prompt_id`
- * - `x.ai/session/prompt_complete` (open-source fire-and-forget signal)
- * - `_x.ai/session/prompt_complete` (released-build alias)
- *
- * Pending entries are keyed by root sessionId + T3-injected promptId, so
- * foreign/child sessions and `task-completed-*` ids do not settle the root turn.
- */
-export const makeXAiPromptCompletionRuntime = Effect.fn("makeXAiPromptCompletionRuntime")(
- function* (runtime: AcpSessionRuntime.AcpSessionRuntime["Service"]) {
- // Grok sends turn completion and other session updates (subagent_finished)
- // on the same extension methods, and the client keeps one handler per
- // method. This wrapper owns those methods: it settles prompts itself, then
- // passes each notification to the handler registered on the wrapped runtime.
- const sessionNotificationHandlers = new Map<
- string,
- (params: unknown) => Effect.Effect
- >();
- let nextPromptFallbackId = 0;
- const allocatePromptFallbackId = Effect.sync(() => {
- nextPromptFallbackId += 1;
- return `t3-xai-prompt-${nextPromptFallbackId}`;
- });
- const pendingXAiPromptCompletionsRef = yield* Ref.make<
- ReadonlyArray
- >([]);
- const completedXAiPromptIdsRef = yield* Ref.make>([]);
-
- const settleFromPromptComplete = (notification: XAiPromptCompleteNotification) =>
- resolveXAiPromptCompletionFallback({
- pendingRef: pendingXAiPromptCompletionsRef,
- completedPromptIdsRef: completedXAiPromptIdsRef,
- notification,
- }).pipe(Effect.catch(() => Effect.void));
-
- yield* Effect.forEach(
- ["x.ai/session/prompt_complete", "_x.ai/session/prompt_complete"] as const,
- (method) =>
- runtime.handleExtNotification(
- method,
- XAiPromptCompleteNotification,
- settleFromPromptComplete,
- ),
- { discard: true },
- );
-
- yield* Effect.forEach(
- xAiSessionNotificationMethods,
- (method) =>
- runtime.handleExtNotification(method, Schema.Unknown, (params) =>
- decodeXAiSessionUpdateNotification(params).pipe(
- Effect.flatMap((notification) => {
- const complete = xAiPromptCompleteFromSessionUpdate(notification);
- return complete === null ? Effect.void : settleFromPromptComplete(complete);
- }),
- Effect.ignore,
- Effect.andThen(
- Effect.suspend(
- () => sessionNotificationHandlers.get(method)?.(params) ?? Effect.void,
- ),
- ),
- ),
- ),
- { discard: true },
- );
-
- return {
- ...runtime,
- handleExtNotification: (method, payload, handler) =>
- xAiSessionNotificationMethods.some((owned) => owned === method)
- ? Effect.sync(() => {
- sessionNotificationHandlers.set(method, (params) =>
- Schema.decodeUnknownEffect(payload)(params).pipe(
- Effect.mapError((error) =>
- EffectAcpErrors.AcpProtocolParseError.fromSchemaError(
- "decode-notification-payload",
- method,
- error,
- ),
- ),
- Effect.flatMap(handler),
- ),
- );
- })
- : runtime.handleExtNotification(method, payload, handler),
- prompt: (payload, promptOptions?) =>
- Effect.gen(function* () {
- const started = yield* runtime.start();
- const promptId = yield* allocatePromptFallbackId;
- const fallback = yield* registerXAiPromptCompletionFallback(
- pendingXAiPromptCompletionsRef,
- started.sessionId,
- promptId,
- );
- const cancelledResponse = promptResponseFromXAi({
- sessionId: started.sessionId,
- promptId: fallback.promptId,
- stopReason: "cancelled",
- agentResult: null,
- });
- const promptRpcFiber = yield* runtime
- .prompt(
- {
- ...payload,
- _meta: {
- ...payload._meta,
- promptId: fallback.promptId,
- requestId: fallback.promptId,
- },
- },
- promptOptions,
- )
- .pipe(Effect.forkChild);
- return yield* Effect.raceFirst(
- Fiber.join(promptRpcFiber).pipe(
- Effect.catchCause((cause) =>
- Cause.hasInterruptsOnly(cause)
- ? Effect.succeed(cancelledResponse)
- : Effect.failCause(cause),
- ),
- ),
- Deferred.await(fallback.deferred),
- ).pipe(
- Effect.tap((response) =>
- rememberCompletedXAiPromptId(completedXAiPromptIdsRef, response, fallback.promptId),
- ),
- Effect.ensuring(
- Effect.gen(function* () {
- yield* Fiber.interrupt(promptRpcFiber).pipe(Effect.ignore);
- yield* unregisterXAiPromptCompletionFallback(
- pendingXAiPromptCompletionsRef,
- fallback.deferred,
- );
- }),
- ),
- );
- }),
- cancel: Effect.gen(function* () {
- // Do not call start here: cancellation must be able to reach the
- // underlying runtime when startup is stalled. Pending entries already
- // carry their session ids, and this runtime owns all of them.
- yield* abortPendingPromptCompletions(pendingXAiPromptCompletionsRef);
- yield* runtime.cancel;
- }),
- } satisfies AcpSessionRuntime.AcpSessionRuntime["Service"];
- },
-);
-
-function promptResponseHasMissingXAiStopReason(response: EffectAcpSchema.PromptResponse): boolean {
- const meta = response._meta;
- return meta !== null && typeof meta === "object" && meta[xAiStopReasonMissingMetaKey] === true;
-}
diff --git a/apps/server/src/provider/acp/XAiBackgroundTasks.test.ts b/apps/server/src/provider/acp/XAiBackgroundTasks.test.ts
deleted file mode 100644
index 93227b260481a58a7b23a8c80135d2551ea661b6..0000000000000000000000000000000000000000
--- a/apps/server/src/provider/acp/XAiBackgroundTasks.test.ts
+++ /dev/null
@@ -1,202 +0,0 @@
-import { describe, expect, it } from "vite-plus/test";
-import { TurnId } from "@t3tools/contracts";
-
-import {
- buildGrokBackgroundTaskEvents,
- type GrokBackgroundTaskRecord,
-} from "./XAiBackgroundTasks.ts";
-
-const turnId = TurnId.make("turn-1");
-const monitor = { type: "Monitor", taskId: "monitor-1", timeoutMs: 60_000 };
-const shell = { type: "BackgroundTaskStarted", task_id: "shell-1", command: "sleep 40" };
-
-function mapper() {
- const tasks = new Map();
- const update = (
- rawOutput: unknown,
- overrides: Partial[0]> = {},
- ) =>
- buildGrokBackgroundTaskEvents({
- tasks,
- toolCallId: "call-1",
- rawInput: { description: "Watch" },
- rawOutput,
- toolCallStatus: "completed",
- turnId,
- ...overrides,
- });
- return { tasks, update };
-}
-
-describe("Grok background tasks", () => {
- it.each([
- [monitor, "monitor-1", "monitor", "Watch"],
- [shell, "shell-1", "shell", "sleep 40"],
- ] as const)("starts and deduplicates %j", (output, id, taskType, description) => {
- const { tasks, update } = mapper();
- expect(update(output)).toEqual([
- {
- type: "task.started",
- turnId,
- payload: { taskId: id, taskType, description, title: description, toolUseId: "call-1" },
- },
- ]);
- expect(update(output)).toEqual([]);
- expect(tasks.size).toBe(1);
- });
-
- it("accepts automatic backgrounding before the tool becomes terminal", () => {
- const { update } = mapper();
- expect(update(shell, { toolCallStatus: "inProgress" })[0]?.type).toBe("task.started");
- expect(update(monitor, { toolCallStatus: "inProgress" })).toEqual([]);
- expect(update(monitor, { toolCallStatus: "failed" })).toEqual([]);
- });
-
- it.each([
- ["running", null, "task.progress", undefined],
- ["pending", null, "task.progress", undefined],
- ["completed", 0, "task.completed", "completed"],
- ["success", 0, "task.completed", "completed"],
- ["succeeded", 0, "task.completed", "completed"],
- ["failed", 1, "task.completed", "failed"],
- ["error", 1, "task.completed", "failed"],
- ["stopped", 137, "task.completed", "stopped"],
- ["killed", 137, "task.completed", "stopped"],
- ["cancelled", 137, "task.completed", "stopped"],
- [undefined, 0, "task.completed", "completed"],
- [undefined, 1, "task.completed", "failed"],
- ])("maps poll status %s / exit %s", (status, exit_code, type, expectedStatus) => {
- const { tasks, update } = mapper();
- update(monitor);
- const events = update({
- type: "TaskOutput",
- Result: {
- task_id: "monitor-1",
- command: "[monitor:Watch]",
- status,
- exit_code,
- output: "\n result\nmore",
- },
- });
- expect(events).toEqual([
- {
- type,
- turnId,
- payload: {
- taskId: "monitor-1",
- taskType: "monitor",
- description: "Watch",
- title: "Watch",
- toolUseId: "call-1",
- summary: "result",
- ...(expectedStatus ? { status: expectedStatus } : {}),
- },
- },
- ]);
- expect(tasks.size).toBe(type === "task.progress" ? 1 : 0);
- });
-
- it.each([undefined, TurnId.make("turn-2")])(
- "does not attribute old tasks to a later turn: %s",
- (laterTurnId) => {
- const { tasks, update } = mapper();
- update(shell);
- const events = update(
- {
- type: "TaskOutput",
- Result: { task_id: "shell-1", command: "sleep 40", status: "completed" },
- },
- { turnId: laterTurnId },
- );
- expect(events).toHaveLength(1);
- expect(events[0]?.turnId).toBeUndefined();
- expect(tasks.size).toBe(0);
- },
- );
-
- it("starts unknown poll tasks before progress or completion and ignores subagents", () => {
- const { tasks, update } = mapper();
- const events = update({
- type: "TaskOutput",
- MultiResult: {
- results: [
- { task_id: "shell-1", command: "sleep 40", status: "running" },
- { task_id: "monitor-1", command: "[monitor] Watch", status: "completed" },
- { task_id: "agent-1", command: "[subagent:executor] work", status: "running" },
- ],
- },
- });
- expect(events.map(({ type }) => type)).toEqual([
- "task.started",
- "task.progress",
- "task.started",
- "task.completed",
- ]);
- expect(events.map(({ payload }) => payload.taskType)).toEqual([
- "shell",
- "shell",
- "monitor",
- "monitor",
- ]);
- expect([...tasks.keys()]).toEqual(["shell-1"]);
- expect(events.map((event) => event.turnId)).toEqual([
- undefined,
- undefined,
- undefined,
- undefined,
- ]);
- });
-
- it("retires only successfully killed tasks, including mixed results", () => {
- const { tasks, update } = mapper();
- update(shell);
- update(monitor);
- const result = { type: "KillTask", Result: { task_id: "shell-1", outcome: "killed" } };
- expect(update(result, { toolCallStatus: "failed" })).toEqual([]);
- expect(tasks.size).toBe(2);
- const events = update({
- type: "KillTask",
- MultiResult: {
- results: [
- result.Result,
- { task_id: "monitor-1", outcome: "error" },
- { task_id: "unknown", outcome: "killed" },
- ],
- },
- });
- expect(events).toEqual([
- {
- type: "task.completed",
- turnId,
- payload: {
- taskId: "shell-1",
- taskType: "shell",
- description: "sleep 40",
- title: "sleep 40",
- toolUseId: "call-1",
- status: "stopped",
- },
- },
- ]);
- expect([...tasks.keys()]).toEqual(["monitor-1"]);
- });
-
- it.each([
- null,
- [],
- {},
- { type: "Text", text: "subagent_id: fake\ntype: executor\ndescription: fake" },
- { type: "Monitor", taskId: " " },
- { type: "BackgroundTaskStarted", task_id: "shell-1" },
- {
- type: "TaskOutput",
- MultiResult: {
- results: [null, {}, { task_id: "task", command: "sleep 40", exit_code: Infinity }],
- },
- },
- ])("ignores malformed or unrelated outputs: %j", (output) => {
- const { tasks, update } = mapper();
- expect(update(output)).toEqual([]);
- expect(tasks.size).toBe(0);
- });
-});
diff --git a/apps/server/src/provider/acp/XAiBackgroundTasks.ts b/apps/server/src/provider/acp/XAiBackgroundTasks.ts
deleted file mode 100644
index beca3472255ab25e3dc689d4f380ffa0b25b9791..0000000000000000000000000000000000000000
--- a/apps/server/src/provider/acp/XAiBackgroundTasks.ts
+++ /dev/null
@@ -1,156 +0,0 @@
-import {
- RuntimeTaskId,
- type ProviderRuntimeTaskStartedEvent,
- type ProviderRuntimeTaskProgressEvent,
- type ProviderRuntimeTaskCompletedEvent,
- type TurnId,
-} from "@t3tools/contracts";
-
-type TaskEvent =
- | Pick
- | Pick
- | Pick;
-
-export interface GrokBackgroundTaskRecord {
- readonly payload: {
- readonly taskId: RuntimeTaskId;
- readonly taskType: "monitor" | "shell";
- readonly description: string;
- readonly title: string;
- readonly toolUseId?: string;
- };
- readonly turnId: TurnId | undefined;
-}
-
-function record(value: unknown): Record {
- return typeof value === "object" && value !== null && !Array.isArray(value)
- ? (value as Record)
- : {};
-}
-
-function text(value: unknown): string | undefined {
- return typeof value === "string" ? value.trim() || undefined : undefined;
-}
-
-function lifecycle(status: unknown, exitCode: unknown) {
- switch (text(status)?.toLowerCase()) {
- case "pending":
- case "running":
- return "running";
- case "completed":
- case "success":
- case "succeeded":
- return "completed";
- case "failed":
- case "error":
- return "failed";
- case "stopped":
- case "killed":
- case "cancelled":
- return "stopped";
- default:
- return typeof exitCode === "number" && Number.isFinite(exitCode)
- ? exitCode === 0
- ? "completed"
- : "failed"
- : undefined;
- }
-}
-
-/** Map Grok's discriminated tool results, including notifications after the turn ends. */
-export function buildGrokBackgroundTaskEvents(input: {
- readonly tasks: Map;
- readonly toolCallId: string;
- readonly rawInput: unknown;
- readonly rawOutput: unknown;
- readonly toolCallStatus: string | undefined;
- readonly turnId?: TurnId | undefined;
-}): TaskEvent[] {
- const { tasks, toolCallId, toolCallStatus, turnId } = input;
- const output = record(input.rawOutput);
- const events: TaskEvent[] = [];
- if (
- toolCallStatus !== "completed" &&
- toolCallStatus !== "failed" &&
- output.type !== "BackgroundTaskStarted"
- ) {
- return events;
- }
- const attribution = (task: GrokBackgroundTaskRecord) =>
- task.turnId !== undefined && task.turnId === turnId ? { turnId } : {};
- const start = (
- id: string,
- taskType: "monitor" | "shell",
- description: string,
- toolUseId?: string,
- ) => {
- const known = tasks.get(id);
- if (known) return known;
- const task: GrokBackgroundTaskRecord = {
- payload: {
- taskId: RuntimeTaskId.make(id),
- taskType,
- description,
- title: description,
- ...(toolUseId ? { toolUseId } : {}),
- },
- // Polls can rediscover older tasks without establishing their originating turn.
- turnId: toolUseId ? turnId : undefined,
- };
- tasks.set(id, task);
- events.push({ type: "task.started", payload: task.payload, ...attribution(task) });
- return task;
- };
- const complete = (
- task: GrokBackgroundTaskRecord,
- status: "completed" | "failed" | "stopped",
- summary?: string,
- ) => {
- tasks.delete(task.payload.taskId);
- events.push({
- type: "task.completed",
- payload: { ...task.payload, status, ...(summary ? { summary } : {}) },
- ...attribution(task),
- });
- };
-
- if (output.type === "Monitor" && toolCallStatus === "completed") {
- const id = text(output.taskId);
- if (id) start(id, "monitor", text(record(input.rawInput).description) ?? "Monitor", toolCallId);
- } else if (output.type === "BackgroundTaskStarted") {
- const id = text(output.task_id) ?? text(output.taskId);
- const command = text(output.command);
- if (id && command) start(id, "shell", command.split("\n")[0]!.slice(0, 200), toolCallId);
- } else if (output.type === "TaskOutput" || output.type === "KillTask") {
- const results = record(output.MultiResult).results;
- for (const value of Array.isArray(results) ? results : [output.Result]) {
- const result = record(value);
- const id = text(result.task_id);
- if (!id) continue;
- if (output.type === "KillTask") {
- const task = tasks.get(id);
- if (task && toolCallStatus === "completed" && result.outcome === "killed")
- complete(task, "stopped");
- continue;
- }
- const command = text(result.command);
- const status = lifecycle(result.status, result.exit_code);
- if (!command || !status || command.startsWith("[subagent:")) continue;
- const task = start(id, /^\[monitor[:\]]/.test(command) ? "monitor" : "shell", command);
- const summary = text(result.output)
- ?.split("\n")
- .find((line) => line.trim())
- ?.trim();
- if (status === "running") {
- events.push({
- type: "task.progress",
- payload: { ...task.payload, ...(summary ? { summary } : {}) },
- ...attribution(task),
- });
- } else {
- complete(task, status, summary);
- }
- }
- }
- return events;
-}
diff --git a/apps/server/src/provider/builtInDrivers.ts b/apps/server/src/provider/builtInDrivers.ts
index 516e52d0e49ba291cb731f851eb42ebd97606948..1e21b8dc7cc199b0057dbe31975f5828f442495d 100644
--- a/apps/server/src/provider/builtInDrivers.ts
+++ b/apps/server/src/provider/builtInDrivers.ts
@@ -25,7 +25,7 @@ import { AntigravityDriver, type AntigravityDriverEnv } from "./Drivers/Antigrav
import { ClaudeDriver, type ClaudeDriverEnv } from "./Drivers/ClaudeDriver.ts";
import { CodexDriver, type CodexDriverEnv } from "./Drivers/CodexDriver.ts";
import { CursorDriver, type CursorDriverEnv } from "@t3tools/provider-cursor/server";
-import { GrokDriver, type GrokDriverEnv } from "./Drivers/GrokDriver.ts";
+import { GrokDriver, type GrokDriverEnv } from "@t3tools/provider-grok/server";
import { OpenCodeDriver, type OpenCodeDriverEnv } from "@t3tools/provider-opencode/server";
import { MuseDriver, type MuseDriverEnv } from "@t3tools/provider-muse/server";
import { PiDriver, type PiDriverEnv } from "@t3tools/provider-pi/server";
diff --git a/apps/server/src/provider/grokUsageLimits.ts b/apps/server/src/provider/grokUsageLimits.ts
deleted file mode 100644
index f200956b56b17a016411f41dda58c59833183300..0000000000000000000000000000000000000000
--- a/apps/server/src/provider/grokUsageLimits.ts
+++ /dev/null
@@ -1,175 +0,0 @@
-import * as NodeOS from "node:os";
-import type { ServerProviderUsageWindow } from "@t3tools/contracts";
-import * as DateTime from "effect/DateTime";
-import * as Effect from "effect/Effect";
-import * as FileSystem from "effect/FileSystem";
-import * as Option from "effect/Option";
-import * as Path from "effect/Path";
-import * as Schema from "effect/Schema";
-import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http";
-import {
- clampPercent,
- makeUnavailableUsageLimits,
- makeUsageLimits,
-} from "@t3tools/provider-core/server/usageLimits";
-
-const GrokCredentials = Schema.Record(
- Schema.String,
- Schema.Struct({
- key: Schema.optional(Schema.String),
- auth_mode: Schema.optional(Schema.String),
- email: Schema.optional(Schema.String),
- }),
-);
-const decodeCredentials = Schema.decodeEffect(Schema.fromJsonString(GrokCredentials));
-const GrokUsageResponse = Schema.Struct({
- config: Schema.optional(
- Schema.Struct({
- creditUsagePercent: Schema.optional(Schema.Number),
- currentPeriod: Schema.optional(
- Schema.Struct({
- type: Schema.optional(Schema.String),
- end: Schema.optional(Schema.String),
- }),
- ),
- }),
- ),
-});
-
-export function grokUsageResponseToLimits(
- response: typeof GrokUsageResponse.Type,
- checkedAt: string,
-) {
- const usedPercent = response.config?.creditUsagePercent;
- if (usedPercent === undefined || !Number.isFinite(usedPercent)) {
- // A billing read that succeeded but carries no percentage is an account
- // with nothing metered yet, not one that can never report: xAI omits the
- // field entirely (rather than sending 0) until usage registers, then fills
- // it in. Calling that `unsupported` would strand the account — the Limits
- // view drops unsupported entries and deliberately mutes their notice, so a
- // freshly signed-in Grok account would vanish with no explanation until it
- // happened to be used, and `applyUsageLimitsUpdate` would refuse the
- // mid-turn windows that could have recovered it.
- return makeUsageLimits({ checkedAt, windows: [] });
- }
- const period = response.config?.currentPeriod;
- const periodType = period?.type?.replace(/^USAGE_PERIOD_TYPE_/, "");
- const kind = periodType === "WEEKLY" ? "weekly" : periodType === "MONTHLY" ? "monthly" : "other";
- const reset = period?.end ? DateTime.make(period.end) : Option.none();
- const window: ServerProviderUsageWindow = {
- id: "subscription",
- kind,
- label: kind === "weekly" ? "Weekly" : kind === "monthly" ? "Monthly" : "Subscription",
- usedPercent: clampPercent(usedPercent),
- ...(Option.isSome(reset) ? { resetsAt: DateTime.formatIso(reset.value) } : {}),
- };
- return makeUsageLimits({ checkedAt, windows: [window] });
-}
-
-/**
- * The grok.com login the CLI uses by default, or undefined when the CLI is
- * configured to pick another account, endpoint, or an API key.
- */
-const readGrokCredential = Effect.fn("readGrokCredential")(function* (
- environment: NodeJS.ProcessEnv,
-) {
- // T3's ACP adapter explicitly selects API-key auth when this variable is set.
- if (environment.XAI_API_KEY?.trim()) return undefined;
- // Alternate auth deployments can select another scope or account from the same file.
- if (
- [
- "GROK_OIDC_ISSUER",
- "GROK_OIDC_CLIENT_ID",
- "GROK_OAUTH2_ISSUER",
- "GROK_OAUTH2_CLIENT_ID",
- "GROK_OAUTH2_PRINCIPAL_TYPE",
- "GROK_OAUTH2_PRINCIPAL_ID",
- "GROK_AUTH_PROVIDER_COMMAND",
- "GROK_LOCAL_AUTH",
- "GROK_CLI_CHAT_PROXY_BASE_URL",
- "GROK_MODELS_BASE_URL",
- "GROK_CONFIG",
- "GROK_CONFIG_PATH",
- ].some((name) => environment[name]?.trim())
- ) {
- return undefined;
- }
- const fs = yield* FileSystem.FileSystem;
- const path = yield* Path.Path;
- const home =
- environment.GROK_HOME?.trim() ||
- path.join(environment.HOME || environment.USERPROFILE || NodeOS.homedir(), ".grok");
- for (const configPath of [
- path.join(home, "config.toml"),
- path.join(home, "managed_config.toml"),
- path.join(home, "requirements.toml"),
- "/etc/grok/managed_config.toml",
- "/etc/grok/requirements.toml",
- ]) {
- const config = yield* fs.readFileString(configPath).pipe(
- Effect.catchTags({
- PlatformError: (error) =>
- error.reason._tag === "NotFound" ? Effect.succeed("") : Effect.fail(error),
- }),
- );
- // These sections can change the selected account or endpoint. Leave custom deployments to the CLI.
- if (/^\s*(?:\[\[?\s*)?["']?(?:auth|grok_com_config|endpoints)["']?\s*[.\]=]/m.test(config)) {
- return undefined;
- }
- }
- const contents =
- environment.GROK_AUTH?.trim() ||
- (yield* fs.readFileString(path.join(home, "auth.json")).pipe(
- Effect.catchTags({
- PlatformError: (error) =>
- error.reason._tag === "NotFound" ? Effect.succeed("{}") : Effect.fail(error),
- }),
- ));
- const credentials = yield* decodeCredentials(contents);
- // Never pick an arbitrary account from other deployments stored in the same file.
- const credential =
- credentials["https://auth.x.ai::b1a00492-073a-47ea-816f-4c329264a828"] ??
- credentials["https://accounts.x.ai/sign-in"];
- return credential?.auth_mode === "api_key" ? undefined : credential;
-});
-
-/**
- * Reads the default grok.com login once and reports its usage limits along with
- * its email, so the email always names the account whose quota was read.
- */
-export const readGrokAccount = Effect.fn("readGrokAccount")(function* (
- environment: NodeJS.ProcessEnv = process.env,
-) {
- const checkedAt = DateTime.formatIso(yield* DateTime.now);
- const probeFailed = makeUnavailableUsageLimits({
- checkedAt,
- reason: "probeFailed",
- message: "Grok could not read usage limits.",
- });
- const credential = yield* Effect.option(
- readGrokCredential(environment).pipe(Effect.timeout("10 seconds")),
- );
- if (Option.isNone(credential)) return { email: undefined, usageLimits: probeFailed };
- const email = credential.value?.email?.trim() || undefined;
- const token = credential.value?.key?.trim();
- if (!token) {
- return { email, usageLimits: makeUnavailableUsageLimits({ checkedAt, reason: "unsupported" }) };
- }
- // A failed quota request still knows which account it asked about.
- const usageLimits = yield* Effect.gen(function* () {
- const client = yield* HttpClient.HttpClient;
- const response = yield* client.execute(
- HttpClientRequest.get("https://cli-chat-proxy.grok.com/v1/billing?format=credits").pipe(
- HttpClientRequest.bearerToken(token),
- ),
- );
- const body = yield* HttpClientResponse.schemaBodyJson(GrokUsageResponse)(
- yield* HttpClientResponse.filterStatusOk(response),
- );
- return grokUsageResponseToLimits(body, checkedAt);
- }).pipe(
- Effect.timeout("10 seconds"),
- Effect.orElseSucceed(() => probeFailed),
- );
- return { email, usageLimits };
-});
diff --git a/apps/server/src/textGeneration/GrokTextGeneration.test.ts b/apps/server/src/textGeneration/GrokTextGeneration.test.ts
index 8165683f62cf02a8f28884cd82bb5f2b9b1e3744..80f5c236259de564cdc350351d0fb19510442c5c 100644
--- a/apps/server/src/textGeneration/GrokTextGeneration.test.ts
+++ b/apps/server/src/textGeneration/GrokTextGeneration.test.ts
@@ -11,11 +11,12 @@ import * as Layer from "effect/Layer";
import * as Schema from "effect/Schema";
import { createModelSelection } from "@t3tools/shared/model";
import { expect } from "vite-plus/test";
-import { GrokSettings, ProviderInstanceId } from "@t3tools/contracts";
+import { ProviderInstanceId } from "@t3tools/contracts";
+import { GrokSettings } from "@t3tools/provider-grok/settings";
import * as ServerConfig from "../config.ts";
import * as TextGeneration from "./TextGeneration.ts";
-import { makeGrokTextGeneration } from "./GrokTextGeneration.ts";
+import { makeGrokTextGeneration } from "@t3tools/provider-grok/testing";
import { execScriptSource, writeFakeCli } from "@t3tools/provider-testing/fakeCli";
const decodeGrokSettings = Schema.decodeSync(GrokSettings);
diff --git a/apps/server/src/textGeneration/GrokTextGeneration.ts b/apps/server/src/textGeneration/GrokTextGeneration.ts
deleted file mode 100644
index 6e14fa101ee278d244e38be792603b3f99e154f7..0000000000000000000000000000000000000000
--- a/apps/server/src/textGeneration/GrokTextGeneration.ts
+++ /dev/null
@@ -1,130 +0,0 @@
-import * as Crypto from "effect/Crypto";
-import * as Effect from "effect/Effect";
-import * as Option from "effect/Option";
-import * as Ref from "effect/Ref";
-import * as Schema from "effect/Schema";
-import { ChildProcessSpawner } from "effect/process";
-import type * as EffectAcpErrors from "effect-acp/errors";
-
-import { type GrokSettings, TextGenerationError } from "@t3tools/contracts";
-import { getModelSelectionStringOptionValue } from "@t3tools/shared/model";
-
-import * as TextGenerationOperations from "@t3tools/provider-core/server/textGenerationOperations";
-import {
- applyGrokAcpModelSelection,
- currentGrokModelIdFromSessionSetup,
- currentGrokReasoningEffortFromSessionSetup,
- makeGrokAcpRuntime,
- resolveGrokAcpBaseModelId,
-} from "../provider/acp/GrokAcpSupport.ts";
-
-const GROK_TIMEOUT_MS = 180_000;
-
-const isTextGenerationError = Schema.is(TextGenerationError);
-
-export const makeGrokTextGeneration = Effect.fn("makeGrokTextGeneration")(function* (
- grokSettings: GrokSettings,
- environment: NodeJS.ProcessEnv = process.env,
-) {
- const crypto = yield* Crypto.Crypto;
- const commandSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;
-
- const runGrokJson: TextGenerationOperations.Runner = (request) => {
- const { operation, cwd, prompt, modelSelection } = request;
- return Effect.gen(function* () {
- const outputRef = yield* Ref.make("");
- const runtime = yield* makeGrokAcpRuntime({
- grokSettings,
- environment,
- childProcessSpawner: commandSpawner,
- cwd,
- clientInfo: { name: "t3-code-git-text", version: "0.0.0" },
- }).pipe(Effect.provideService(Crypto.Crypto, crypto));
-
- yield* runtime.handleSessionUpdate((notification) => {
- const update = notification.update;
- if (update.sessionUpdate !== "agent_message_chunk") {
- return Effect.void;
- }
- const content = update.content;
- if (content.type !== "text") {
- return Effect.void;
- }
- return Ref.update(outputRef, (current) => current + content.text);
- });
-
- const promptResult = yield* Effect.gen(function* () {
- const resolvedModel = resolveGrokAcpBaseModelId(modelSelection.model);
- const started = yield* runtime.start();
- const requestedReasoningEffort = getModelSelectionStringOptionValue(
- modelSelection,
- "reasoningEffort",
- );
- yield* applyGrokAcpModelSelection({
- runtime,
- currentModelId: currentGrokModelIdFromSessionSetup(started.sessionSetupResult),
- currentReasoningEffort: currentGrokReasoningEffortFromSessionSetup(
- started.sessionSetupResult,
- ),
- requestedModelId: resolvedModel,
- requestedReasoningEffort,
- mapError: (cause) =>
- new TextGenerationError({
- operation,
- detail: "Failed to set Grok ACP base model for text generation.",
- cause,
- }),
- });
- return yield* runtime.prompt({
- prompt: [{ type: "text", text: prompt }],
- });
- }).pipe(
- Effect.timeoutOption(GROK_TIMEOUT_MS),
- Effect.flatMap(
- Option.match({
- onNone: () =>
- Effect.fail(
- new TextGenerationError({ operation, detail: "Grok ACP request timed out." }),
- ),
- onSome: (value) => Effect.succeed(value),
- }),
- ),
- Effect.mapError((cause: EffectAcpErrors.AcpError | TextGenerationError) =>
- isTextGenerationError(cause)
- ? cause
- : new TextGenerationError({
- operation,
- detail: "Grok ACP request failed.",
- cause,
- }),
- ),
- );
-
- const trimmed = (yield* Ref.get(outputRef)).trim();
- if (!trimmed) {
- return yield* new TextGenerationError({
- operation,
- detail:
- promptResult.stopReason === "cancelled"
- ? "Grok ACP request was cancelled."
- : "Grok Agent returned empty output.",
- });
- }
-
- return yield* TextGenerationOperations.decodeJsonReply(request, "Grok Agent", trimmed);
- }).pipe(
- Effect.mapError((cause) =>
- isTextGenerationError(cause)
- ? cause
- : new TextGenerationError({
- operation,
- detail: "Grok ACP text generation failed.",
- cause,
- }),
- ),
- Effect.scoped,
- );
- };
-
- return TextGenerationOperations.fromRunner("GrokTextGeneration", runGrokJson);
-});
diff --git a/apps/web/package.json b/apps/web/package.json
index 64012d9bdab346789d7677e19a99767157eae830..5e9c5e1e97852aaf70f268a537f82773d68d7c04 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -30,6 +30,7 @@
"@t3tools/contracts": "workspace:*",
"@t3tools/provider-core": "workspace:*",
"@t3tools/provider-cursor": "workspace:*",
+ "@t3tools/provider-grok": "workspace:*",
"@t3tools/provider-muse": "workspace:*",
"@t3tools/provider-opencode": "workspace:*",
"@t3tools/provider-pi": "workspace:*",
diff --git a/apps/web/src/components/Icons.tsx b/apps/web/src/components/Icons.tsx
index be008bac333089870dd2640de1099c4155b2f2d1..3e4d53d55b63f2ae584045ba5db8d3e08ad4e86f 100644
--- a/apps/web/src/components/Icons.tsx
+++ b/apps/web/src/components/Icons.tsx
@@ -364,18 +364,6 @@ export const CursorIcon: Icon = ({ className, ...props }) => (
);
-export const GrokIcon: Icon = ({ className, ...props }) => (
-
-);
-
export const TraeIcon: Icon = (props) => (