diff --git a/apps/server/src/git/Layers/ClaudeTextGeneration.ts b/apps/server/src/git/Layers/ClaudeTextGeneration.ts index 7d620be3a6..99ca21b06d 100644 --- a/apps/server/src/git/Layers/ClaudeTextGeneration.ts +++ b/apps/server/src/git/Layers/ClaudeTextGeneration.ts @@ -66,7 +66,7 @@ const makeClaudeTextGeneration = Effect.gen(function* () { * Spawn the Claude CLI with structured JSON output and return the parsed, * schema-validated result. */ - const runClaudeJson = ({ + const runClaudeJson = Effect.fn("runClaudeJson")(function* ({ operation, cwd, prompt, @@ -82,131 +82,125 @@ const makeClaudeTextGeneration = Effect.gen(function* () { prompt: string; outputSchemaJson: S; modelSelection: ClaudeModelSelection; - }): Effect.Effect => - Effect.gen(function* () { - const jsonSchemaStr = JSON.stringify(toJsonSchemaObject(outputSchemaJson)); - const normalizedOptions = normalizeClaudeModelOptionsWithCapabilities( - getClaudeModelCapabilities(modelSelection.model), - modelSelection.options, - ); - const settings = { - ...(typeof normalizedOptions?.thinking === "boolean" - ? { alwaysThinkingEnabled: normalizedOptions.thinking } - : {}), - ...(normalizedOptions?.fastMode ? { fastMode: true } : {}), - }; - - const claudeSettings = yield* Effect.map( - serverSettingsService.getSettings, - (settings) => settings.providers.claudeAgent, - ).pipe(Effect.catch(() => Effect.undefined)); - - const runClaudeCommand = Effect.gen(function* () { - const command = ChildProcess.make( - claudeSettings?.binaryPath || "claude", - [ - "-p", - "--output-format", - "json", - "--json-schema", - jsonSchemaStr, - "--model", - resolveApiModelId(modelSelection), - ...(normalizedOptions?.effort ? ["--effort", normalizedOptions.effort] : []), - ...(Object.keys(settings).length > 0 ? ["--settings", JSON.stringify(settings)] : []), - "--dangerously-skip-permissions", - ], - { - cwd, - shell: process.platform === "win32", - stdin: { - stream: Stream.encodeText(Stream.make(prompt)), - }, + }): Effect.fn.Return { + const jsonSchemaStr = JSON.stringify(toJsonSchemaObject(outputSchemaJson)); + const normalizedOptions = normalizeClaudeModelOptionsWithCapabilities( + getClaudeModelCapabilities(modelSelection.model), + modelSelection.options, + ); + const settings = { + ...(typeof normalizedOptions?.thinking === "boolean" + ? { alwaysThinkingEnabled: normalizedOptions.thinking } + : {}), + ...(normalizedOptions?.fastMode ? { fastMode: true } : {}), + }; + + const claudeSettings = yield* Effect.map( + serverSettingsService.getSettings, + (settings) => settings.providers.claudeAgent, + ).pipe(Effect.catch(() => Effect.undefined)); + + const runClaudeCommand = Effect.fn("runClaudeJson.runClaudeCommand")(function* () { + const command = ChildProcess.make( + claudeSettings?.binaryPath || "claude", + [ + "-p", + "--output-format", + "json", + "--json-schema", + jsonSchemaStr, + "--model", + resolveApiModelId(modelSelection), + ...(normalizedOptions?.effort ? ["--effort", normalizedOptions.effort] : []), + ...(Object.keys(settings).length > 0 ? ["--settings", JSON.stringify(settings)] : []), + "--dangerously-skip-permissions", + ], + { + cwd, + shell: process.platform === "win32", + stdin: { + stream: Stream.encodeText(Stream.make(prompt)), }, + }, + ); + + const child = yield* commandSpawner + .spawn(command) + .pipe( + Effect.mapError((cause) => + normalizeCliError("claude", operation, cause, "Failed to spawn Claude CLI process"), + ), ); - const child = yield* commandSpawner - .spawn(command) - .pipe( + const [stdout, stderr, exitCode] = yield* Effect.all( + [ + readStreamAsString(operation, child.stdout), + readStreamAsString(operation, child.stderr), + child.exitCode.pipe( Effect.mapError((cause) => - normalizeCliError("claude", operation, cause, "Failed to spawn Claude CLI process"), + normalizeCliError("claude", operation, cause, "Failed to read Claude CLI exit code"), ), - ); - - const [stdout, stderr, exitCode] = yield* Effect.all( - [ - readStreamAsString(operation, child.stdout), - readStreamAsString(operation, child.stderr), - child.exitCode.pipe( - Effect.mapError((cause) => - normalizeCliError( - "claude", - operation, - cause, - "Failed to read Claude CLI exit code", - ), - ), + ), + ], + { concurrency: "unbounded" }, + ); + + if (exitCode !== 0) { + const stderrDetail = stderr.trim(); + const stdoutDetail = stdout.trim(); + const detail = stderrDetail.length > 0 ? stderrDetail : stdoutDetail; + return yield* new TextGenerationError({ + operation, + detail: + detail.length > 0 + ? `Claude CLI command failed: ${detail}` + : `Claude CLI command failed with code ${exitCode}.`, + }); + } + + return stdout; + }); + + const rawStdout = yield* runClaudeCommand().pipe( + Effect.scoped, + Effect.timeoutOption(CLAUDE_TIMEOUT_MS), + Effect.flatMap( + Option.match({ + onNone: () => + Effect.fail( + new TextGenerationError({ operation, detail: "Claude CLI request timed out." }), ), - ], - { concurrency: "unbounded" }, - ); + onSome: (value) => Effect.succeed(value), + }), + ), + ); - if (exitCode !== 0) { - const stderrDetail = stderr.trim(); - const stdoutDetail = stdout.trim(); - const detail = stderrDetail.length > 0 ? stderrDetail : stdoutDetail; - return yield* new TextGenerationError({ + const envelope = yield* Schema.decodeEffect(Schema.fromJsonString(ClaudeOutputEnvelope))( + rawStdout, + ).pipe( + Effect.catchTag("SchemaError", (cause) => + Effect.fail( + new TextGenerationError({ operation, - detail: - detail.length > 0 - ? `Claude CLI command failed: ${detail}` - : `Claude CLI command failed with code ${exitCode}.`, - }); - } - - return stdout; - }); - - const rawStdout = yield* runClaudeCommand.pipe( - Effect.scoped, - Effect.timeoutOption(CLAUDE_TIMEOUT_MS), - Effect.flatMap( - Option.match({ - onNone: () => - Effect.fail( - new TextGenerationError({ operation, detail: "Claude CLI request timed out." }), - ), - onSome: (value) => Effect.succeed(value), + detail: "Claude CLI returned unexpected output format.", + cause, }), ), - ); - - const envelope = yield* Schema.decodeEffect(Schema.fromJsonString(ClaudeOutputEnvelope))( - rawStdout, - ).pipe( - Effect.catchTag("SchemaError", (cause) => - Effect.fail( - new TextGenerationError({ - operation, - detail: "Claude CLI returned unexpected output format.", - cause, - }), - ), - ), - ); + ), + ); - return yield* Schema.decodeEffect(outputSchemaJson)(envelope.structured_output).pipe( - Effect.catchTag("SchemaError", (cause) => - Effect.fail( - new TextGenerationError({ - operation, - detail: "Claude returned invalid structured output.", - cause, - }), - ), + return yield* Schema.decodeEffect(outputSchemaJson)(envelope.structured_output).pipe( + Effect.catchTag("SchemaError", (cause) => + Effect.fail( + new TextGenerationError({ + operation, + detail: "Claude returned invalid structured output.", + cause, + }), ), - ); - }); + ), + ); + }); // --------------------------------------------------------------------------- // TextGenerationShape methods