Skip to content
Source
Sign inSign up

textGenerationOperations.ts

205 lines6.9 KB

1import * as Effect from "effect/Effect";
2import * as Schema from "effect/Schema";
3​
4import { type ChatAttachment, type ModelSelection, TextGenerationError } from "@t3tools/contracts";
5import { formatGeneratedBranchName, sanitizeFeatureBranchName } from "@t3tools/shared/git";
6import { extractJsonObject } from "@t3tools/shared/schemaJson";
7​
8import type { ProviderTextGeneration } from "./textGeneration.ts";
9import {
10 buildBranchNamePrompt,
11 buildCommitMessagePrompt,
12 buildPrContentPrompt,
13 buildReviewGuidePrompt,
14 buildThreadTitlePrompt,
15} from "./textGenerationPrompts.ts";
16import {
17 sanitizeCommitSubject,
18 sanitizePrTitle,
19 sanitizeThreadTitle,
20} from "./textGenerationUtils.ts";
21​
22export const TranscriptionPostProcessingOutput = Schema.Struct({ transcription: Schema.String });
23​
24export type Operation = keyof ProviderTextGeneration;
25​
26export interface Request<S extends Schema.Top> {
27 readonly operation: Operation;
28 readonly cwd: string;
29 readonly prompt: string;
30 readonly outputSchema: S;
31 readonly modelSelection: ModelSelection;
32 readonly attachments?: ReadonlyArray<ChatAttachment> | undefined;
33 /** Investigate cwd with read-only tools across turns; fail if the runner cannot guarantee read-only access. */
34 readonly investigate?: { readonly timeoutMs: number } | undefined;
36​
37const INVESTIGATION_TIMEOUT_MS = 15 * 60_000;
38​
39export const investigationUnsupported = (operation: Operation, provider: string) =>
40 new TextGenerationError({
41 operation,
42 detail: `${provider} cannot investigate a checkout read-only. Choose another review guide model, or source control writer model when none is set.`,
43 });
44​
45export type Runner = <S extends Schema.Top>(
46 request: Request<S>,
47) => Effect.Effect<S["Type"], TextGenerationError, S["DecodingServices"]>;
48​
49export const withoutInvestigation =
50 (provider: string, run: Runner): Runner =>
51 (request) =>
52 request.investigate
53 ? Effect.fail(investigationUnsupported(request.operation, provider))
54 : run(request);
55​
56export const decodeJsonReply = <S extends Schema.Top>(
57 request: Pick<Request<S>, "operation" | "outputSchema">,
58 provider: string,
59 text: string,
60) => {
61 const decodeOutput = Schema.decodeEffect(Schema.fromJsonString(request.outputSchema));
62 return decodeOutput(extractJsonObject(text)).pipe(
63 Effect.mapError(
64 (cause) =>
65 new TextGenerationError({
66 operation: request.operation,
67 detail: `${provider} returned invalid structured output.`,
68 cause,
69 }),
70 ),
71 );
72};
73​
74export const generatedThreadTitle = (raw: string) => {
75 const title = sanitizeThreadTitle(raw);
76 // Some models echo the output schema's key instead of a title.
77 return title.toLowerCase() === "title"
78 ? Effect.fail(
79 new TextGenerationError({
80 operation: "generateThreadTitle",
81 detail: 'The model returned the placeholder title "title".',
82 }),
83 )
84 : Effect.succeed(title);
85};
86​
87export function fromRunner(name: string, run: Runner): ProviderTextGeneration {
88 const generateCommitMessage: ProviderTextGeneration["generateCommitMessage"] = Effect.fn(
89 `${name}.generateCommitMessage`,
90 )(function* (input) {
91 const generated = yield* run({
92 operation: "generateCommitMessage",
93 cwd: input.cwd,
94 modelSelection: input.modelSelection,
95 ...buildCommitMessagePrompt({
96 branch: input.branch,
97 stagedSummary: input.stagedSummary,
98 stagedPatch: input.stagedPatch,
99 includeBranch: input.includeBranch === true,
100 policy: input.policy,
101 }),
102 });
103 return {
104 subject: sanitizeCommitSubject(generated.subject),
105 body: generated.body.trim(),
106 ...("branch" in generated && typeof generated.branch === "string"
107 ? { branch: sanitizeFeatureBranchName(generated.branch) }
108 : {}),
109 };
110 });
111​
112 const generatePrContent: ProviderTextGeneration["generatePrContent"] = Effect.fn(
113 `${name}.generatePrContent`,
114 )(function* (input) {
115 const generated = yield* run({
116 operation: "generatePrContent",
117 cwd: input.cwd,
118 modelSelection: input.modelSelection,
119 ...buildPrContentPrompt({
120 baseBranch: input.baseBranch,
121 headBranch: input.headBranch,
122 commitSummary: input.commitSummary,
123 diffSummary: input.diffSummary,
124 diffPatch: input.diffPatch,
125 policy: input.policy,
126 changeRequestTemplate: input.changeRequestTemplate,
127 threadContext: input.threadContext,
128 }),
129 });
130 return { title: sanitizePrTitle(generated.title), body: generated.body.trim() };
131 });
132​
133 const generateBranchName: ProviderTextGeneration["generateBranchName"] = Effect.fn(
134 `${name}.generateBranchName`,
135 )(function* (input) {
136 const generated = yield* run({
137 operation: "generateBranchName",
138 cwd: input.cwd,
139 modelSelection: input.modelSelection,
140 attachments: input.attachments,
141 ...buildBranchNamePrompt({
142 message: input.message,
143 attachments: input.attachments,
144 naming: input.naming,
145 }),
146 });
147 return { branch: formatGeneratedBranchName(generated.branch, input.naming) };
148 });
149​
150 const generateThreadTitle: ProviderTextGeneration["generateThreadTitle"] = Effect.fn(
151 `${name}.generateThreadTitle`,
152 )(function* (input) {
153 const generated = yield* run({
154 operation: "generateThreadTitle",
155 cwd: input.cwd,
156 modelSelection: input.modelSelection,
157 attachments: input.attachments,
158 ...buildThreadTitlePrompt({
159 message: input.message,
160 projectTitle: input.projectTitle,
161 previousTitle: input.previousTitle,
162 forkSource: input.forkSource,
163 linkedContext: input.linkedContext,
164 attachments: input.attachments,
165 instructions: input.instructions,
166 }),
167 });
168 return {
169 title: yield* generatedThreadTitle(generated.title),
170 ...(generated.needsRefinement ? { needsRefinement: true } : {}),
171 };
172 });
173​
174 const generateTranscriptionPostProcessing: ProviderTextGeneration["generateTranscriptionPostProcessing"] =
175 Effect.fn(`${name}.generateTranscriptionPostProcessing`)(function* (input) {
176 return yield* run({
177 operation: "generateTranscriptionPostProcessing",
178 cwd: input.cwd,
179 prompt: input.prompt,
180 outputSchema: TranscriptionPostProcessingOutput,
181 modelSelection: input.modelSelection,
182 });
183 });
184​
185 const generateReviewGuide: ProviderTextGeneration["generateReviewGuide"] = Effect.fn(
186 `${name}.generateReviewGuide`,
187 )(function* (input) {
188 return yield* run({
189 operation: "generateReviewGuide",
190 cwd: input.cwd,
191 modelSelection: input.modelSelection,
192 investigate: { timeoutMs: INVESTIGATION_TIMEOUT_MS },
193 ...buildReviewGuidePrompt(input),
194 });
195 });
196​
197 return {
198 generateCommitMessage,
199 generatePrContent,
200 generateBranchName,
201 generateThreadTitle,
202 generateReviewGuide,
203 generateTranscriptionPostProcessing,
204 } satisfies ProviderTextGeneration;