270 lines · 8.7 KB · TypeScript
RawDownload
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/unstable/process";import type * as EffectAcpErrors from "effect-acp/errors"; import { type GrokSettings, type ModelSelection } from "@t3tools/contracts";import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/shared/git";import { getModelSelectionStringOptionValue } from "@t3tools/shared/model";import { extractJsonObject } from "@t3tools/shared/schemaJson"; import { TextGenerationError } from "@t3tools/contracts";import * as TextGeneration from "./TextGeneration.ts";import {  buildBranchNamePrompt,  buildCommitMessagePrompt,  buildPrContentPrompt,  buildThreadTitlePrompt,} from "./TextGenerationPrompts.ts";import {  sanitizeCommitSubject,  sanitizePrTitle,  sanitizeThreadTitle,} from "./TextGenerationUtils.ts";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 = <S extends Schema.Top>({    operation,    cwd,    prompt,    outputSchemaJson,    modelSelection,  }: {    operation:      | "generateCommitMessage"      | "generatePrContent"      | "generateBranchName"      | "generateThreadTitle";    cwd: string;    prompt: string;    outputSchemaJson: S;    modelSelection: ModelSelection;  }): Effect.Effect<S["Type"], TextGenerationError, S["DecodingServices"]> =>    Effect.gen(function* () {      const resolvedModel = resolveGrokAcpBaseModelId(modelSelection.model);      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 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.",        });      }       const decodeOutput = Schema.decodeEffect(Schema.fromJsonString(outputSchemaJson));      return yield* decodeOutput(extractJsonObject(trimmed)).pipe(        Effect.catchTags({          SchemaError: (cause) =>            Effect.fail(              new TextGenerationError({                operation,                detail: "Grok Agent returned invalid structured output.",                cause,              }),            ),        }),      );    }).pipe(      Effect.mapError((cause) =>        isTextGenerationError(cause)          ? cause          : new TextGenerationError({              operation,              detail: "Grok ACP text generation failed.",              cause,            }),      ),      Effect.scoped,    );   const generateCommitMessage: TextGeneration.TextGeneration["Service"]["generateCommitMessage"] =    Effect.fn("GrokTextGeneration.generateCommitMessage")(function* (input) {      const { prompt, outputSchema } = buildCommitMessagePrompt({        branch: input.branch,        stagedSummary: input.stagedSummary,        stagedPatch: input.stagedPatch,        includeBranch: input.includeBranch === true,        policy: input.policy,      });       const generated = yield* runGrokJson({        operation: "generateCommitMessage",        cwd: input.cwd,        prompt,        outputSchemaJson: outputSchema,        modelSelection: input.modelSelection,      });       return {        subject: sanitizeCommitSubject(generated.subject),        body: generated.body.trim(),        ...("branch" in generated && typeof generated.branch === "string"          ? { branch: sanitizeFeatureBranchName(generated.branch) }          : {}),      };    });   const generatePrContent: TextGeneration.TextGeneration["Service"]["generatePrContent"] =    Effect.fn("GrokTextGeneration.generatePrContent")(function* (input) {      const { prompt, outputSchema } = buildPrContentPrompt({        baseBranch: input.baseBranch,        headBranch: input.headBranch,        commitSummary: input.commitSummary,        diffSummary: input.diffSummary,        diffPatch: input.diffPatch,        policy: input.policy,        changeRequestTemplate: input.changeRequestTemplate,      });       const generated = yield* runGrokJson({        operation: "generatePrContent",        cwd: input.cwd,        prompt,        outputSchemaJson: outputSchema,        modelSelection: input.modelSelection,      });       return {        title: sanitizePrTitle(generated.title),        body: generated.body.trim(),      };    });   const generateBranchName: TextGeneration.TextGeneration["Service"]["generateBranchName"] =    Effect.fn("GrokTextGeneration.generateBranchName")(function* (input) {      const { prompt, outputSchema } = buildBranchNamePrompt({        message: input.message,        attachments: input.attachments,      });       const generated = yield* runGrokJson({        operation: "generateBranchName",        cwd: input.cwd,        prompt,        outputSchemaJson: outputSchema,        modelSelection: input.modelSelection,      });       return {        branch: sanitizeBranchFragment(generated.branch),      };    });   const generateThreadTitle: TextGeneration.TextGeneration["Service"]["generateThreadTitle"] =    Effect.fn("GrokTextGeneration.generateThreadTitle")(function* (input) {      const { prompt, outputSchema } = buildThreadTitlePrompt({        message: input.message,        previousTitle: input.previousTitle,        attachments: input.attachments,      });       const generated = yield* runGrokJson({        operation: "generateThreadTitle",        cwd: input.cwd,        prompt,        outputSchemaJson: outputSchema,        modelSelection: input.modelSelection,      });       return {        title: sanitizeThreadTitle(generated.title),      } satisfies TextGeneration.ThreadTitleGenerationResult;    });   return {    generateCommitMessage,    generatePrContent,    generateBranchName,    generateThreadTitle,  } satisfies TextGeneration.TextGeneration["Service"];});