1import * as Crypto from "effect/Crypto";
2import * as Effect from "effect/Effect";
3import * as Option from "effect/Option";
4import * as Ref from "effect/Ref";
5import * as Schema from "effect/Schema";
6import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
7import type * as EffectAcpErrors from "effect-acp/errors";
8
9import { TextGenerationError } from "@t3tools/contracts";
10import type { GrokSettings } from "../settings.ts";
11import { getModelSelectionStringOptionValue } from "@t3tools/shared/model";
12
13import * as TextGenerationOperations from "@t3tools/provider-core/server/textGenerationOperations";
14import {
15 applyGrokAcpModelSelection,
16 currentGrokModelIdFromSessionSetup,
17 currentGrokReasoningEffortFromSessionSetup,
18 makeGrokAcpRuntime,
19 resolveGrokAcpBaseModelId,
20} from "./acpSupport.ts";
21
22const GROK_TIMEOUT_MS = 180_000;
23
24const isTextGenerationError = Schema.is(TextGenerationError);
25
26export const makeGrokTextGeneration = Effect.fn("makeGrokTextGeneration")(function* (
27 grokSettings: GrokSettings,
28 environment: NodeJS.ProcessEnv = process.env,
29) {
30 const crypto = yield* Crypto.Crypto;
31 const commandSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;
32
33 const runGrokJson: TextGenerationOperations.Runner = (request) => {
34 const { operation, cwd, prompt, modelSelection } = request;
35 return Effect.gen(function* () {
36 const outputRef = yield* Ref.make("");
37 const runtime = yield* makeGrokAcpRuntime({
38 grokSettings,
39 environment,
40 cwd,
41 clientInfo: { name: "t3-code-git-text", version: "0.0.0" },
42 }).pipe(
43 Effect.provideService(Crypto.Crypto, crypto),
44 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, commandSpawner),
45 );
46
47 yield* runtime.handleSessionUpdate((notification) => {
48 const update = notification.update;
49 if (update.sessionUpdate !== "agent_message_chunk") {
50 return Effect.void;
51 }
52 const content = update.content;
53 if (content.type !== "text") {
54 return Effect.void;
55 }
56 return Ref.update(outputRef, (current) => current + content.text);
57 });
58
59 const promptResult = yield* Effect.gen(function* () {
60 const resolvedModel = resolveGrokAcpBaseModelId(modelSelection.model);
61 const started = yield* runtime.start();
62 const requestedReasoningEffort = getModelSelectionStringOptionValue(
63 modelSelection,
64 "reasoningEffort",
65 );
66 yield* applyGrokAcpModelSelection({
67 runtime,
68 currentModelId: currentGrokModelIdFromSessionSetup(started.sessionSetupResult),
69 currentReasoningEffort: currentGrokReasoningEffortFromSessionSetup(
70 started.sessionSetupResult,
71 ),
72 requestedModelId: resolvedModel,
73 requestedReasoningEffort,
74 mapError: (cause) =>
75 new TextGenerationError({
76 operation,
77 detail: "Failed to set Grok ACP base model for text generation.",
78 cause,
79 }),
80 });
81 return yield* runtime.prompt({
82 prompt: [{ type: "text", text: prompt }],
83 });
84 }).pipe(
85 Effect.timeoutOption(GROK_TIMEOUT_MS),
86 Effect.flatMap(
87 Option.match({
88 onNone: () =>
89 Effect.fail(
90 new TextGenerationError({ operation, detail: "Grok ACP request timed out." }),
91 ),
92 onSome: (value) => Effect.succeed(value),
93 }),
94 ),
95 Effect.mapError((cause: EffectAcpErrors.AcpError | TextGenerationError) =>
96 isTextGenerationError(cause)
97 ? cause
98 : new TextGenerationError({
99 operation,
100 detail: "Grok ACP request failed.",
101 cause,
102 }),
103 ),
104 );
105
106 const trimmed = (yield* Ref.get(outputRef)).trim();
107 if (!trimmed) {
108 return yield* new TextGenerationError({
109 operation,
110 detail:
111 promptResult.stopReason === "cancelled"
112 ? "Grok ACP request was cancelled."
113 : "Grok Agent returned empty output.",
114 });
115 }
116
117 return yield* TextGenerationOperations.decodeJsonReply(request, "Grok Agent", trimmed);
118 }).pipe(
119 Effect.mapError((cause) =>
120 isTextGenerationError(cause)
121 ? cause
122 : new TextGenerationError({
123 operation,
124 detail: "Grok ACP text generation failed.",
125 cause,
126 }),
127 ),
128 Effect.scoped,
129 );
130 };
131
132 return TextGenerationOperations.fromRunner(
133 "GrokTextGeneration",
134 TextGenerationOperations.withoutInvestigation("Grok", runGrokJson),
135 );
136});