authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-04-01 08:44:38-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-04-01 15:44:38+00:00
logc542ce336965261dd01c6e670b603434596e3527
tree6b1786a5569c9323b12eba3e8fc698b658b6b5b3
parent15f0be848df55ce27a9c24e622cd58e987886240
signature Signed by PGP key B5690EEEBB952194

[codex] migrate Effect.fn in apps/server/src/git/Layers/ClaudeTextGeneration.ts (#1625)

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

1 files changed, 108 insertions(+), 114 deletions(-)

apps/server/src/git/Layers/ClaudeTextGeneration.ts+108-114
......@@ -66,7 +66,7 @@ const makeClaudeTextGeneration = Effect.gen(function* () {
6666 * Spawn the Claude CLI with structured JSON output and return the parsed,
6767 * schema-validated result.
6868 */
69 const runClaudeJson = <S extends Schema.Top>({
69 const runClaudeJson = Effect.fn("runClaudeJson")(function* <S extends Schema.Top>({
7070 operation,
7171 cwd,
7272 prompt,
......@@ -82,131 +82,125 @@ const makeClaudeTextGeneration = Effect.gen(function* () {
8282 prompt: string;
8383 outputSchemaJson: S;
8484 modelSelection: ClaudeModelSelection;
85 }): Effect.Effect<S["Type"], TextGenerationError, S["DecodingServices"]> =>
86 Effect.gen(function* () {
87 const jsonSchemaStr = JSON.stringify(toJsonSchemaObject(outputSchemaJson));
88 const normalizedOptions = normalizeClaudeModelOptionsWithCapabilities(
89 getClaudeModelCapabilities(modelSelection.model),
90 modelSelection.options,
91 );
92 const settings = {
93 ...(typeof normalizedOptions?.thinking === "boolean"
94 ? { alwaysThinkingEnabled: normalizedOptions.thinking }
95 : {}),
96 ...(normalizedOptions?.fastMode ? { fastMode: true } : {}),
97 };
98
99 const claudeSettings = yield* Effect.map(
100 serverSettingsService.getSettings,
101 (settings) => settings.providers.claudeAgent,
102 ).pipe(Effect.catch(() => Effect.undefined));
103
104 const runClaudeCommand = Effect.gen(function* () {
105 const command = ChildProcess.make(
106 claudeSettings?.binaryPath || "claude",
107 [
108 "-p",
109 "--output-format",
110 "json",
111 "--json-schema",
112 jsonSchemaStr,
113 "--model",
114 resolveApiModelId(modelSelection),
115 ...(normalizedOptions?.effort ? ["--effort", normalizedOptions.effort] : []),
116 ...(Object.keys(settings).length > 0 ? ["--settings", JSON.stringify(settings)] : []),
117 "--dangerously-skip-permissions",
118 ],
119 {
120 cwd,
121 shell: process.platform === "win32",
122 stdin: {
123 stream: Stream.encodeText(Stream.make(prompt)),
124 },
85 }): Effect.fn.Return<S["Type"], TextGenerationError, S["DecodingServices"]> {
86 const jsonSchemaStr = JSON.stringify(toJsonSchemaObject(outputSchemaJson));
87 const normalizedOptions = normalizeClaudeModelOptionsWithCapabilities(
88 getClaudeModelCapabilities(modelSelection.model),
89 modelSelection.options,
90 );
91 const settings = {
92 ...(typeof normalizedOptions?.thinking === "boolean"
93 ? { alwaysThinkingEnabled: normalizedOptions.thinking }
94 : {}),
95 ...(normalizedOptions?.fastMode ? { fastMode: true } : {}),
96 };
97
98 const claudeSettings = yield* Effect.map(
99 serverSettingsService.getSettings,
100 (settings) => settings.providers.claudeAgent,
101 ).pipe(Effect.catch(() => Effect.undefined));
102
103 const runClaudeCommand = Effect.fn("runClaudeJson.runClaudeCommand")(function* () {
104 const command = ChildProcess.make(
105 claudeSettings?.binaryPath || "claude",
106 [
107 "-p",
108 "--output-format",
109 "json",
110 "--json-schema",
111 jsonSchemaStr,
112 "--model",
113 resolveApiModelId(modelSelection),
114 ...(normalizedOptions?.effort ? ["--effort", normalizedOptions.effort] : []),
115 ...(Object.keys(settings).length > 0 ? ["--settings", JSON.stringify(settings)] : []),
116 "--dangerously-skip-permissions",
117 ],
118 {
119 cwd,
120 shell: process.platform === "win32",
121 stdin: {
122 stream: Stream.encodeText(Stream.make(prompt)),
125123 },
124 },
125 );
126
127 const child = yield* commandSpawner
128 .spawn(command)
129 .pipe(
130 Effect.mapError((cause) =>
131 normalizeCliError("claude", operation, cause, "Failed to spawn Claude CLI process"),
132 ),
126133 );
127134
128 const child = yield* commandSpawner
129 .spawn(command)
130 .pipe(
135 const [stdout, stderr, exitCode] = yield* Effect.all(
136 [
137 readStreamAsString(operation, child.stdout),
138 readStreamAsString(operation, child.stderr),
139 child.exitCode.pipe(
131140 Effect.mapError((cause) =>
132 normalizeCliError("claude", operation, cause, "Failed to spawn Claude CLI process"),
141 normalizeCliError("claude", operation, cause, "Failed to read Claude CLI exit code"),
133142 ),
134 );
135
136 const [stdout, stderr, exitCode] = yield* Effect.all(
137 [
138 readStreamAsString(operation, child.stdout),
139 readStreamAsString(operation, child.stderr),
140 child.exitCode.pipe(
141 Effect.mapError((cause) =>
142 normalizeCliError(
143 "claude",
144 operation,
145 cause,
146 "Failed to read Claude CLI exit code",
147 ),
148 ),
143 ),
144 ],
145 { concurrency: "unbounded" },
146 );
147
148 if (exitCode !== 0) {
149 const stderrDetail = stderr.trim();
150 const stdoutDetail = stdout.trim();
151 const detail = stderrDetail.length > 0 ? stderrDetail : stdoutDetail;
152 return yield* new TextGenerationError({
153 operation,
154 detail:
155 detail.length > 0
156 ? `Claude CLI command failed: ${detail}`
157 : `Claude CLI command failed with code ${exitCode}.`,
158 });
159 }
160
161 return stdout;
162 });
163
164 const rawStdout = yield* runClaudeCommand().pipe(
165 Effect.scoped,
166 Effect.timeoutOption(CLAUDE_TIMEOUT_MS),
167 Effect.flatMap(
168 Option.match({
169 onNone: () =>
170 Effect.fail(
171 new TextGenerationError({ operation, detail: "Claude CLI request timed out." }),
149172 ),
150 ],
151 { concurrency: "unbounded" },
152 );
173 onSome: (value) => Effect.succeed(value),
174 }),
175 ),
176 );
153177
154 if (exitCode !== 0) {
155 const stderrDetail = stderr.trim();
156 const stdoutDetail = stdout.trim();
157 const detail = stderrDetail.length > 0 ? stderrDetail : stdoutDetail;
158 return yield* new TextGenerationError({
178 const envelope = yield* Schema.decodeEffect(Schema.fromJsonString(ClaudeOutputEnvelope))(
179 rawStdout,
180 ).pipe(
181 Effect.catchTag("SchemaError", (cause) =>
182 Effect.fail(
183 new TextGenerationError({
159184 operation,
160 detail:
161 detail.length > 0
162 ? `Claude CLI command failed: ${detail}`
163 : `Claude CLI command failed with code ${exitCode}.`,
164 });
165 }
166
167 return stdout;
168 });
169
170 const rawStdout = yield* runClaudeCommand.pipe(
171 Effect.scoped,
172 Effect.timeoutOption(CLAUDE_TIMEOUT_MS),
173 Effect.flatMap(
174 Option.match({
175 onNone: () =>
176 Effect.fail(
177 new TextGenerationError({ operation, detail: "Claude CLI request timed out." }),
178 ),
179 onSome: (value) => Effect.succeed(value),
185 detail: "Claude CLI returned unexpected output format.",
186 cause,
180187 }),
181188 ),
182 );
183
184 const envelope = yield* Schema.decodeEffect(Schema.fromJsonString(ClaudeOutputEnvelope))(
185 rawStdout,
186 ).pipe(
187 Effect.catchTag("SchemaError", (cause) =>
188 Effect.fail(
189 new TextGenerationError({
190 operation,
191 detail: "Claude CLI returned unexpected output format.",
192 cause,
193 }),
194 ),
195 ),
196 );
189 ),
190 );
197191
198 return yield* Schema.decodeEffect(outputSchemaJson)(envelope.structured_output).pipe(
199 Effect.catchTag("SchemaError", (cause) =>
200 Effect.fail(
201 new TextGenerationError({
202 operation,
203 detail: "Claude returned invalid structured output.",
204 cause,
205 }),
206 ),
192 return yield* Schema.decodeEffect(outputSchemaJson)(envelope.structured_output).pipe(
193 Effect.catchTag("SchemaError", (cause) =>
194 Effect.fail(
195 new TextGenerationError({
196 operation,
197 detail: "Claude returned invalid structured output.",
198 cause,
199 }),
207200 ),
208 );
209 });
201 ),
202 );
203 });
210204
211205 // ---------------------------------------------------------------------------
212206 // TextGenerationShape methods