mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-01 08:13:37 +00:00
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 <noreply@anthropic.com>
This commit is contained in:
83
apps/server/src/services/agent-executor-types.ts
Normal file
83
apps/server/src/services/agent-executor-types.ts
Normal file
@@ -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<string, unknown>;
|
||||||
|
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<void>;
|
||||||
|
|
||||||
|
export type UpdateFeatureSummaryFn = (
|
||||||
|
projectPath: string,
|
||||||
|
featureId: string,
|
||||||
|
summary: string
|
||||||
|
) => Promise<void>;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -3,15 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import type {
|
import type { ExecuteOptions, ParsedTask } from '@automaker/types';
|
||||||
ExecuteOptions,
|
|
||||||
PlanningMode,
|
|
||||||
ThinkingLevel,
|
|
||||||
ParsedTask,
|
|
||||||
ClaudeCompatibleProvider,
|
|
||||||
Credentials,
|
|
||||||
} from '@automaker/types';
|
|
||||||
import type { BaseProvider } from '../providers/base-provider.js';
|
|
||||||
import { buildPromptWithImages, createLogger } from '@automaker/utils';
|
import { buildPromptWithImages, createLogger } from '@automaker/utils';
|
||||||
import { getFeatureDir } from '@automaker/platform';
|
import { getFeatureDir } from '@automaker/platform';
|
||||||
import * as secureFs from '../lib/secure-fs.js';
|
import * as secureFs from '../lib/secure-fs.js';
|
||||||
@@ -28,70 +20,24 @@ import {
|
|||||||
extractSummary,
|
extractSummary,
|
||||||
} from './spec-parser.js';
|
} from './spec-parser.js';
|
||||||
import { getPromptCustomization } from '../lib/settings-helpers.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');
|
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<string, unknown>;
|
|
||||||
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<void>;
|
|
||||||
export type UpdateFeatureSummaryFn = (
|
|
||||||
projectPath: string,
|
|
||||||
featureId: string,
|
|
||||||
summary: string
|
|
||||||
) => Promise<void>;
|
|
||||||
export type BuildTaskPromptFn = (
|
|
||||||
task: ParsedTask,
|
|
||||||
allTasks: ParsedTask[],
|
|
||||||
taskIndex: number,
|
|
||||||
planContent: string,
|
|
||||||
taskPromptTemplate: string,
|
|
||||||
userFeedback?: string
|
|
||||||
) => string;
|
|
||||||
|
|
||||||
export class AgentExecutor {
|
export class AgentExecutor {
|
||||||
private static readonly WRITE_DEBOUNCE_MS = 500;
|
private static readonly WRITE_DEBOUNCE_MS = 500;
|
||||||
private static readonly STREAM_HEARTBEAT_MS = 15_000;
|
private static readonly STREAM_HEARTBEAT_MS = 15_000;
|
||||||
@@ -105,12 +51,7 @@ export class AgentExecutor {
|
|||||||
|
|
||||||
async execute(
|
async execute(
|
||||||
options: AgentExecutionOptions,
|
options: AgentExecutionOptions,
|
||||||
callbacks: {
|
callbacks: AgentExecutorCallbacks
|
||||||
waitForApproval: WaitForApprovalFn;
|
|
||||||
saveFeatureSummary: SaveFeatureSummaryFn;
|
|
||||||
updateFeatureSummary: UpdateFeatureSummaryFn;
|
|
||||||
buildTaskPrompt: BuildTaskPromptFn;
|
|
||||||
}
|
|
||||||
): Promise<AgentExecutionResult> {
|
): Promise<AgentExecutionResult> {
|
||||||
const {
|
const {
|
||||||
workDir,
|
workDir,
|
||||||
@@ -340,32 +281,21 @@ export class AgentExecutor {
|
|||||||
return { responseText, specDetected, tasksCompleted, aborted };
|
return { responseText, specDetected, tasksCompleted, aborted };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Execute tasks loop - shared by recovery and multi-agent paths */
|
|
||||||
private async executeTasksLoop(
|
private async executeTasksLoop(
|
||||||
options: AgentExecutionOptions,
|
options: AgentExecutionOptions,
|
||||||
tasks: ParsedTask[],
|
tasks: ParsedTask[],
|
||||||
planContent: string,
|
planContent: string,
|
||||||
initialResponseText: string,
|
initialResponseText: string,
|
||||||
scheduleWrite: () => void,
|
scheduleWrite: () => void,
|
||||||
callbacks: {
|
callbacks: AgentExecutorCallbacks,
|
||||||
waitForApproval: WaitForApprovalFn;
|
|
||||||
saveFeatureSummary: SaveFeatureSummaryFn;
|
|
||||||
updateFeatureSummary: UpdateFeatureSummaryFn;
|
|
||||||
buildTaskPrompt: BuildTaskPromptFn;
|
|
||||||
},
|
|
||||||
userFeedback?: string
|
userFeedback?: string
|
||||||
): Promise<{ responseText: string; tasksCompleted: number; aborted: boolean }> {
|
): Promise<{ responseText: string; tasksCompleted: number; aborted: boolean }> {
|
||||||
const {
|
const {
|
||||||
workDir,
|
|
||||||
featureId,
|
featureId,
|
||||||
projectPath,
|
projectPath,
|
||||||
abortController,
|
abortController,
|
||||||
branchName = null,
|
branchName = null,
|
||||||
provider,
|
provider,
|
||||||
effectiveBareModel,
|
|
||||||
credentials,
|
|
||||||
claudeCompatibleProvider,
|
|
||||||
mcpServers,
|
|
||||||
sdkOptions,
|
sdkOptions,
|
||||||
} = options;
|
} = options;
|
||||||
logger.info(`Starting task execution for feature ${featureId} with ${tasks.length} tasks`);
|
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++) {
|
for (let taskIndex = 0; taskIndex < tasks.length; taskIndex++) {
|
||||||
const task = tasks[taskIndex];
|
const task = tasks[taskIndex];
|
||||||
if (task.status === 'completed') {
|
if (task.status === 'completed') {
|
||||||
logger.info(`Skipping completed task ${task.id}`);
|
|
||||||
tasksCompleted++;
|
tasksCompleted++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -387,7 +316,6 @@ export class AgentExecutor {
|
|||||||
task.id,
|
task.id,
|
||||||
'in_progress'
|
'in_progress'
|
||||||
);
|
);
|
||||||
logger.info(`Starting task ${task.id}: ${task.description}`);
|
|
||||||
this.eventBus.emitAutoModeEvent('auto_mode_task_started', {
|
this.eventBus.emitAutoModeEvent('auto_mode_task_started', {
|
||||||
featureId,
|
featureId,
|
||||||
projectPath,
|
projectPath,
|
||||||
@@ -408,29 +336,18 @@ export class AgentExecutor {
|
|||||||
taskPrompts.taskExecution.taskPromptTemplate,
|
taskPrompts.taskExecution.taskPromptTemplate,
|
||||||
userFeedback
|
userFeedback
|
||||||
);
|
);
|
||||||
const taskStream = provider.executeQuery({
|
const taskStream = provider.executeQuery(
|
||||||
prompt: taskPrompt,
|
this.buildExecOpts(options, taskPrompt, Math.min(sdkOptions?.maxTurns || 100, 50))
|
||||||
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<string, { command: string }>)
|
|
||||||
: undefined,
|
|
||||||
credentials,
|
|
||||||
claudeCompatibleProvider,
|
|
||||||
});
|
|
||||||
let taskOutput = '',
|
let taskOutput = '',
|
||||||
taskStartDetected = false,
|
taskStartDetected = false,
|
||||||
taskCompleteDetected = false;
|
taskCompleteDetected = false;
|
||||||
|
|
||||||
for await (const msg of taskStream) {
|
for await (const msg of taskStream) {
|
||||||
if (msg.type === 'assistant' && msg.message?.content) {
|
if (msg.type === 'assistant' && msg.message?.content) {
|
||||||
for (const block of msg.message.content) {
|
for (const b of msg.message.content) {
|
||||||
if (block.type === 'text') {
|
if (b.type === 'text') {
|
||||||
const text = block.text || '';
|
const text = b.text || '';
|
||||||
taskOutput += text;
|
taskOutput += text;
|
||||||
responseText += text;
|
responseText += text;
|
||||||
this.eventBus.emitAutoModeEvent('auto_mode_progress', {
|
this.eventBus.emitAutoModeEvent('auto_mode_progress', {
|
||||||
@@ -440,43 +357,43 @@ export class AgentExecutor {
|
|||||||
});
|
});
|
||||||
scheduleWrite();
|
scheduleWrite();
|
||||||
if (!taskStartDetected) {
|
if (!taskStartDetected) {
|
||||||
const startId = detectTaskStartMarker(taskOutput);
|
const sid = detectTaskStartMarker(taskOutput);
|
||||||
if (startId) {
|
if (sid) {
|
||||||
taskStartDetected = true;
|
taskStartDetected = true;
|
||||||
await this.featureStateManager.updateTaskStatus(
|
await this.featureStateManager.updateTaskStatus(
|
||||||
projectPath,
|
projectPath,
|
||||||
featureId,
|
featureId,
|
||||||
startId,
|
sid,
|
||||||
'in_progress'
|
'in_progress'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!taskCompleteDetected) {
|
if (!taskCompleteDetected) {
|
||||||
const completeId = detectTaskCompleteMarker(taskOutput);
|
const cid = detectTaskCompleteMarker(taskOutput);
|
||||||
if (completeId) {
|
if (cid) {
|
||||||
taskCompleteDetected = true;
|
taskCompleteDetected = true;
|
||||||
await this.featureStateManager.updateTaskStatus(
|
await this.featureStateManager.updateTaskStatus(
|
||||||
projectPath,
|
projectPath,
|
||||||
featureId,
|
featureId,
|
||||||
completeId,
|
cid,
|
||||||
'completed'
|
'completed'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const phaseNum = detectPhaseCompleteMarker(text);
|
const pn = detectPhaseCompleteMarker(text);
|
||||||
if (phaseNum !== null)
|
if (pn !== null)
|
||||||
this.eventBus.emitAutoModeEvent('auto_mode_phase_complete', {
|
this.eventBus.emitAutoModeEvent('auto_mode_phase_complete', {
|
||||||
featureId,
|
featureId,
|
||||||
projectPath,
|
projectPath,
|
||||||
branchName,
|
branchName,
|
||||||
phaseNumber: phaseNum,
|
phaseNumber: pn,
|
||||||
});
|
});
|
||||||
} else if (block.type === 'tool_use')
|
} else if (b.type === 'tool_use')
|
||||||
this.eventBus.emitAutoModeEvent('auto_mode_tool', {
|
this.eventBus.emitAutoModeEvent('auto_mode_tool', {
|
||||||
featureId,
|
featureId,
|
||||||
branchName,
|
branchName,
|
||||||
tool: block.name,
|
tool: b.name,
|
||||||
input: block.input,
|
input: b.input,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (msg.type === 'error')
|
} else if (msg.type === 'error')
|
||||||
@@ -486,7 +403,6 @@ export class AgentExecutor {
|
|||||||
responseText += msg.result || '';
|
responseText += msg.result || '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!taskCompleteDetected)
|
if (!taskCompleteDetected)
|
||||||
await this.featureStateManager.updateTaskStatus(
|
await this.featureStateManager.updateTaskStatus(
|
||||||
projectPath,
|
projectPath,
|
||||||
@@ -495,7 +411,6 @@ export class AgentExecutor {
|
|||||||
'completed'
|
'completed'
|
||||||
);
|
);
|
||||||
tasksCompleted = taskIndex + 1;
|
tasksCompleted = taskIndex + 1;
|
||||||
logger.info(`Task ${task.id} completed for feature ${featureId}`);
|
|
||||||
this.eventBus.emitAutoModeEvent('auto_mode_task_complete', {
|
this.eventBus.emitAutoModeEvent('auto_mode_task_complete', {
|
||||||
featureId,
|
featureId,
|
||||||
projectPath,
|
projectPath,
|
||||||
@@ -508,8 +423,8 @@ export class AgentExecutor {
|
|||||||
tasksCompleted,
|
tasksCompleted,
|
||||||
});
|
});
|
||||||
if (task.phase) {
|
if (task.phase) {
|
||||||
const nextTask = tasks[taskIndex + 1];
|
const next = tasks[taskIndex + 1];
|
||||||
if (!nextTask || nextTask.phase !== task.phase) {
|
if (!next || next.phase !== task.phase) {
|
||||||
const m = task.phase.match(/Phase\s*(\d+)/i);
|
const m = task.phase.match(/Phase\s*(\d+)/i);
|
||||||
if (m)
|
if (m)
|
||||||
this.eventBus.emitAutoModeEvent('auto_mode_phase_complete', {
|
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);
|
const summary = extractSummary(responseText);
|
||||||
if (summary) await callbacks.saveFeatureSummary(projectPath, featureId, summary);
|
if (summary) await callbacks.saveFeatureSummary(projectPath, featureId, summary);
|
||||||
return { responseText, tasksCompleted, aborted: false };
|
return { responseText, tasksCompleted, aborted: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Handle spec generation and approval workflow */
|
|
||||||
private async handleSpecGenerated(
|
private async handleSpecGenerated(
|
||||||
options: AgentExecutionOptions,
|
options: AgentExecutionOptions,
|
||||||
planContent: string,
|
planContent: string,
|
||||||
initialResponseText: string,
|
initialResponseText: string,
|
||||||
requiresApproval: boolean,
|
requiresApproval: boolean,
|
||||||
scheduleWrite: () => void,
|
scheduleWrite: () => void,
|
||||||
callbacks: {
|
callbacks: AgentExecutorCallbacks
|
||||||
waitForApproval: WaitForApprovalFn;
|
|
||||||
saveFeatureSummary: SaveFeatureSummaryFn;
|
|
||||||
updateFeatureSummary: UpdateFeatureSummaryFn;
|
|
||||||
buildTaskPrompt: BuildTaskPromptFn;
|
|
||||||
}
|
|
||||||
): Promise<{ responseText: string; tasksCompleted: number }> {
|
): Promise<{ responseText: string; tasksCompleted: number }> {
|
||||||
const {
|
const {
|
||||||
workDir,
|
workDir,
|
||||||
@@ -639,23 +547,11 @@ export class AgentExecutor {
|
|||||||
status: 'generating',
|
status: 'generating',
|
||||||
version: planVersion,
|
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<string, { command: string }>)
|
|
||||||
: undefined,
|
|
||||||
credentials,
|
|
||||||
claudeCompatibleProvider,
|
|
||||||
});
|
|
||||||
let revText = '';
|
let revText = '';
|
||||||
for await (const msg of revStream) {
|
for await (const msg of provider.executeQuery(
|
||||||
if (msg.type === 'assistant' && msg.message?.content) {
|
this.buildExecOpts(options, revPrompt, sdkOptions?.maxTurns || 100)
|
||||||
|
)) {
|
||||||
|
if (msg.type === 'assistant' && msg.message?.content)
|
||||||
for (const b of msg.message.content)
|
for (const b of msg.message.content)
|
||||||
if (b.type === 'text') {
|
if (b.type === 'text') {
|
||||||
revText += b.text || '';
|
revText += b.text || '';
|
||||||
@@ -664,7 +560,6 @@ export class AgentExecutor {
|
|||||||
content: b.text,
|
content: b.text,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (msg.type === 'error') throw new Error(msg.error || 'Error during plan revision');
|
if (msg.type === 'error') throw new Error(msg.error || 'Error during plan revision');
|
||||||
if (msg.type === 'result' && msg.subtype === 'success') revText += msg.result || '';
|
if (msg.type === 'result' && msg.subtype === 'success') revText += msg.result || '';
|
||||||
}
|
}
|
||||||
@@ -705,10 +600,9 @@ export class AgentExecutor {
|
|||||||
approvedAt: new Date().toISOString(),
|
approvedAt: new Date().toISOString(),
|
||||||
reviewedByUser: requiresApproval,
|
reviewedByUser: requiresApproval,
|
||||||
});
|
});
|
||||||
|
|
||||||
let tasksCompleted = 0;
|
let tasksCompleted = 0;
|
||||||
if (parsedTasks.length > 0) {
|
if (parsedTasks.length > 0) {
|
||||||
const result = await this.executeTasksLoop(
|
const r = await this.executeTasksLoop(
|
||||||
options,
|
options,
|
||||||
parsedTasks,
|
parsedTasks,
|
||||||
approvedPlanContent,
|
approvedPlanContent,
|
||||||
@@ -717,77 +611,70 @@ export class AgentExecutor {
|
|||||||
callbacks,
|
callbacks,
|
||||||
userFeedback
|
userFeedback
|
||||||
);
|
);
|
||||||
responseText = result.responseText;
|
responseText = r.responseText;
|
||||||
tasksCompleted = result.tasksCompleted;
|
tasksCompleted = r.tasksCompleted;
|
||||||
} else {
|
} else {
|
||||||
const result = await this.executeSingleAgentContinuation(
|
const r = await this.executeSingleAgentContinuation(
|
||||||
options,
|
options,
|
||||||
approvedPlanContent,
|
approvedPlanContent,
|
||||||
userFeedback,
|
userFeedback,
|
||||||
responseText
|
responseText
|
||||||
);
|
);
|
||||||
responseText = result.responseText;
|
responseText = r.responseText;
|
||||||
}
|
}
|
||||||
const summary = extractSummary(responseText);
|
const summary = extractSummary(responseText);
|
||||||
if (summary) await callbacks.saveFeatureSummary(projectPath, featureId, summary);
|
if (summary) await callbacks.saveFeatureSummary(projectPath, featureId, summary);
|
||||||
return { responseText, tasksCompleted };
|
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<string, { command: string }>)
|
||||||
|
: undefined,
|
||||||
|
credentials: o.credentials,
|
||||||
|
claudeCompatibleProvider: o.claudeCompatibleProvider,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private async executeSingleAgentContinuation(
|
private async executeSingleAgentContinuation(
|
||||||
options: AgentExecutionOptions,
|
options: AgentExecutionOptions,
|
||||||
planContent: string,
|
planContent: string,
|
||||||
userFeedback: string | undefined,
|
userFeedback: string | undefined,
|
||||||
initialResponseText: string
|
initialResponseText: string
|
||||||
): Promise<{ responseText: string }> {
|
): Promise<{ responseText: string }> {
|
||||||
const {
|
const { featureId, branchName = null, provider } = options;
|
||||||
workDir,
|
|
||||||
featureId,
|
|
||||||
abortController,
|
|
||||||
branchName = null,
|
|
||||||
provider,
|
|
||||||
effectiveBareModel,
|
|
||||||
credentials,
|
|
||||||
claudeCompatibleProvider,
|
|
||||||
mcpServers,
|
|
||||||
sdkOptions,
|
|
||||||
} = options;
|
|
||||||
logger.info(`No parsed tasks, using single-agent execution for feature ${featureId}`);
|
logger.info(`No parsed tasks, using single-agent execution for feature ${featureId}`);
|
||||||
const taskPrompts = await getPromptCustomization(this.settingsService, '[AutoMode]');
|
const prompts = await getPromptCustomization(this.settingsService, '[AutoMode]');
|
||||||
const continuationPrompt = taskPrompts.taskExecution.continuationAfterApprovalTemplate
|
const contPrompt = prompts.taskExecution.continuationAfterApprovalTemplate
|
||||||
.replace(/\{\{userFeedback\}\}/g, userFeedback || '')
|
.replace(/\{\{userFeedback\}\}/g, userFeedback || '')
|
||||||
.replace(/\{\{approvedPlan\}\}/g, planContent);
|
.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<string, { command: string }>)
|
|
||||||
: undefined,
|
|
||||||
credentials,
|
|
||||||
claudeCompatibleProvider,
|
|
||||||
});
|
|
||||||
let responseText = initialResponseText;
|
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)
|
if (msg.type === 'assistant' && msg.message?.content)
|
||||||
for (const block of msg.message.content) {
|
for (const b of msg.message.content) {
|
||||||
if (block.type === 'text') {
|
if (b.type === 'text') {
|
||||||
responseText += block.text || '';
|
responseText += b.text || '';
|
||||||
this.eventBus.emitAutoModeEvent('auto_mode_progress', {
|
this.eventBus.emitAutoModeEvent('auto_mode_progress', {
|
||||||
featureId,
|
featureId,
|
||||||
branchName,
|
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', {
|
this.eventBus.emitAutoModeEvent('auto_mode_tool', {
|
||||||
featureId,
|
featureId,
|
||||||
branchName,
|
branchName,
|
||||||
tool: block.name,
|
tool: b.name,
|
||||||
input: block.input,
|
input: b.input,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else if (msg.type === 'error')
|
else if (msg.type === 'error')
|
||||||
|
|||||||
@@ -27,75 +27,32 @@ import type { SettingsService } from './settings-service.js';
|
|||||||
import type { ConcurrencyManager } from './concurrency-manager.js';
|
import type { ConcurrencyManager } from './concurrency-manager.js';
|
||||||
import { pipelineService } from './pipeline-service.js';
|
import { pipelineService } from './pipeline-service.js';
|
||||||
import type { TestRunnerService, TestRunStatus } from './test-runner-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');
|
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<void>;
|
|
||||||
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<void>;
|
|
||||||
export type RunAgentFn = (
|
|
||||||
workDir: string,
|
|
||||||
featureId: string,
|
|
||||||
prompt: string,
|
|
||||||
abortController: AbortController,
|
|
||||||
projectPath: string,
|
|
||||||
imagePaths?: string[],
|
|
||||||
model?: string,
|
|
||||||
options?: Record<string, unknown>
|
|
||||||
) => Promise<void>;
|
|
||||||
|
|
||||||
export class PipelineOrchestrator {
|
export class PipelineOrchestrator {
|
||||||
private serverPort: number;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private eventBus: TypedEventBus,
|
private eventBus: TypedEventBus,
|
||||||
private featureStateManager: FeatureStateManager,
|
private featureStateManager: FeatureStateManager,
|
||||||
@@ -109,14 +66,12 @@ export class PipelineOrchestrator {
|
|||||||
private buildFeaturePromptFn: BuildFeaturePromptFn,
|
private buildFeaturePromptFn: BuildFeaturePromptFn,
|
||||||
private executeFeatureFn: ExecuteFeatureFn,
|
private executeFeatureFn: ExecuteFeatureFn,
|
||||||
private runAgentFn: RunAgentFn,
|
private runAgentFn: RunAgentFn,
|
||||||
serverPort = 3008
|
private serverPort = 3008
|
||||||
) {
|
) {}
|
||||||
this.serverPort = serverPort;
|
|
||||||
}
|
|
||||||
|
|
||||||
async executePipeline(context: PipelineContext): Promise<void> {
|
async executePipeline(ctx: PipelineContext): Promise<void> {
|
||||||
const { projectPath, featureId, feature, steps, workDir, abortController, autoLoadClaudeMd } =
|
const { projectPath, featureId, feature, steps, workDir, abortController, autoLoadClaudeMd } =
|
||||||
context;
|
ctx;
|
||||||
const prompts = await getPromptCustomization(this.settingsService, '[AutoMode]');
|
const prompts = await getPromptCustomization(this.settingsService, '[AutoMode]');
|
||||||
const contextResult = await this.loadContextFilesFn({
|
const contextResult = await this.loadContextFilesFn({
|
||||||
projectPath,
|
projectPath,
|
||||||
@@ -183,8 +138,8 @@ export class PipelineOrchestrator {
|
|||||||
projectPath,
|
projectPath,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (context.branchName) {
|
if (ctx.branchName) {
|
||||||
const mergeResult = await this.attemptMerge(context);
|
const mergeResult = await this.attemptMerge(ctx);
|
||||||
if (!mergeResult.success && mergeResult.hasConflicts) return;
|
if (!mergeResult.success && mergeResult.hasConflicts) return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
72
apps/server/src/services/pipeline-types.ts
Normal file
72
apps/server/src/services/pipeline-types.ts
Normal file
@@ -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<void>;
|
||||||
|
|
||||||
|
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<void>;
|
||||||
|
|
||||||
|
export type RunAgentFn = (
|
||||||
|
workDir: string,
|
||||||
|
featureId: string,
|
||||||
|
prompt: string,
|
||||||
|
abortController: AbortController,
|
||||||
|
projectPath: string,
|
||||||
|
imagePaths?: string[],
|
||||||
|
model?: string,
|
||||||
|
options?: Record<string, unknown>
|
||||||
|
) => Promise<void>;
|
||||||
Reference in New Issue
Block a user