| 1 | import * as Crypto from "effect/Crypto"; |
| 2 | import * as Effect from "effect/Effect"; |
| 3 | import * as Option from "effect/Option"; |
| 4 | import * as Ref from "effect/Ref"; |
| 5 | import * as Schema from "effect/Schema"; |
| 6 | import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; |
| 7 | import type * as EffectAcpErrors from "effect-acp/errors"; |
| 8 | |
| 9 | import { TextGenerationError } from "@t3tools/contracts"; |
| 10 | import type { GrokSettings } from "../settings.ts"; |
| 11 | import { getModelSelectionStringOptionValue } from "@t3tools/shared/model"; |
| 12 | |
| 13 | import * as TextGenerationOperations from "@t3tools/provider-core/server/textGenerationOperations"; |
| 14 | import { |
| 15 | applyGrokAcpModelSelection, |
| 16 | currentGrokModelIdFromSessionSetup, |
| 17 | currentGrokReasoningEffortFromSessionSetup, |
| 18 | makeGrokAcpRuntime, |
| 19 | resolveGrokAcpBaseModelId, |
| 20 | } from "./acpSupport.ts"; |
| 21 | |
| 22 | const GROK_TIMEOUT_MS = 180_000; |
| 23 | |
| 24 | const isTextGenerationError = Schema.is(TextGenerationError); |
| 25 | |
| 26 | export 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 | }); |