authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 20:20:09-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 20:20:09-07:00
log8c1605b38327176bae94c33bbc6de86e8b09fe02
tree7329ed9c871ea021fa3ab484d9649684f463b523
parent90074e359f360bd7983661757ccf995524b9dbaa
signature Signed by PGP key B5690EEEBB952194

[codex] Structure OpenCode text generation failures (#3472)

Co-authored-by: codex <codex@users.noreply.github.com>

2 files changed, 353 insertions(+), 88 deletions(-)

apps/server/src/textGeneration/OpenCodeTextGeneration.test.ts+124-21
...@@ -1,4 +1,4 @@...@@ -1,4 +1,4 @@
1import { OpenCodeSettings, ProviderInstanceId } from "@t3tools/contracts";1import { OpenCodeSettings, ProviderInstanceId, TextGenerationError } from "@t3tools/contracts";
2import * as NodeServices from "@effect/platform-node/NodeServices";2import * as NodeServices from "@effect/platform-node/NodeServices";
3import { it } from "@effect/vitest";3import { it } from "@effect/vitest";
4import * as Duration from "effect/Duration";4import * as Duration from "effect/Duration";
...@@ -11,8 +11,8 @@ import { beforeEach, expect } from "vite-plus/test";...@@ -11,8 +11,8 @@ import { beforeEach, expect } from "vite-plus/test";
1111
12import * as ServerConfig from "../config.ts";12import * as ServerConfig from "../config.ts";
13import * as OpenCodeRuntime from "../provider/opencodeRuntime.ts";13import * as OpenCodeRuntime from "../provider/opencodeRuntime.ts";
14import * as OpenCodeTextGeneration from "./OpenCodeTextGeneration.ts";
14import * as TextGeneration from "./TextGeneration.ts";15import * as TextGeneration from "./TextGeneration.ts";
15import { makeOpenCodeTextGeneration } from "./OpenCodeTextGeneration.ts";
1616
17const runtimeMock = {17const runtimeMock = {
18 state: {18 state: {
...@@ -20,8 +20,11 @@ const runtimeMock = {...@@ -20,8 +20,11 @@ const runtimeMock = {
20 promptUrls: [] as string[],20 promptUrls: [] as string[],
21 authHeaders: [] as Array<string | null>,21 authHeaders: [] as Array<string | null>,
22 closeCalls: [] as string[],22 closeCalls: [] as string[],
23 sessionCreateError: undefined as unknown,
24 sessionResult: undefined as { data?: { id: string } } | undefined,
25 promptRequestError: undefined as unknown,
23 promptResult: undefined as26 promptResult: undefined as
24 | { data?: { info?: { error?: unknown }; parts?: Array<{ type: string; text?: string }> } }27 | { data?: { info?: { error?: unknown }; parts?: Array<unknown> } }
25 | undefined,28 | undefined,
26 },29 },
27 reset() {30 reset() {
...@@ -29,6 +32,9 @@ const runtimeMock = {...@@ -29,6 +32,9 @@ const runtimeMock = {
29 this.state.promptUrls.length = 0;32 this.state.promptUrls.length = 0;
30 this.state.authHeaders.length = 0;33 this.state.authHeaders.length = 0;
31 this.state.closeCalls.length = 0;34 this.state.closeCalls.length = 0;
35 this.state.sessionCreateError = undefined;
36 this.state.sessionResult = undefined;
37 this.state.promptRequestError = undefined;
32 this.state.promptResult = undefined;38 this.state.promptResult = undefined;
33 },39 },
34};40};
...@@ -61,12 +67,20 @@ const OpenCodeRuntimeTestDouble: OpenCodeRuntime.OpenCodeRuntimeShape = {...@@ -61,12 +67,20 @@ const OpenCodeRuntimeTestDouble: OpenCodeRuntime.OpenCodeRuntimeShape = {
61 createOpenCodeSdkClient: ({ baseUrl, serverPassword }) =>67 createOpenCodeSdkClient: ({ baseUrl, serverPassword }) =>
62 ({68 ({
63 session: {69 session: {
64 create: async () => ({ data: { id: `${baseUrl}/session` } }),70 create: async () => {
71 if (runtimeMock.state.sessionCreateError !== undefined) {
72 throw runtimeMock.state.sessionCreateError;
73 }
74 return runtimeMock.state.sessionResult ?? { data: { id: `${baseUrl}/session` } };
75 },
65 prompt: async () => {76 prompt: async () => {
66 runtimeMock.state.promptUrls.push(baseUrl);77 runtimeMock.state.promptUrls.push(baseUrl);
67 runtimeMock.state.authHeaders.push(78 runtimeMock.state.authHeaders.push(
68 serverPassword ? `Basic ${btoa(`opencode:${serverPassword}`)}` : null,79 serverPassword ? `Basic ${btoa(`opencode:${serverPassword}`)}` : null,
69 );80 );
81 if (runtimeMock.state.promptRequestError !== undefined) {
82 throw runtimeMock.state.promptRequestError;
83 }
70 return (84 return (
71 runtimeMock.state.promptResult ?? {85 runtimeMock.state.promptResult ?? {
72 data: {86 data: {
...@@ -99,6 +113,13 @@ const DEFAULT_TEST_MODEL_SELECTION = {...@@ -99,6 +113,13 @@ const DEFAULT_TEST_MODEL_SELECTION = {
99 instanceId: ProviderInstanceId.make("opencode"),113 instanceId: ProviderInstanceId.make("opencode"),
100 model: "openai/gpt-5",114 model: "openai/gpt-5",
101};115};
116const DEFAULT_COMMIT_MESSAGE_INPUT = {
117 cwd: process.cwd(),
118 branch: "feature/opencode-reuse",
119 stagedSummary: "M README.md",
120 stagedPatch: "diff --git a/README.md b/README.md",
121 modelSelection: DEFAULT_TEST_MODEL_SELECTION,
122};
102123
103const OPENCODE_TEXT_GENERATION_IDLE_TTL_MS = 30_000;124const OPENCODE_TEXT_GENERATION_IDLE_TTL_MS = 30_000;
104125
...@@ -142,7 +163,7 @@ function withOpenCodeTextGeneration<A, E, R>(...@@ -142,7 +163,7 @@ function withOpenCodeTextGeneration<A, E, R>(
142 effectFn: (textGeneration: TextGeneration.TextGeneration["Service"]) => Effect.Effect<A, E, R>,163 effectFn: (textGeneration: TextGeneration.TextGeneration["Service"]) => Effect.Effect<A, E, R>,
143) {164) {
144 return Effect.gen(function* () {165 return Effect.gen(function* () {
145 const textGeneration = yield* makeOpenCodeTextGeneration(settings);166 const textGeneration = yield* OpenCodeTextGeneration.makeOpenCodeTextGeneration(settings);
146 return yield* effectFn(textGeneration);167 return yield* effectFn(textGeneration);
147 }).pipe(Effect.scoped);168 }).pipe(Effect.scoped);
148}169}
...@@ -221,22 +242,99 @@ it.layer(OpenCodeTextGenerationTestLayer)("OpenCodeTextGeneration", (it) => {...@@ -221,22 +242,99 @@ it.layer(OpenCodeTextGenerationTestLayer)("OpenCodeTextGeneration", (it) => {
221 ).pipe(Effect.provide(TestClock.layer())),242 ).pipe(Effect.provide(TestClock.layer())),
222 );243 );
223244
224 it.effect("returns a typed empty-output error when OpenCode returns no text parts", () =>245 it.effect("preserves the SDK cause when session creation fails", () =>
225 withOpenCodeTextGeneration(DEFAULT_OPENCODE_SETTINGS, (textGeneration) =>246 withOpenCodeTextGeneration(DEFAULT_OPENCODE_SETTINGS, (textGeneration) =>
226 Effect.gen(function* () {247 Effect.gen(function* () {
227 runtimeMock.state.promptResult = { data: {} };248 const sdkCause = new Error("session endpoint unavailable");
249 runtimeMock.state.sessionCreateError = sdkCause;
228250
229 const error = yield* textGeneration251 const error = yield* textGeneration
230 .generateCommitMessage({252 .generateCommitMessage(DEFAULT_COMMIT_MESSAGE_INPUT)
231 cwd: process.cwd(),253 .pipe(Effect.flip);
232 branch: "feature/opencode-reuse",254
233 stagedSummary: "M README.md",255 expect(error).toBeInstanceOf(TextGenerationError);
234 stagedPatch: "diff --git a/README.md b/README.md",256 expect(error.message).toContain("OpenCode session.create request failed.");
235 modelSelection: DEFAULT_TEST_MODEL_SELECTION,257 expect(error.cause).toMatchObject({
236 })258 _tag: "OpenCodeTextGenerationSessionRequestError",
259 operation: "generateCommitMessage",
260 cwd: process.cwd(),
261 cause: sdkCause,
262 });
263 expect((error.cause as { cause: unknown }).cause).toBe(sdkCause);
264 }),
265 ),
266 );
267
268 it.effect("reports a missing session payload without manufacturing a cause", () =>
269 withOpenCodeTextGeneration(DEFAULT_OPENCODE_SETTINGS, (textGeneration) =>
270 Effect.gen(function* () {
271 runtimeMock.state.sessionResult = {};
272
273 const error = yield* textGeneration
274 .generateCommitMessage(DEFAULT_COMMIT_MESSAGE_INPUT)
275 .pipe(Effect.flip);
276
277 expect(error.message).toContain("OpenCode session.create returned no session payload.");
278 expect(error.cause).toMatchObject({
279 _tag: "OpenCodeTextGenerationSessionPayloadError",
280 operation: "generateCommitMessage",
281 cwd: process.cwd(),
282 });
283 expect(error.cause).not.toHaveProperty("cause");
284 }),
285 ),
286 );
287
288 it.effect("preserves the SDK cause and request context when prompting fails", () =>
289 withOpenCodeTextGeneration(DEFAULT_OPENCODE_SETTINGS, (textGeneration) =>
290 Effect.gen(function* () {
291 const sdkCause = new Error("prompt endpoint unavailable");
292 runtimeMock.state.promptRequestError = sdkCause;
293
294 const error = yield* textGeneration
295 .generateCommitMessage(DEFAULT_COMMIT_MESSAGE_INPUT)
296 .pipe(Effect.flip);
297
298 expect(error.message).toContain("OpenCode session.prompt request failed.");
299 expect(error.cause).toMatchObject({
300 _tag: "OpenCodeTextGenerationPromptRequestError",
301 operation: "generateCommitMessage",
302 cwd: process.cwd(),
303 sessionId: "http://127.0.0.1:4301/session",
304 providerId: "openai",
305 modelId: "gpt-5",
306 cause: sdkCause,
307 });
308 expect((error.cause as { cause: unknown }).cause).toBe(sdkCause);
309 }),
310 ),
311 );
312
313 it.effect("returns a typed empty-output error for malformed and blank response parts", () =>
314 withOpenCodeTextGeneration(DEFAULT_OPENCODE_SETTINGS, (textGeneration) =>
315 Effect.gen(function* () {
316 runtimeMock.state.promptResult = {
317 data: {
318 parts: [null, { type: "tool" }, { type: "text", text: " " }],
319 },
320 };
321
322 const error = yield* textGeneration
323 .generateCommitMessage(DEFAULT_COMMIT_MESSAGE_INPUT)
237 .pipe(Effect.flip);324 .pipe(Effect.flip);
238325
239 expect(error.message).toContain("OpenCode returned empty output.");326 expect(error.message).toContain("OpenCode returned empty output.");
327 expect(error.cause).toMatchObject({
328 _tag: "OpenCodeTextGenerationEmptyOutputError",
329 operation: "generateCommitMessage",
330 cwd: process.cwd(),
331 sessionId: "http://127.0.0.1:4301/session",
332 providerId: "openai",
333 modelId: "gpt-5",
334 responsePartCount: 3,
335 textPartCount: 1,
336 });
337 expect(error.cause).not.toHaveProperty("cause");
240 }),338 }),
241 ),339 ),
242 );340 );
...@@ -289,16 +387,21 @@ it.layer(OpenCodeTextGenerationTestLayer)("OpenCodeTextGeneration", (it) => {...@@ -289,16 +387,21 @@ it.layer(OpenCodeTextGenerationTestLayer)("OpenCodeTextGeneration", (it) => {
289 };387 };
290388
291 const error = yield* textGeneration389 const error = yield* textGeneration
292 .generateCommitMessage({390 .generateCommitMessage(DEFAULT_COMMIT_MESSAGE_INPUT)
293 cwd: process.cwd(),
294 branch: "feature/opencode-reuse",
295 stagedSummary: "M README.md",
296 stagedPatch: "diff --git a/README.md b/README.md",
297 modelSelection: DEFAULT_TEST_MODEL_SELECTION,
298 })
299 .pipe(Effect.flip);391 .pipe(Effect.flip);
300392
301 expect(error.message).toContain("Model did not produce structured output");393 expect(error.message).toContain("Model did not produce structured output");
394 expect(error.cause).toMatchObject({
395 _tag: "OpenCodeTextGenerationPromptResponseError",
396 operation: "generateCommitMessage",
397 cwd: process.cwd(),
398 sessionId: "http://127.0.0.1:4301/session",
399 providerId: "openai",
400 modelId: "gpt-5",
401 providerErrorName: "StructuredOutputError",
402 providerMessage: "Model did not produce structured output",
403 });
404 expect(error.cause).not.toHaveProperty("cause");
302 }),405 }),
303 ),406 ),
304 );407 );
apps/server/src/textGeneration/OpenCodeTextGeneration.ts+229-67
...@@ -6,6 +6,7 @@ import * as Scope from "effect/Scope";...@@ -6,6 +6,7 @@ import * as Scope from "effect/Scope";
6import * as Semaphore from "effect/Semaphore";6import * as Semaphore from "effect/Semaphore";
77
8import {8import {
9 NonNegativeInt,
9 TextGenerationError,10 TextGenerationError,
10 type ChatAttachment,11 type ChatAttachment,
11 type ModelSelection,12 type ModelSelection,
...@@ -33,11 +34,106 @@ import * as OpenCodeRuntime from "../provider/opencodeRuntime.ts";...@@ -33,11 +34,106 @@ import * as OpenCodeRuntime from "../provider/opencodeRuntime.ts";
3334
34const OPENCODE_TEXT_GENERATION_IDLE_TTL = "30 seconds";35const OPENCODE_TEXT_GENERATION_IDLE_TTL = "30 seconds";
3536
36function getOpenCodePromptErrorMessage(error: unknown): string | null {37const OpenCodeTextGenerationOperation = Schema.Literals([
38 "generateCommitMessage",
39 "generatePrContent",
40 "generateBranchName",
41 "generateThreadTitle",
42]);
43
44type OpenCodeTextGenerationOperation = typeof OpenCodeTextGenerationOperation.Type;
45
46const openCodeTextGenerationErrorContext = {
47 operation: OpenCodeTextGenerationOperation,
48 cwd: Schema.String,
49};
50
51export class OpenCodeTextGenerationSessionRequestError extends Schema.TaggedErrorClass<OpenCodeTextGenerationSessionRequestError>()(
52 "OpenCodeTextGenerationSessionRequestError",
53 {
54 ...openCodeTextGenerationErrorContext,
55 cause: Schema.Defect(),
56 },
57) {
58 override get message(): string {
59 return `OpenCode session creation request failed for ${this.operation} in ${this.cwd}.`;
60 }
61}
62
63export class OpenCodeTextGenerationSessionPayloadError extends Schema.TaggedErrorClass<OpenCodeTextGenerationSessionPayloadError>()(
64 "OpenCodeTextGenerationSessionPayloadError",
65 openCodeTextGenerationErrorContext,
66) {
67 override get message(): string {
68 return `OpenCode session.create returned no session payload for ${this.operation} in ${this.cwd}.`;
69 }
70}
71
72const openCodePromptErrorContext = {
73 ...openCodeTextGenerationErrorContext,
74 sessionId: Schema.String,
75 providerId: Schema.String,
76 modelId: Schema.String,
77};
78
79export class OpenCodeTextGenerationPromptRequestError extends Schema.TaggedErrorClass<OpenCodeTextGenerationPromptRequestError>()(
80 "OpenCodeTextGenerationPromptRequestError",
81 {
82 ...openCodePromptErrorContext,
83 cause: Schema.Defect(),
84 },
85) {
86 override get message(): string {
87 return `OpenCode prompt request failed for ${this.operation} in ${this.cwd} using ${this.providerId}/${this.modelId} (session ${this.sessionId}).`;
88 }
89}
90
91export class OpenCodeTextGenerationPromptResponseError extends Schema.TaggedErrorClass<OpenCodeTextGenerationPromptResponseError>()(
92 "OpenCodeTextGenerationPromptResponseError",
93 {
94 ...openCodePromptErrorContext,
95 providerErrorName: Schema.optional(Schema.String),
96 providerMessage: Schema.String,
97 },
98) {
99 override get message(): string {
100 const providerError = this.providerErrorName ? ` ${this.providerErrorName}` : "";
101 return `OpenCode prompt${providerError} failed for ${this.operation} in ${this.cwd} using ${this.providerId}/${this.modelId} (session ${this.sessionId}): ${this.providerMessage}`;
102 }
103}
104
105export class OpenCodeTextGenerationEmptyOutputError extends Schema.TaggedErrorClass<OpenCodeTextGenerationEmptyOutputError>()(
106 "OpenCodeTextGenerationEmptyOutputError",
107 {
108 ...openCodePromptErrorContext,
109 responsePartCount: NonNegativeInt,
110 textPartCount: NonNegativeInt,
111 },
112) {
113 override get message(): string {
114 return `OpenCode returned empty output for ${this.operation} in ${this.cwd} using ${this.providerId}/${this.modelId} (session ${this.sessionId}, ${this.responsePartCount} response parts, ${this.textPartCount} text parts).`;
115 }
116}
117
118interface OpenCodePromptFailure {
119 readonly name?: string;
120 readonly message: string;
121}
122
123interface OpenCodeTextPart {
124 readonly type: "text";
125 readonly text: string;
126}
127
128function getOpenCodePromptFailure(error: unknown): OpenCodePromptFailure | null {
37 if (!error || typeof error !== "object") {129 if (!error || typeof error !== "object") {
38 return null;130 return null;
39 }131 }
40132
133 const name =
134 "name" in error && typeof error.name === "string" && error.name.trim().length > 0
135 ? error.name.trim()
136 : undefined;
41 const message =137 const message =
42 "data" in error &&138 "data" in error &&
43 error.data &&139 error.data &&
...@@ -47,31 +143,34 @@ function getOpenCodePromptErrorMessage(error: unknown): string | null {...@@ -47,31 +143,34 @@ function getOpenCodePromptErrorMessage(error: unknown): string | null {
47 ? error.data.message.trim()143 ? error.data.message.trim()
48 : "";144 : "";
49 if (message.length > 0) {145 if (message.length > 0) {
50 return message;146 return {
147 ...(name ? { name } : {}),
148 message,
149 };
51 }150 }
52151
53 if ("name" in error && typeof error.name === "string") {152 if (name) {
54 const name = error.name.trim();153 return { name, message: name };
55 return name.length > 0 ? name : null;
56 }154 }
57155
58 return null;156 return null;
59}157}
60158
159function isOpenCodeTextPart(part: unknown): part is OpenCodeTextPart {
160 return (
161 part !== null &&
162 typeof part === "object" &&
163 "type" in part &&
164 part.type === "text" &&
165 "text" in part &&
166 typeof part.text === "string"
167 );
168}
169
61function getOpenCodeTextResponse(parts: ReadonlyArray<unknown> | undefined): string {170function getOpenCodeTextResponse(parts: ReadonlyArray<unknown> | undefined): string {
62 return (parts ?? [])171 return (parts ?? [])
63 .flatMap((part) => {172 .filter(isOpenCodeTextPart)
64 if (!part || typeof part !== "object") {173 .map((part) => part.text)
65 return [];
66 }
67 if (!("type" in part) || part.type !== "text") {
68 return [];
69 }
70 if (!("text" in part) || typeof part.text !== "string") {
71 return [];
72 }
73 return [part.text];
74 })
75 .join("")174 .join("")
76 .trim();175 .trim();
77}176}
...@@ -260,11 +359,7 @@ export const makeOpenCodeTextGeneration = Effect.fn("makeOpenCodeTextGeneration"...@@ -260,11 +359,7 @@ export const makeOpenCodeTextGeneration = Effect.fn("makeOpenCodeTextGeneration"
260 );359 );
261360
262 const runOpenCodeJson = Effect.fn("runOpenCodeJson")(function* <S extends Schema.Top>(input: {361 const runOpenCodeJson = Effect.fn("runOpenCodeJson")(function* <S extends Schema.Top>(input: {
263 readonly operation:362 readonly operation: OpenCodeTextGenerationOperation;
264 | "generateCommitMessage"
265 | "generatePrContent"
266 | "generateBranchName"
267 | "generateThreadTitle";
268 readonly cwd: string;363 readonly cwd: string;
269 readonly prompt: string;364 readonly prompt: string;
270 readonly outputSchemaJson: S;365 readonly outputSchemaJson: S;
...@@ -285,54 +380,121 @@ export const makeOpenCodeTextGeneration = Effect.fn("makeOpenCodeTextGeneration"...@@ -285,54 +380,121 @@ export const makeOpenCodeTextGeneration = Effect.fn("makeOpenCodeTextGeneration"
285 resolveAttachmentPath({ attachmentsDir: serverConfig.attachmentsDir, attachment }),380 resolveAttachmentPath({ attachmentsDir: serverConfig.attachmentsDir, attachment }),
286 });381 });
287382
288 const runAgainstServer = (server: Pick<OpenCodeRuntime.OpenCodeServerConnection, "url">) =>383 const runAgainstServer = Effect.fn("runOpenCodeJson.runAgainstServer")(
289 Effect.tryPromise({384 function* (server: Pick<OpenCodeRuntime.OpenCodeServerConnection, "url">) {
290 try: async () => {385 const client = openCodeRuntime.createOpenCodeSdkClient({
291 const client = openCodeRuntime.createOpenCodeSdkClient({386 baseUrl: server.url,
292 baseUrl: server.url,387 directory: input.cwd,
293 directory: input.cwd,388 ...(openCodeSettings.serverUrl.length > 0 && openCodeSettings.serverPassword
294 ...(openCodeSettings.serverUrl.length > 0 && openCodeSettings.serverPassword389 ? { serverPassword: openCodeSettings.serverPassword }
295 ? { serverPassword: openCodeSettings.serverPassword }390 : {}),
296 : {}),391 });
392 const session = yield* Effect.tryPromise({
393 try: () =>
394 client.session.create({
395 title: `T3 Code ${input.operation}`,
396 permission: [{ permission: "*", pattern: "*", action: "deny" }],
397 }),
398 catch: (cause) =>
399 new OpenCodeTextGenerationSessionRequestError({
400 operation: input.operation,
401 cwd: input.cwd,
402 cause,
403 }),
404 });
405 if (!session.data) {
406 return yield* new OpenCodeTextGenerationSessionPayloadError({
407 operation: input.operation,
408 cwd: input.cwd,
297 });409 });
298 const session = await client.session.create({410 }
299 title: `T3 Code ${input.operation}`,411 const selectedAgent = getModelSelectionStringOptionValue(input.modelSelection, "agent");
300 permission: [{ permission: "*", pattern: "*", action: "deny" }],412 const selectedVariant = getModelSelectionStringOptionValue(input.modelSelection, "variant");
413 const promptContext = {
414 operation: input.operation,
415 cwd: input.cwd,
416 sessionId: session.data.id,
417 providerId: parsedModel.providerID,
418 modelId: parsedModel.modelID,
419 };
420
421 const result = yield* Effect.tryPromise({
422 try: () =>
423 client.session.prompt({
424 sessionID: session.data.id,
425 model: parsedModel,
426 ...(selectedAgent ? { agent: selectedAgent } : {}),
427 ...(selectedVariant ? { variant: selectedVariant } : {}),
428 parts: [{ type: "text", text: input.prompt }, ...fileParts],
429 }),
430 catch: (cause) =>
431 new OpenCodeTextGenerationPromptRequestError({
432 ...promptContext,
433 cause,
434 }),
435 });
436 const promptFailure = getOpenCodePromptFailure(result.data?.info?.error);
437 if (promptFailure) {
438 return yield* new OpenCodeTextGenerationPromptResponseError({
439 ...promptContext,
440 ...(promptFailure.name ? { providerErrorName: promptFailure.name } : {}),
441 providerMessage: promptFailure.message,
301 });442 });
302 if (!session.data) {443 }
303 throw new Error("OpenCode session.create returned no session payload.");444 const responseParts = result.data?.parts ?? [];
304 }445 const rawText = getOpenCodeTextResponse(responseParts);
305 const selectedAgent = getModelSelectionStringOptionValue(input.modelSelection, "agent");446 if (rawText.length === 0) {
306 const selectedVariant = getModelSelectionStringOptionValue(447 return yield* new OpenCodeTextGenerationEmptyOutputError({
307 input.modelSelection,448 ...promptContext,
308 "variant",449 responsePartCount: responseParts.length,
309 );450 textPartCount: responseParts.filter(isOpenCodeTextPart).length,
310
311 const result = await client.session.prompt({
312 sessionID: session.data.id,
313 model: parsedModel,
314 ...(selectedAgent ? { agent: selectedAgent } : {}),
315 ...(selectedVariant ? { variant: selectedVariant } : {}),
316 parts: [{ type: "text", text: input.prompt }, ...fileParts],
317 });451 });
318 const info = result.data?.info;452 }
319 const errorMessage = getOpenCodePromptErrorMessage(info?.error);453 return rawText;
320 if (errorMessage) {454 },
321 throw new Error(errorMessage);455 Effect.catchTags({
322 }456 OpenCodeTextGenerationSessionRequestError: (cause) =>
323 const rawText = getOpenCodeTextResponse(result.data?.parts);457 Effect.fail(
324 if (rawText.length === 0) {458 new TextGenerationError({
325 throw new Error("OpenCode returned empty output.");459 operation: cause.operation,
326 }460 detail: "OpenCode session.create request failed.",
327 return rawText;461 cause,
328 },462 }),
329 catch: (cause) =>463 ),
330 new TextGenerationError({464 OpenCodeTextGenerationSessionPayloadError: (cause) =>
331 operation: input.operation,465 Effect.fail(
332 detail: OpenCodeRuntime.openCodeRuntimeErrorDetail(cause),466 new TextGenerationError({
333 cause,467 operation: cause.operation,
334 }),468 detail: "OpenCode session.create returned no session payload.",
335 });469 cause,
470 }),
471 ),
472 OpenCodeTextGenerationPromptRequestError: (cause) =>
473 Effect.fail(
474 new TextGenerationError({
475 operation: cause.operation,
476 detail: "OpenCode session.prompt request failed.",
477 cause,
478 }),
479 ),
480 OpenCodeTextGenerationPromptResponseError: (cause) =>
481 Effect.fail(
482 new TextGenerationError({
483 operation: cause.operation,
484 detail: cause.providerMessage,
485 cause,
486 }),
487 ),
488 OpenCodeTextGenerationEmptyOutputError: (cause) =>
489 Effect.fail(
490 new TextGenerationError({
491 operation: cause.operation,
492 detail: "OpenCode returned empty output.",
493 cause,
494 }),
495 ),
496 }),
497 );
336498
337 const rawOutput =499 const rawOutput =
338 openCodeSettings.serverUrl.length > 0500 openCodeSettings.serverUrl.length > 0