Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions apps/mobile/src/components/ProviderIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,17 @@ export function ProviderIcon(props: ProviderIconProps) {
);
}

if (props.provider === "grokBuild") {
return (
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
<Path
fill={isDarkMode ? "#F5F5F5" : "#111111"}
d="M5.46 4h2.71L12 9.12 15.83 4h2.71l-5.18 6.91L18.9 20h-2.72L12 14.08 7.82 20H5.1l5.55-9.09L5.46 4Z"
/>
</Svg>
);
}

return (
<Svg width={size} height={size} viewBox="0 0 256 260" fill="none">
<Path
Expand Down
1 change: 1 addition & 0 deletions apps/mobile/src/lib/modelOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ function providerDisplayLabel(provider: {
if (provider.displayName) return provider.displayName;
if (provider.driver === "codex") return "Codex";
if (provider.driver === "claudeAgent") return "Claude";
if (provider.driver === "grokBuild") return "Grok Build";
return provider.instanceId;
}

Expand Down
63 changes: 54 additions & 9 deletions apps/server/scripts/acp-mock-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ const emitGenericToolPlaceholders = process.env.T3_ACP_EMIT_GENERIC_TOOL_PLACEHO
const emitAskQuestion = process.env.T3_ACP_EMIT_ASK_QUESTION === "1";
const failSetConfigOption = process.env.T3_ACP_FAIL_SET_CONFIG_OPTION === "1";
const exitOnSetConfigOption = process.env.T3_ACP_EXIT_ON_SET_CONFIG_OPTION === "1";
const failPrompt = process.env.T3_ACP_FAIL_PROMPT === "1";
const promptDelayMs = Number(process.env.T3_ACP_PROMPT_DELAY_MS ?? "0");
const sessionModelsOnly = process.env.T3_ACP_SESSION_MODELS_ONLY === "1";
const promptResponseText = process.env.T3_ACP_PROMPT_RESPONSE_TEXT;
const sessionId = "mock-session-1";

Expand Down Expand Up @@ -201,6 +204,17 @@ const availableModes: ReadonlyArray<AcpSchema.SessionMode> = [
},
];

function modelsState(): AcpSchema.SessionModelState {
return {
currentModelId,
availableModels: [
{ modelId: "default", name: "Auto" },
{ modelId: "composer-2", name: "Composer 2" },
{ modelId: "grok-build", name: "Grok Build" },
],
};
}

function modeState(): AcpSchema.SessionModeState {
return {
currentModeId,
Expand All @@ -225,11 +239,19 @@ const program = Effect.gen(function* () {
yield* agent.handleAuthenticate(() => Effect.succeed({}));

yield* agent.handleCreateSession(() =>
Effect.succeed({
sessionId,
modes: modeState(),
configOptions: configOptions(),
}),
Effect.succeed(
sessionModelsOnly
? {
sessionId,
modes: modeState(),
models: modelsState(),
}
: {
sessionId,
modes: modeState(),
configOptions: configOptions(),
},
),
);

yield* agent.handleLoadSession((request) =>
Expand All @@ -242,13 +264,27 @@ const program = Effect.gen(function* () {
},
})
.pipe(
Effect.as({
modes: modeState(),
configOptions: configOptions(),
}),
Effect.as(
sessionModelsOnly
? {
modes: modeState(),
models: modelsState(),
}
: {
modes: modeState(),
configOptions: configOptions(),
},
),
),
);

yield* agent.handleSetSessionModel((request) =>
Effect.sync(() => {
currentModelId = request.modelId;
return {};
}),
);

yield* agent.handleSetSessionConfigOption((request) =>
Effect.gen(function* () {
if (exitOnSetConfigOption) {
Expand Down Expand Up @@ -295,6 +331,15 @@ const program = Effect.gen(function* () {
yield* agent.handlePrompt((request) =>
Effect.gen(function* () {
const requestedSessionId = String(request.sessionId ?? sessionId);
if (Number.isFinite(promptDelayMs) && promptDelayMs > 0) {
yield* Effect.sleep(`${promptDelayMs} millis`);
}
if (failPrompt) {
return yield* AcpError.AcpRequestError.internalError("Mock prompt failed", {
method: "session/prompt",
params: request,
});
}

if (emitInterleavedAssistantToolCalls) {
const toolCallId = "tool-call-1";
Expand Down
170 changes: 170 additions & 0 deletions apps/server/src/provider/Drivers/GrokBuildDriver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/**
* GrokBuildDriver — `ProviderDriver` for the Grok Build runtime.
*
* Grok Build exposes an ACP-based CLI over `grok --agent build agent stdio`.
*
* Text generation is supported via the ACP runtime — `makeGrokBuildTextGeneration`
* drives `runtime.prompt` with a structured-output schema and collects the
* assistant's `agent_message_chunk` stream into a single JSON blob.
*
* @module provider/Drivers/GrokBuildDriver
*/
import { GrokBuildSettings, ProviderDriverKind, type ServerProvider } from "@t3tools/contracts";
import * as Duration from "effect/Duration";
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 * as Stream from "effect/Stream";
import { HttpClient } from "effect/unstable/http";
import { ChildProcessSpawner } from "effect/unstable/process";

import { ServerConfig } from "../../config.ts";
import { makeGrokBuildTextGeneration } from "../../textGeneration/GrokBuildTextGeneration.ts";
import { ProviderDriverError } from "../Errors.ts";
import { makeGrokBuildAdapter } from "../Layers/GrokBuildAdapter.ts";
import {
buildInitialGrokBuildProviderSnapshot,
checkGrokBuildProviderStatus,
} from "../Layers/GrokBuildProvider.ts";
import { ProviderEventLoggers } from "../Layers/ProviderEventLoggers.ts";
import { makeManagedServerProvider } from "../makeManagedServerProvider.ts";
import {
defaultProviderContinuationIdentity,
type ProviderDriver,
type ProviderInstance,
} from "../ProviderDriver.ts";
import type { ServerProviderDraft } from "../providerSnapshot.ts";
import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts";
import {
makeProviderMaintenanceCapabilities,
makeStaticProviderMaintenanceResolver,
resolveProviderMaintenanceCapabilitiesEffect,
} from "../providerMaintenance.ts";
import { enrichProviderSnapshotWithVersionAdvisory } from "../providerMaintenance.ts";
const decodeGrokBuildSettings = Schema.decodeSync(GrokBuildSettings);

const DRIVER_KIND = ProviderDriverKind.make("grokBuild");
const SNAPSHOT_REFRESH_INTERVAL = Duration.minutes(5);
const UPDATE = makeStaticProviderMaintenanceResolver(
makeProviderMaintenanceCapabilities({
provider: DRIVER_KIND,
packageName: null,
updateExecutable: "grok",
updateArgs: ["update"],
updateLockKey: "grok-build",
}),
);

export type GrokBuildDriverEnv =
| ChildProcessSpawner.ChildProcessSpawner
| Crypto.Crypto
| FileSystem.FileSystem
| HttpClient.HttpClient
| Path.Path
| ProviderEventLoggers
| ServerConfig;

const withInstanceIdentity =
(input: {
readonly instanceId: ProviderInstance["instanceId"];
readonly displayName: string | undefined;
readonly accentColor: string | undefined;
readonly continuationGroupKey: string;
}) =>
(snapshot: ServerProviderDraft): ServerProvider => ({
...snapshot,
instanceId: input.instanceId,
driver: DRIVER_KIND,
...(input.displayName ? { displayName: input.displayName } : {}),
...(input.accentColor ? { accentColor: input.accentColor } : {}),
continuation: { groupKey: input.continuationGroupKey },
});

export const GrokBuildDriver: ProviderDriver<GrokBuildSettings, GrokBuildDriverEnv> = {
driverKind: DRIVER_KIND,
metadata: {
displayName: "Grok Build",
supportsMultipleInstances: true,
},
configSchema: GrokBuildSettings,
defaultConfig: (): GrokBuildSettings => decodeGrokBuildSettings({}),
create: ({ instanceId, displayName, accentColor, environment, enabled, config }) =>
Effect.gen(function* () {
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
const httpClient = yield* HttpClient.HttpClient;
const serverConfig = yield* ServerConfig;
const eventLoggers = yield* ProviderEventLoggers;
const processEnv = mergeProviderInstanceEnvironment(environment);
const continuationIdentity = defaultProviderContinuationIdentity({
driverKind: DRIVER_KIND,
instanceId,
});
const stampIdentity = withInstanceIdentity({
instanceId,
displayName,
accentColor,
continuationGroupKey: continuationIdentity.continuationKey,
});
const effectiveConfig = { ...config, enabled } satisfies GrokBuildSettings;
const maintenanceCapabilities = yield* resolveProviderMaintenanceCapabilitiesEffect(UPDATE, {
binaryPath: effectiveConfig.binaryPath,
env: processEnv,
});

const adapter = yield* makeGrokBuildAdapter(effectiveConfig, {
environment: processEnv,
...(eventLoggers.native ? { nativeEventLogger: eventLoggers.native } : {}),
instanceId,
});
const textGeneration = yield* makeGrokBuildTextGeneration(effectiveConfig, processEnv);

const checkProvider = checkGrokBuildProviderStatus(
effectiveConfig,
serverConfig.cwd,
processEnv,
).pipe(
Effect.map(stampIdentity),
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
);

const snapshot = yield* makeManagedServerProvider<GrokBuildSettings>({
maintenanceCapabilities,
getSettings: Effect.succeed(effectiveConfig),
streamSettings: Stream.never,
haveSettingsChanged: () => false,
initialSnapshot: (settings) =>
buildInitialGrokBuildProviderSnapshot(settings).pipe(Effect.map(stampIdentity)),
checkProvider,
enrichSnapshot: ({ snapshot: currentSnapshot, publishSnapshot }) =>
enrichProviderSnapshotWithVersionAdvisory(currentSnapshot, maintenanceCapabilities).pipe(
Effect.provideService(HttpClient.HttpClient, httpClient),
Effect.flatMap((enrichedSnapshot) => publishSnapshot(enrichedSnapshot)),
),
refreshInterval: SNAPSHOT_REFRESH_INTERVAL,
}).pipe(
Effect.mapError(
(cause) =>
new ProviderDriverError({
driver: DRIVER_KIND,
instanceId,
detail: `Failed to build Grok Build snapshot: ${cause.message ?? String(cause)}`,
cause,
}),
),
);

return {
instanceId,
driverKind: DRIVER_KIND,
continuationIdentity,
displayName,
accentColor,
enabled,
snapshot,
adapter,
textGeneration,
} satisfies ProviderInstance;
}),
};
Loading
Loading