import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import type { ChatAttachment, ModelSelection, ProviderInstanceId } from "@t3tools/contracts"; import { TextGenerationError } from "@t3tools/contracts"; import * as ProviderInstanceRegistry from "../provider/Services/ProviderInstanceRegistry.ts"; import type { ProviderInstance } from "../provider/ProviderDriver.ts"; import type { TextGenerationPolicy } from "./TextGenerationPolicy.ts"; export type TextGenerationProvider = "codex" | "claudeAgent" | "cursor" | "grok" | "opencode"; export interface CommitMessageGenerationInput { cwd: string; branch: string | null; stagedSummary: string; stagedPatch: string; /** When true, the model also returns a semantic branch name for the change. */ includeBranch?: boolean; policy?: TextGenerationPolicy | undefined; /** What model and provider to use for generation. */ modelSelection: ModelSelection; } export interface CommitMessageGenerationResult { subject: string; body: string; /** Only present when `includeBranch` was set on the input. */ branch?: string | undefined; } export interface PrContentGenerationInput { cwd: string; baseBranch: string; headBranch: string; commitSummary: string; diffSummary: string; diffPatch: string; changeRequestTemplate?: string | undefined; policy?: TextGenerationPolicy | undefined; /** What model and provider to use for generation. */ modelSelection: ModelSelection; } export interface PrContentGenerationResult { title: string; body: string; } export interface BranchNameGenerationInput { cwd: string; message: string; attachments?: ReadonlyArray | undefined; /** What model and provider to use for generation. */ modelSelection: ModelSelection; } export interface BranchNameGenerationResult { branch: string; } export interface ThreadTitleGenerationInput { cwd: string; message: string; /** Present when replacing an existing title from the current thread history. */ previousTitle?: string | undefined; attachments?: ReadonlyArray | undefined; /** What model and provider to use for generation. */ modelSelection: ModelSelection; } export interface ThreadTitleGenerationResult { title: string; } /** * TextGeneration - Service tag for commit and change request text generation. */ export class TextGeneration extends Context.Service< TextGeneration, { /** * Generate a commit message from staged change context. */ readonly generateCommitMessage: ( input: CommitMessageGenerationInput, ) => Effect.Effect; /** * Generate change request title/body from branch and diff context. */ readonly generatePrContent: ( input: PrContentGenerationInput, ) => Effect.Effect; /** * Generate a concise branch name from a user message. */ readonly generateBranchName: ( input: BranchNameGenerationInput, ) => Effect.Effect; /** Generate a concise thread title from a first message or thread history. */ readonly generateThreadTitle: ( input: ThreadTitleGenerationInput, ) => Effect.Effect; } >()("t3/textGeneration/TextGeneration") {} type TextGenerationOp = | "generateCommitMessage" | "generatePrContent" | "generateBranchName" | "generateThreadTitle"; const resolveInstance = ( registry: ProviderInstanceRegistry.ProviderInstanceRegistry["Service"], operation: TextGenerationOp, instanceId: ProviderInstanceId, ): Effect.Effect => registry.getInstance(instanceId).pipe( Effect.flatMap((instance) => instance ? Effect.succeed(instance.textGeneration) : Effect.fail( new TextGenerationError({ operation, detail: `No provider instance registered for id '${instanceId}'.`, }), ), ), ); export const makeTextGenerationFromRegistry = ( registry: ProviderInstanceRegistry.ProviderInstanceRegistry["Service"], ): TextGeneration["Service"] => TextGeneration.of({ generateCommitMessage: (input) => resolveInstance(registry, "generateCommitMessage", input.modelSelection.instanceId).pipe( Effect.flatMap((textGeneration) => textGeneration.generateCommitMessage(input)), ), generatePrContent: (input) => resolveInstance(registry, "generatePrContent", input.modelSelection.instanceId).pipe( Effect.flatMap((textGeneration) => textGeneration.generatePrContent(input)), ), generateBranchName: (input) => resolveInstance(registry, "generateBranchName", input.modelSelection.instanceId).pipe( Effect.flatMap((textGeneration) => textGeneration.generateBranchName(input)), ), generateThreadTitle: (input) => resolveInstance(registry, "generateThreadTitle", input.modelSelection.instanceId).pipe( Effect.flatMap((textGeneration) => textGeneration.generateThreadTitle(input)), ), }); /** @public Service construction is part of the canonical Effect module API. */ export const make = Effect.gen(function* () { const registry = yield* ProviderInstanceRegistry.ProviderInstanceRegistry; return makeTextGenerationFromRegistry(registry); }); export const layer = Layer.effect(TextGeneration, make);