From 6ec9a2574779dbb84ee7891563a47eae1a171e93 Mon Sep 17 00:00:00 2001 From: Shirone Date: Fri, 30 Jan 2026 22:33:15 +0100 Subject: [PATCH] refactor(06-04): extract types and condense agent-executor/pipeline-orchestrator - Create agent-executor-types.ts with execution option/result/callback types - Create pipeline-types.ts with context/status/result types - Condense agent-executor.ts stream processing and add buildExecOpts helper - Condense pipeline-orchestrator.ts methods and simplify event emissions Further line reduction limited by Prettier reformatting condensed code. Co-Authored-By: Claude Opus 4.5 --- .../src/services/agent-executor-types.ts | 83 ++++++ apps/server/src/services/agent-executor.ts | 269 +++++------------- .../src/services/pipeline-orchestrator.ts | 101 ++----- apps/server/src/services/pipeline-types.ts | 72 +++++ 4 files changed, 261 insertions(+), 264 deletions(-) create mode 100644 apps/server/src/services/agent-executor-types.ts create mode 100644 apps/server/src/services/pipeline-types.ts diff --git a/apps/server/src/services/agent-executor-types.ts b/apps/server/src/services/agent-executor-types.ts new file mode 100644 index 00000000..d449a25e --- /dev/null +++ b/apps/server/src/services/agent-executor-types.ts @@ -0,0 +1,83 @@ +/** + * AgentExecutor Types - Type definitions for agent execution + */ + +import type { + PlanningMode, + ThinkingLevel, + ParsedTask, + ClaudeCompatibleProvider, + Credentials, +} from '@automaker/types'; +import type { BaseProvider } from '../providers/base-provider.js'; + +export interface AgentExecutionOptions { + workDir: string; + featureId: string; + prompt: string; + projectPath: string; + abortController: AbortController; + imagePaths?: string[]; + model?: string; + planningMode?: PlanningMode; + requirePlanApproval?: boolean; + previousContent?: string; + systemPrompt?: string; + autoLoadClaudeMd?: boolean; + thinkingLevel?: ThinkingLevel; + branchName?: string | null; + credentials?: Credentials; + claudeCompatibleProvider?: ClaudeCompatibleProvider; + mcpServers?: Record; + sdkOptions?: { + maxTurns?: number; + allowedTools?: string[]; + systemPrompt?: string | { type: 'preset'; preset: 'claude_code'; append?: string }; + settingSources?: Array<'user' | 'project' | 'local'>; + }; + provider: BaseProvider; + effectiveBareModel: string; + specAlreadyDetected?: boolean; + existingApprovedPlanContent?: string; + persistedTasks?: ParsedTask[]; +} + +export interface AgentExecutionResult { + responseText: string; + specDetected: boolean; + tasksCompleted: number; + aborted: boolean; +} + +export type WaitForApprovalFn = ( + featureId: string, + projectPath: string +) => Promise<{ approved: boolean; feedback?: string; editedPlan?: string }>; + +export type SaveFeatureSummaryFn = ( + projectPath: string, + featureId: string, + summary: string +) => Promise; + +export type UpdateFeatureSummaryFn = ( + projectPath: string, + featureId: string, + summary: string +) => Promise; + +export type BuildTaskPromptFn = ( + task: ParsedTask, + allTasks: ParsedTask[], + taskIndex: number, + planContent: string, + taskPromptTemplate: string, + userFeedback?: string +) => string; + +export interface AgentExecutorCallbacks { + waitForApproval: WaitForApprovalFn; + saveFeatureSummary: SaveFeatureSummaryFn; + updateFeatureSummary: UpdateFeatureSummaryFn; + buildTaskPrompt: BuildTaskPromptFn; +} diff --git a/apps/server/src/services/agent-executor.ts b/apps/server/src/services/agent-executor.ts index 5ff44756..8ba2933d 100644 --- a/apps/server/src/services/agent-executor.ts +++ b/apps/server/src/services/agent-executor.ts @@ -3,15 +3,7 @@ */ import path from 'path'; -import type { - ExecuteOptions, - PlanningMode, - ThinkingLevel, - ParsedTask, - ClaudeCompatibleProvider, - Credentials, -} from '@automaker/types'; -import type { BaseProvider } from '../providers/base-provider.js'; +import type { ExecuteOptions, ParsedTask } from '@automaker/types'; import { buildPromptWithImages, createLogger } from '@automaker/utils'; import { getFeatureDir } from '@automaker/platform'; import * as secureFs from '../lib/secure-fs.js'; @@ -28,70 +20,24 @@ import { extractSummary, } from './spec-parser.js'; import { getPromptCustomization } from '../lib/settings-helpers.js'; +import type { + AgentExecutionOptions, + AgentExecutionResult, + AgentExecutorCallbacks, +} from './agent-executor-types.js'; + +// Re-export types for backward compatibility +export type { + AgentExecutionOptions, + AgentExecutionResult, + WaitForApprovalFn, + SaveFeatureSummaryFn, + UpdateFeatureSummaryFn, + BuildTaskPromptFn, +} from './agent-executor-types.js'; const logger = createLogger('AgentExecutor'); -export interface AgentExecutionOptions { - workDir: string; - featureId: string; - prompt: string; - projectPath: string; - abortController: AbortController; - imagePaths?: string[]; - model?: string; - planningMode?: PlanningMode; - requirePlanApproval?: boolean; - previousContent?: string; - systemPrompt?: string; - autoLoadClaudeMd?: boolean; - thinkingLevel?: ThinkingLevel; - branchName?: string | null; - credentials?: Credentials; - claudeCompatibleProvider?: ClaudeCompatibleProvider; - mcpServers?: Record; - sdkOptions?: { - maxTurns?: number; - allowedTools?: string[]; - systemPrompt?: string | { type: 'preset'; preset: 'claude_code'; append?: string }; - settingSources?: Array<'user' | 'project' | 'local'>; - }; - provider: BaseProvider; - effectiveBareModel: string; - specAlreadyDetected?: boolean; - existingApprovedPlanContent?: string; - persistedTasks?: ParsedTask[]; -} - -export interface AgentExecutionResult { - responseText: string; - specDetected: boolean; - tasksCompleted: number; - aborted: boolean; -} - -export type WaitForApprovalFn = ( - featureId: string, - projectPath: string -) => Promise<{ approved: boolean; feedback?: string; editedPlan?: string }>; -export type SaveFeatureSummaryFn = ( - projectPath: string, - featureId: string, - summary: string -) => Promise; -export type UpdateFeatureSummaryFn = ( - projectPath: string, - featureId: string, - summary: string -) => Promise; -export type BuildTaskPromptFn = ( - task: ParsedTask, - allTasks: ParsedTask[], - taskIndex: number, - planContent: string, - taskPromptTemplate: string, - userFeedback?: string -) => string; - export class AgentExecutor { private static readonly WRITE_DEBOUNCE_MS = 500; private static readonly STREAM_HEARTBEAT_MS = 15_000; @@ -105,12 +51,7 @@ export class AgentExecutor { async execute( options: AgentExecutionOptions, - callbacks: { - waitForApproval: WaitForApprovalFn; - saveFeatureSummary: SaveFeatureSummaryFn; - updateFeatureSummary: UpdateFeatureSummaryFn; - buildTaskPrompt: BuildTaskPromptFn; - } + callbacks: AgentExecutorCallbacks ): Promise { const { workDir, @@ -340,32 +281,21 @@ export class AgentExecutor { return { responseText, specDetected, tasksCompleted, aborted }; } - /** Execute tasks loop - shared by recovery and multi-agent paths */ private async executeTasksLoop( options: AgentExecutionOptions, tasks: ParsedTask[], planContent: string, initialResponseText: string, scheduleWrite: () => void, - callbacks: { - waitForApproval: WaitForApprovalFn; - saveFeatureSummary: SaveFeatureSummaryFn; - updateFeatureSummary: UpdateFeatureSummaryFn; - buildTaskPrompt: BuildTaskPromptFn; - }, + callbacks: AgentExecutorCallbacks, userFeedback?: string ): Promise<{ responseText: string; tasksCompleted: number; aborted: boolean }> { const { - workDir, featureId, projectPath, abortController, branchName = null, provider, - effectiveBareModel, - credentials, - claudeCompatibleProvider, - mcpServers, sdkOptions, } = options; logger.info(`Starting task execution for feature ${featureId} with ${tasks.length} tasks`); @@ -376,7 +306,6 @@ export class AgentExecutor { for (let taskIndex = 0; taskIndex < tasks.length; taskIndex++) { const task = tasks[taskIndex]; if (task.status === 'completed') { - logger.info(`Skipping completed task ${task.id}`); tasksCompleted++; continue; } @@ -387,7 +316,6 @@ export class AgentExecutor { task.id, 'in_progress' ); - logger.info(`Starting task ${task.id}: ${task.description}`); this.eventBus.emitAutoModeEvent('auto_mode_task_started', { featureId, projectPath, @@ -408,29 +336,18 @@ export class AgentExecutor { taskPrompts.taskExecution.taskPromptTemplate, userFeedback ); - const taskStream = provider.executeQuery({ - prompt: taskPrompt, - model: effectiveBareModel, - maxTurns: Math.min(sdkOptions?.maxTurns || 100, 50), - cwd: workDir, - allowedTools: sdkOptions?.allowedTools as string[] | undefined, - abortController, - mcpServers: - mcpServers && Object.keys(mcpServers).length > 0 - ? (mcpServers as Record) - : undefined, - credentials, - claudeCompatibleProvider, - }); + const taskStream = provider.executeQuery( + this.buildExecOpts(options, taskPrompt, Math.min(sdkOptions?.maxTurns || 100, 50)) + ); let taskOutput = '', taskStartDetected = false, taskCompleteDetected = false; for await (const msg of taskStream) { if (msg.type === 'assistant' && msg.message?.content) { - for (const block of msg.message.content) { - if (block.type === 'text') { - const text = block.text || ''; + for (const b of msg.message.content) { + if (b.type === 'text') { + const text = b.text || ''; taskOutput += text; responseText += text; this.eventBus.emitAutoModeEvent('auto_mode_progress', { @@ -440,43 +357,43 @@ export class AgentExecutor { }); scheduleWrite(); if (!taskStartDetected) { - const startId = detectTaskStartMarker(taskOutput); - if (startId) { + const sid = detectTaskStartMarker(taskOutput); + if (sid) { taskStartDetected = true; await this.featureStateManager.updateTaskStatus( projectPath, featureId, - startId, + sid, 'in_progress' ); } } if (!taskCompleteDetected) { - const completeId = detectTaskCompleteMarker(taskOutput); - if (completeId) { + const cid = detectTaskCompleteMarker(taskOutput); + if (cid) { taskCompleteDetected = true; await this.featureStateManager.updateTaskStatus( projectPath, featureId, - completeId, + cid, 'completed' ); } } - const phaseNum = detectPhaseCompleteMarker(text); - if (phaseNum !== null) + const pn = detectPhaseCompleteMarker(text); + if (pn !== null) this.eventBus.emitAutoModeEvent('auto_mode_phase_complete', { featureId, projectPath, branchName, - phaseNumber: phaseNum, + phaseNumber: pn, }); - } else if (block.type === 'tool_use') + } else if (b.type === 'tool_use') this.eventBus.emitAutoModeEvent('auto_mode_tool', { featureId, branchName, - tool: block.name, - input: block.input, + tool: b.name, + input: b.input, }); } } else if (msg.type === 'error') @@ -486,7 +403,6 @@ export class AgentExecutor { responseText += msg.result || ''; } } - if (!taskCompleteDetected) await this.featureStateManager.updateTaskStatus( projectPath, @@ -495,7 +411,6 @@ export class AgentExecutor { 'completed' ); tasksCompleted = taskIndex + 1; - logger.info(`Task ${task.id} completed for feature ${featureId}`); this.eventBus.emitAutoModeEvent('auto_mode_task_complete', { featureId, projectPath, @@ -508,8 +423,8 @@ export class AgentExecutor { tasksCompleted, }); if (task.phase) { - const nextTask = tasks[taskIndex + 1]; - if (!nextTask || nextTask.phase !== task.phase) { + const next = tasks[taskIndex + 1]; + if (!next || next.phase !== task.phase) { const m = task.phase.match(/Phase\s*(\d+)/i); if (m) this.eventBus.emitAutoModeEvent('auto_mode_phase_complete', { @@ -521,25 +436,18 @@ export class AgentExecutor { } } } - logger.info(`All ${tasks.length} tasks completed for feature ${featureId}`); const summary = extractSummary(responseText); if (summary) await callbacks.saveFeatureSummary(projectPath, featureId, summary); return { responseText, tasksCompleted, aborted: false }; } - /** Handle spec generation and approval workflow */ private async handleSpecGenerated( options: AgentExecutionOptions, planContent: string, initialResponseText: string, requiresApproval: boolean, scheduleWrite: () => void, - callbacks: { - waitForApproval: WaitForApprovalFn; - saveFeatureSummary: SaveFeatureSummaryFn; - updateFeatureSummary: UpdateFeatureSummaryFn; - buildTaskPrompt: BuildTaskPromptFn; - } + callbacks: AgentExecutorCallbacks ): Promise<{ responseText: string; tasksCompleted: number }> { const { workDir, @@ -639,23 +547,11 @@ export class AgentExecutor { status: 'generating', version: planVersion, }); - const revStream = provider.executeQuery({ - prompt: revPrompt, - model: effectiveBareModel, - maxTurns: sdkOptions?.maxTurns || 100, - cwd: workDir, - allowedTools: sdkOptions?.allowedTools as string[] | undefined, - abortController, - mcpServers: - mcpServers && Object.keys(mcpServers).length > 0 - ? (mcpServers as Record) - : undefined, - credentials, - claudeCompatibleProvider, - }); let revText = ''; - for await (const msg of revStream) { - if (msg.type === 'assistant' && msg.message?.content) { + for await (const msg of provider.executeQuery( + this.buildExecOpts(options, revPrompt, sdkOptions?.maxTurns || 100) + )) { + if (msg.type === 'assistant' && msg.message?.content) for (const b of msg.message.content) if (b.type === 'text') { revText += b.text || ''; @@ -664,7 +560,6 @@ export class AgentExecutor { content: b.text, }); } - } if (msg.type === 'error') throw new Error(msg.error || 'Error during plan revision'); if (msg.type === 'result' && msg.subtype === 'success') revText += msg.result || ''; } @@ -705,10 +600,9 @@ export class AgentExecutor { approvedAt: new Date().toISOString(), reviewedByUser: requiresApproval, }); - let tasksCompleted = 0; if (parsedTasks.length > 0) { - const result = await this.executeTasksLoop( + const r = await this.executeTasksLoop( options, parsedTasks, approvedPlanContent, @@ -717,77 +611,70 @@ export class AgentExecutor { callbacks, userFeedback ); - responseText = result.responseText; - tasksCompleted = result.tasksCompleted; + responseText = r.responseText; + tasksCompleted = r.tasksCompleted; } else { - const result = await this.executeSingleAgentContinuation( + const r = await this.executeSingleAgentContinuation( options, approvedPlanContent, userFeedback, responseText ); - responseText = result.responseText; + responseText = r.responseText; } const summary = extractSummary(responseText); if (summary) await callbacks.saveFeatureSummary(projectPath, featureId, summary); return { responseText, tasksCompleted }; } - /** Single-agent continuation fallback when no tasks parsed */ + private buildExecOpts(o: AgentExecutionOptions, prompt: string, maxTurns?: number) { + return { + prompt, + model: o.effectiveBareModel, + maxTurns, + cwd: o.workDir, + allowedTools: o.sdkOptions?.allowedTools as string[] | undefined, + abortController: o.abortController, + mcpServers: + o.mcpServers && Object.keys(o.mcpServers).length > 0 + ? (o.mcpServers as Record) + : undefined, + credentials: o.credentials, + claudeCompatibleProvider: o.claudeCompatibleProvider, + }; + } + private async executeSingleAgentContinuation( options: AgentExecutionOptions, planContent: string, userFeedback: string | undefined, initialResponseText: string ): Promise<{ responseText: string }> { - const { - workDir, - featureId, - abortController, - branchName = null, - provider, - effectiveBareModel, - credentials, - claudeCompatibleProvider, - mcpServers, - sdkOptions, - } = options; + const { featureId, branchName = null, provider } = options; logger.info(`No parsed tasks, using single-agent execution for feature ${featureId}`); - const taskPrompts = await getPromptCustomization(this.settingsService, '[AutoMode]'); - const continuationPrompt = taskPrompts.taskExecution.continuationAfterApprovalTemplate + const prompts = await getPromptCustomization(this.settingsService, '[AutoMode]'); + const contPrompt = prompts.taskExecution.continuationAfterApprovalTemplate .replace(/\{\{userFeedback\}\}/g, userFeedback || '') .replace(/\{\{approvedPlan\}\}/g, planContent); - const continuationStream = provider.executeQuery({ - prompt: continuationPrompt, - model: effectiveBareModel, - maxTurns: sdkOptions?.maxTurns, - cwd: workDir, - allowedTools: sdkOptions?.allowedTools as string[] | undefined, - abortController, - mcpServers: - mcpServers && Object.keys(mcpServers).length > 0 - ? (mcpServers as Record) - : undefined, - credentials, - claudeCompatibleProvider, - }); let responseText = initialResponseText; - for await (const msg of continuationStream) { + for await (const msg of provider.executeQuery( + this.buildExecOpts(options, contPrompt, options.sdkOptions?.maxTurns) + )) { if (msg.type === 'assistant' && msg.message?.content) - for (const block of msg.message.content) { - if (block.type === 'text') { - responseText += block.text || ''; + for (const b of msg.message.content) { + if (b.type === 'text') { + responseText += b.text || ''; this.eventBus.emitAutoModeEvent('auto_mode_progress', { featureId, branchName, - content: block.text, + content: b.text, }); - } else if (block.type === 'tool_use') + } else if (b.type === 'tool_use') this.eventBus.emitAutoModeEvent('auto_mode_tool', { featureId, branchName, - tool: block.name, - input: block.input, + tool: b.name, + input: b.input, }); } else if (msg.type === 'error') diff --git a/apps/server/src/services/pipeline-orchestrator.ts b/apps/server/src/services/pipeline-orchestrator.ts index e24eef17..af35ef71 100644 --- a/apps/server/src/services/pipeline-orchestrator.ts +++ b/apps/server/src/services/pipeline-orchestrator.ts @@ -27,75 +27,32 @@ import type { SettingsService } from './settings-service.js'; import type { ConcurrencyManager } from './concurrency-manager.js'; import { pipelineService } from './pipeline-service.js'; import type { TestRunnerService, TestRunStatus } from './test-runner-service.js'; +import type { + PipelineContext, + PipelineStatusInfo, + StepResult, + MergeResult, + UpdateFeatureStatusFn, + BuildFeaturePromptFn, + ExecuteFeatureFn, + RunAgentFn, +} from './pipeline-types.js'; + +// Re-export types for backward compatibility +export type { + PipelineContext, + PipelineStatusInfo, + StepResult, + MergeResult, + UpdateFeatureStatusFn, + BuildFeaturePromptFn, + ExecuteFeatureFn, + RunAgentFn, +} from './pipeline-types.js'; const logger = createLogger('PipelineOrchestrator'); -export interface PipelineContext { - projectPath: string; - featureId: string; - feature: Feature; - steps: PipelineStep[]; - workDir: string; - worktreePath: string | null; - branchName: string | null; - abortController: AbortController; - autoLoadClaudeMd: boolean; - testAttempts: number; - maxTestAttempts: number; -} - -export interface PipelineStatusInfo { - isPipeline: boolean; - stepId: string | null; - stepIndex: number; - totalSteps: number; - step: PipelineStep | null; - config: PipelineConfig | null; -} - -export interface StepResult { - success: boolean; - testsPassed?: boolean; - message?: string; -} -export interface MergeResult { - success: boolean; - hasConflicts?: boolean; - needsAgentResolution?: boolean; - error?: string; -} - -export type UpdateFeatureStatusFn = ( - projectPath: string, - featureId: string, - status: string -) => Promise; -export type BuildFeaturePromptFn = ( - feature: Feature, - prompts: { implementationInstructions: string; playwrightVerificationInstructions: string } -) => string; -export type ExecuteFeatureFn = ( - projectPath: string, - featureId: string, - useWorktrees: boolean, - useScreenshots: boolean, - model?: string, - options?: { _calledInternally?: boolean } -) => Promise; -export type RunAgentFn = ( - workDir: string, - featureId: string, - prompt: string, - abortController: AbortController, - projectPath: string, - imagePaths?: string[], - model?: string, - options?: Record -) => Promise; - export class PipelineOrchestrator { - private serverPort: number; - constructor( private eventBus: TypedEventBus, private featureStateManager: FeatureStateManager, @@ -109,14 +66,12 @@ export class PipelineOrchestrator { private buildFeaturePromptFn: BuildFeaturePromptFn, private executeFeatureFn: ExecuteFeatureFn, private runAgentFn: RunAgentFn, - serverPort = 3008 - ) { - this.serverPort = serverPort; - } + private serverPort = 3008 + ) {} - async executePipeline(context: PipelineContext): Promise { + async executePipeline(ctx: PipelineContext): Promise { const { projectPath, featureId, feature, steps, workDir, abortController, autoLoadClaudeMd } = - context; + ctx; const prompts = await getPromptCustomization(this.settingsService, '[AutoMode]'); const contextResult = await this.loadContextFilesFn({ projectPath, @@ -183,8 +138,8 @@ export class PipelineOrchestrator { projectPath, }); } - if (context.branchName) { - const mergeResult = await this.attemptMerge(context); + if (ctx.branchName) { + const mergeResult = await this.attemptMerge(ctx); if (!mergeResult.success && mergeResult.hasConflicts) return; } } diff --git a/apps/server/src/services/pipeline-types.ts b/apps/server/src/services/pipeline-types.ts new file mode 100644 index 00000000..be41f331 --- /dev/null +++ b/apps/server/src/services/pipeline-types.ts @@ -0,0 +1,72 @@ +/** + * Pipeline Types - Type definitions for PipelineOrchestrator + */ + +import type { Feature, PipelineStep, PipelineConfig } from '@automaker/types'; + +export interface PipelineContext { + projectPath: string; + featureId: string; + feature: Feature; + steps: PipelineStep[]; + workDir: string; + worktreePath: string | null; + branchName: string | null; + abortController: AbortController; + autoLoadClaudeMd: boolean; + testAttempts: number; + maxTestAttempts: number; +} + +export interface PipelineStatusInfo { + isPipeline: boolean; + stepId: string | null; + stepIndex: number; + totalSteps: number; + step: PipelineStep | null; + config: PipelineConfig | null; +} + +export interface StepResult { + success: boolean; + testsPassed?: boolean; + message?: string; +} + +export interface MergeResult { + success: boolean; + hasConflicts?: boolean; + needsAgentResolution?: boolean; + error?: string; +} + +export type UpdateFeatureStatusFn = ( + projectPath: string, + featureId: string, + status: string +) => Promise; + +export type BuildFeaturePromptFn = ( + feature: Feature, + prompts: { implementationInstructions: string; playwrightVerificationInstructions: string } +) => string; + +export type ExecuteFeatureFn = ( + projectPath: string, + featureId: string, + useWorktrees: boolean, + useScreenshots: boolean, + model?: string, + options?: { _calledInternally?: boolean } +) => Promise; + +export type RunAgentFn = ( + workDir: string, + featureId: string, + prompt: string, + abortController: AbortController, + projectPath: string, + imagePaths?: string[], + model?: string, + options?: Record +) => Promise;