resolve #23
This commit is contained in:
parent
4b924851a8
commit
1c46a76bbd
@ -256,6 +256,76 @@ describe('WorkflowEngine Integration: Happy Path', () => {
|
||||
expect(startedSteps).toEqual(['plan', 'implement', 'ai_review', 'reviewers', 'supervise']);
|
||||
});
|
||||
|
||||
it('should pass instruction to step:start for normal steps', async () => {
|
||||
const simpleConfig: WorkflowConfig = {
|
||||
name: 'test',
|
||||
maxIterations: 10,
|
||||
initialStep: 'plan',
|
||||
steps: [
|
||||
makeStep('plan', {
|
||||
rules: [makeRule('done', 'COMPLETE')],
|
||||
}),
|
||||
],
|
||||
};
|
||||
const engine = new WorkflowEngine(simpleConfig, tmpDir, 'test task');
|
||||
|
||||
mockRunAgentSequence([
|
||||
makeResponse({ agent: 'plan', content: 'Plan done' }),
|
||||
]);
|
||||
mockDetectMatchedRuleSequence([
|
||||
{ index: 0, method: 'phase1_tag' },
|
||||
]);
|
||||
|
||||
const startFn = vi.fn();
|
||||
engine.on('step:start', startFn);
|
||||
|
||||
await engine.run();
|
||||
|
||||
expect(startFn).toHaveBeenCalledTimes(1);
|
||||
// step:start should receive (step, iteration, instruction)
|
||||
const [_step, _iteration, instruction] = startFn.mock.calls[0];
|
||||
expect(typeof instruction).toBe('string');
|
||||
expect(instruction.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should pass empty instruction to step:start for parallel steps', async () => {
|
||||
const config = buildDefaultWorkflowConfig();
|
||||
const engine = new WorkflowEngine(config, tmpDir, 'test task');
|
||||
|
||||
mockRunAgentSequence([
|
||||
makeResponse({ agent: 'plan', content: 'Plan' }),
|
||||
makeResponse({ agent: 'implement', content: 'Impl' }),
|
||||
makeResponse({ agent: 'ai_review', content: 'OK' }),
|
||||
makeResponse({ agent: 'arch-review', content: 'OK' }),
|
||||
makeResponse({ agent: 'security-review', content: 'OK' }),
|
||||
makeResponse({ agent: 'supervise', content: 'Pass' }),
|
||||
]);
|
||||
|
||||
mockDetectMatchedRuleSequence([
|
||||
{ index: 0, method: 'phase1_tag' },
|
||||
{ index: 0, method: 'phase1_tag' },
|
||||
{ index: 0, method: 'phase1_tag' },
|
||||
{ index: 0, method: 'phase1_tag' },
|
||||
{ index: 0, method: 'phase1_tag' },
|
||||
{ index: 0, method: 'aggregate' },
|
||||
{ index: 0, method: 'phase1_tag' },
|
||||
]);
|
||||
|
||||
const startFn = vi.fn();
|
||||
engine.on('step:start', startFn);
|
||||
|
||||
await engine.run();
|
||||
|
||||
// Find the "reviewers" step:start call (parallel step)
|
||||
const reviewersCall = startFn.mock.calls.find(
|
||||
(call) => (call[0] as WorkflowStep).name === 'reviewers'
|
||||
);
|
||||
expect(reviewersCall).toBeDefined();
|
||||
// Parallel steps emit empty string for instruction
|
||||
const [, , instruction] = reviewersCall!;
|
||||
expect(instruction).toBe('');
|
||||
});
|
||||
|
||||
it('should emit iteration:limit when max iterations reached', async () => {
|
||||
const config = buildDefaultWorkflowConfig({ maxIterations: 1 });
|
||||
const engine = new WorkflowEngine(config, tmpDir, 'test task');
|
||||
|
||||
@ -201,9 +201,15 @@ export async function executeWorkflow(
|
||||
|
||||
let abortReason: string | undefined;
|
||||
|
||||
engine.on('step:start', (step, iteration) => {
|
||||
engine.on('step:start', (step, iteration, instruction) => {
|
||||
log.debug('Step starting', { step: step.name, agent: step.agentDisplayName, iteration });
|
||||
info(`[${iteration}/${workflowConfig.maxIterations}] ${step.name} (${step.agentDisplayName})`);
|
||||
|
||||
// Log prompt content for debugging
|
||||
if (instruction) {
|
||||
log.debug('Step instruction', instruction);
|
||||
}
|
||||
|
||||
displayRef.current = new StreamDisplay(step.agentDisplayName);
|
||||
stepRef.current = step.name;
|
||||
|
||||
@ -214,6 +220,7 @@ export async function executeWorkflow(
|
||||
agent: step.agentDisplayName,
|
||||
iteration,
|
||||
timestamp: new Date().toISOString(),
|
||||
...(instruction ? { instruction } : {}),
|
||||
};
|
||||
appendNdjsonLine(ndjsonLogPath, record);
|
||||
});
|
||||
|
||||
@ -48,6 +48,8 @@ export interface NdjsonStepStart {
|
||||
agent: string;
|
||||
iteration: number;
|
||||
timestamp: string;
|
||||
/** Instruction (prompt) sent to the agent. Empty for parallel parent steps. */
|
||||
instruction?: string;
|
||||
}
|
||||
|
||||
/** NDJSON record: streaming chunk received */
|
||||
|
||||
@ -199,11 +199,11 @@ export class WorkflowEngine extends EventEmitter {
|
||||
}
|
||||
|
||||
/** Run a single step (delegates to runParallelStep if step has parallel sub-steps) */
|
||||
private async runStep(step: WorkflowStep): Promise<{ response: AgentResponse; instruction: string }> {
|
||||
private async runStep(step: WorkflowStep, prebuiltInstruction?: string): Promise<{ response: AgentResponse; instruction: string }> {
|
||||
if (step.parallel && step.parallel.length > 0) {
|
||||
return this.runParallelStep(step);
|
||||
}
|
||||
return this.runNormalStep(step);
|
||||
return this.runNormalStep(step, prebuiltInstruction);
|
||||
}
|
||||
|
||||
/** Build common RunAgentOptions shared by all phases */
|
||||
@ -272,9 +272,11 @@ export class WorkflowEngine extends EventEmitter {
|
||||
}
|
||||
|
||||
/** Run a normal (non-parallel) step */
|
||||
private async runNormalStep(step: WorkflowStep): Promise<{ response: AgentResponse; instruction: string }> {
|
||||
const stepIteration = incrementStepIteration(this.state, step.name);
|
||||
const instruction = this.buildInstruction(step, stepIteration);
|
||||
private async runNormalStep(step: WorkflowStep, prebuiltInstruction?: string): Promise<{ response: AgentResponse; instruction: string }> {
|
||||
const stepIteration = prebuiltInstruction
|
||||
? this.state.stepIterations.get(step.name) ?? 1
|
||||
: incrementStepIteration(this.state, step.name);
|
||||
const instruction = prebuiltInstruction ?? this.buildInstruction(step, stepIteration);
|
||||
log.debug('Running step', {
|
||||
step: step.name,
|
||||
agent: step.agent,
|
||||
@ -475,10 +477,18 @@ export class WorkflowEngine extends EventEmitter {
|
||||
}
|
||||
|
||||
this.state.iteration++;
|
||||
this.emit('step:start', step, this.state.iteration);
|
||||
|
||||
// Build instruction before emitting step:start so listeners can log it
|
||||
const isParallel = step.parallel && step.parallel.length > 0;
|
||||
let prebuiltInstruction: string | undefined;
|
||||
if (!isParallel) {
|
||||
const stepIteration = incrementStepIteration(this.state, step.name);
|
||||
prebuiltInstruction = this.buildInstruction(step, stepIteration);
|
||||
}
|
||||
this.emit('step:start', step, this.state.iteration, prebuiltInstruction ?? '');
|
||||
|
||||
try {
|
||||
const { response, instruction } = await this.runStep(step);
|
||||
const { response, instruction } = await this.runStep(step, prebuiltInstruction);
|
||||
this.emit('step:complete', step, response, instruction);
|
||||
|
||||
if (response.status === 'blocked') {
|
||||
|
||||
@ -11,7 +11,7 @@ import type { PermissionHandler, AskUserQuestionHandler } from '../claude/proces
|
||||
|
||||
/** Events emitted by workflow engine */
|
||||
export interface WorkflowEvents {
|
||||
'step:start': (step: WorkflowStep, iteration: number) => void;
|
||||
'step:start': (step: WorkflowStep, iteration: number, instruction: string) => void;
|
||||
'step:complete': (step: WorkflowStep, response: AgentResponse, instruction: string) => void;
|
||||
'step:report': (step: WorkflowStep, filePath: string, fileName: string) => void;
|
||||
'step:blocked': (step: WorkflowStep, response: AgentResponse) => void;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user