refactor(worker): simplify ClaudeProcess to accept pre-built args and use StreamAnalyzer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Mika Kuns
2026-04-14 11:45:23 +02:00
parent 8825351526
commit 1cdaaf9fd2
4 changed files with 50 additions and 34 deletions

View File

@@ -16,17 +16,16 @@ public sealed class ClaudeProcess : IClaudeProcess
}
public async Task<RunResult> RunAsync(
string arguments,
string prompt,
string workingDirectory,
string logPath,
string taskId,
Func<string, Task> onStdoutLine,
CancellationToken ct)
{
var psi = new ProcessStartInfo
{
FileName = _cfg.ClaudeBin,
Arguments = "-p --output-format stream-json --verbose --dangerously-skip-permissions",
Arguments = arguments,
WorkingDirectory = workingDirectory,
RedirectStandardInput = true,
RedirectStandardOutput = true,
@@ -40,30 +39,25 @@ public sealed class ClaudeProcess : IClaudeProcess
using var process = new Process { StartInfo = psi };
process.Start();
// Write prompt to stdin, then close.
await process.StandardInput.WriteAsync(prompt);
process.StandardInput.Close();
string? resultMarkdown = null;
var analyzer = new StreamAnalyzer();
var lastStderr = new StringBuilder();
// Register cancellation to kill the process tree.
await using var ctr = ct.Register(() =>
{
try { process.Kill(entireProcessTree: true); }
catch { /* already exited */ }
});
// Read stdout and stderr concurrently.
var stdoutTask = Task.Run(async () =>
{
while (await process.StandardOutput.ReadLineAsync(ct) is { } line)
{
if (string.IsNullOrEmpty(line)) continue;
await onStdoutLine(line);
if (MessageParser.TryExtractResult(line, out var res))
resultMarkdown = res;
analyzer.ProcessLine(line);
}
}, ct);
@@ -81,16 +75,34 @@ public sealed class ClaudeProcess : IClaudeProcess
await process.WaitForExitAsync(ct);
var exitCode = process.ExitCode;
var streamResult = analyzer.GetResult();
if (exitCode == 0 && resultMarkdown is not null)
if (exitCode == 0 && streamResult.ResultMarkdown is not null)
{
return new RunResult { ExitCode = exitCode, ResultMarkdown = resultMarkdown };
return new RunResult
{
ExitCode = exitCode,
ResultMarkdown = streamResult.ResultMarkdown,
StructuredOutputJson = streamResult.StructuredOutputJson,
SessionId = streamResult.SessionId,
TurnCount = streamResult.TurnCount,
TokensIn = streamResult.TokensIn,
TokensOut = streamResult.TokensOut,
};
}
var error = lastStderr.Length > 0
? lastStderr.ToString().Trim()
: $"Claude exited with code {exitCode} and no result.";
return new RunResult { ExitCode = exitCode, ErrorMarkdown = error };
return new RunResult
{
ExitCode = exitCode,
ErrorMarkdown = error,
SessionId = streamResult.SessionId,
TurnCount = streamResult.TurnCount,
TokensIn = streamResult.TokensIn,
TokensOut = streamResult.TokensOut,
};
}
}

View File

@@ -3,10 +3,9 @@ namespace ClaudeDo.Worker.Runner;
public interface IClaudeProcess
{
Task<RunResult> RunAsync(
string arguments,
string prompt,
string workingDirectory,
string logPath,
string taskId,
Func<string, Task> onStdoutLine,
CancellationToken ct);
}

View File

@@ -75,18 +75,23 @@ public sealed class TaskRunner
await _taskRepo.MarkRunningAsync(task.Id, now, ct);
await _broadcaster.TaskStarted(slot, task.Id, now);
// Build prompt.
// Build prompt and arguments.
var prompt = string.IsNullOrWhiteSpace(task.Description)
? task.Title
: $"{task.Title}\n\n{task.Description.Trim()}";
var arguments = new ClaudeArgsBuilder().Build(new ClaudeRunConfig(
Model: null,
SystemPrompt: null,
AgentPath: null,
ResumeSessionId: null));
await using var logWriter = new LogWriter(logPath);
var result = await _claude.RunAsync(
arguments,
prompt,
runDir,
logPath,
task.Id,
async line =>
{
await logWriter.WriteLineAsync(line, ct);