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']);
|
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 () => {
|
it('should emit iteration:limit when max iterations reached', async () => {
|
||||||
const config = buildDefaultWorkflowConfig({ maxIterations: 1 });
|
const config = buildDefaultWorkflowConfig({ maxIterations: 1 });
|
||||||
const engine = new WorkflowEngine(config, tmpDir, 'test task');
|
const engine = new WorkflowEngine(config, tmpDir, 'test task');
|
||||||
|
|||||||
@ -201,9 +201,15 @@ export async function executeWorkflow(
|
|||||||
|
|
||||||
let abortReason: string | undefined;
|
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 });
|
log.debug('Step starting', { step: step.name, agent: step.agentDisplayName, iteration });
|
||||||
info(`[${iteration}/${workflowConfig.maxIterations}] ${step.name} (${step.agentDisplayName})`);
|
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);
|
displayRef.current = new StreamDisplay(step.agentDisplayName);
|
||||||
stepRef.current = step.name;
|
stepRef.current = step.name;
|
||||||
|
|
||||||
@ -214,6 +220,7 @@ export async function executeWorkflow(
|
|||||||
agent: step.agentDisplayName,
|
agent: step.agentDisplayName,
|
||||||
iteration,
|
iteration,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
|
...(instruction ? { instruction } : {}),
|
||||||
};
|
};
|
||||||
appendNdjsonLine(ndjsonLogPath, record);
|
appendNdjsonLine(ndjsonLogPath, record);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -48,6 +48,8 @@ export interface NdjsonStepStart {
|
|||||||
agent: string;
|
agent: string;
|
||||||
iteration: number;
|
iteration: number;
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
|
/** Instruction (prompt) sent to the agent. Empty for parallel parent steps. */
|
||||||
|
instruction?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** NDJSON record: streaming chunk received */
|
/** 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) */
|
/** 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) {
|
if (step.parallel && step.parallel.length > 0) {
|
||||||
return this.runParallelStep(step);
|
return this.runParallelStep(step);
|
||||||
}
|
}
|
||||||
return this.runNormalStep(step);
|
return this.runNormalStep(step, prebuiltInstruction);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Build common RunAgentOptions shared by all phases */
|
/** Build common RunAgentOptions shared by all phases */
|
||||||
@ -272,9 +272,11 @@ export class WorkflowEngine extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Run a normal (non-parallel) step */
|
/** Run a normal (non-parallel) step */
|
||||||
private async runNormalStep(step: WorkflowStep): Promise<{ response: AgentResponse; instruction: string }> {
|
private async runNormalStep(step: WorkflowStep, prebuiltInstruction?: string): Promise<{ response: AgentResponse; instruction: string }> {
|
||||||
const stepIteration = incrementStepIteration(this.state, step.name);
|
const stepIteration = prebuiltInstruction
|
||||||
const instruction = this.buildInstruction(step, stepIteration);
|
? this.state.stepIterations.get(step.name) ?? 1
|
||||||
|
: incrementStepIteration(this.state, step.name);
|
||||||
|
const instruction = prebuiltInstruction ?? this.buildInstruction(step, stepIteration);
|
||||||
log.debug('Running step', {
|
log.debug('Running step', {
|
||||||
step: step.name,
|
step: step.name,
|
||||||
agent: step.agent,
|
agent: step.agent,
|
||||||
@ -475,10 +477,18 @@ export class WorkflowEngine extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.state.iteration++;
|
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 {
|
try {
|
||||||
const { response, instruction } = await this.runStep(step);
|
const { response, instruction } = await this.runStep(step, prebuiltInstruction);
|
||||||
this.emit('step:complete', step, response, instruction);
|
this.emit('step:complete', step, response, instruction);
|
||||||
|
|
||||||
if (response.status === 'blocked') {
|
if (response.status === 'blocked') {
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import type { PermissionHandler, AskUserQuestionHandler } from '../claude/proces
|
|||||||
|
|
||||||
/** Events emitted by workflow engine */
|
/** Events emitted by workflow engine */
|
||||||
export interface WorkflowEvents {
|
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:complete': (step: WorkflowStep, response: AgentResponse, instruction: string) => void;
|
||||||
'step:report': (step: WorkflowStep, filePath: string, fileName: string) => void;
|
'step:report': (step: WorkflowStep, filePath: string, fileName: string) => void;
|
||||||
'step:blocked': (step: WorkflowStep, response: AgentResponse) => void;
|
'step:blocked': (step: WorkflowStep, response: AgentResponse) => void;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user