Phase システムをエージェントに注入する

This commit is contained in:
nrslib 2026-02-03 20:31:20 +09:00
parent bed836f08b
commit 62de1ede3c
10 changed files with 151 additions and 6 deletions

View File

@ -503,6 +503,121 @@ describe('instruction-builder', () => {
expect(result).toContain('- Step Iteration: 3このステップの実行回数'); expect(result).toContain('- Step Iteration: 3このステップの実行回数');
}); });
it('should include workflow structure when workflowSteps is provided', () => {
const step = createMinimalStep('Do work');
step.name = 'implement';
const context = createMinimalContext({
language: 'en',
workflowSteps: [
{ name: 'plan' },
{ name: 'implement' },
{ name: 'review' },
],
currentStepIndex: 1,
});
const result = buildInstruction(step, context);
expect(result).toContain('This workflow consists of 3 steps:');
expect(result).toContain('- Step 1: plan');
expect(result).toContain('- Step 2: implement');
expect(result).toContain('← current');
expect(result).toContain('- Step 3: review');
});
it('should mark current step with marker', () => {
const step = createMinimalStep('Do work');
step.name = 'plan';
const context = createMinimalContext({
language: 'en',
workflowSteps: [
{ name: 'plan' },
{ name: 'implement' },
],
currentStepIndex: 0,
});
const result = buildInstruction(step, context);
expect(result).toContain('- Step 1: plan ← current');
expect(result).not.toContain('- Step 2: implement ← current');
});
it('should include description in parentheses when provided', () => {
const step = createMinimalStep('Do work');
step.name = 'plan';
const context = createMinimalContext({
language: 'ja',
workflowSteps: [
{ name: 'plan', description: 'タスクを分析し実装計画を作成する' },
{ name: 'implement' },
],
currentStepIndex: 0,
});
const result = buildInstruction(step, context);
expect(result).toContain('- Step 1: planタスクを分析し実装計画を作成する ← 現在');
});
it('should skip workflow structure when workflowSteps is not provided', () => {
const step = createMinimalStep('Do work');
const context = createMinimalContext({ language: 'en' });
const result = buildInstruction(step, context);
expect(result).not.toContain('This workflow consists of');
});
it('should skip workflow structure when workflowSteps is empty', () => {
const step = createMinimalStep('Do work');
const context = createMinimalContext({
language: 'en',
workflowSteps: [],
currentStepIndex: -1,
});
const result = buildInstruction(step, context);
expect(result).not.toContain('This workflow consists of');
});
it('should render workflow structure in Japanese', () => {
const step = createMinimalStep('Do work');
step.name = 'plan';
const context = createMinimalContext({
language: 'ja',
workflowSteps: [
{ name: 'plan' },
{ name: 'implement' },
],
currentStepIndex: 0,
});
const result = buildInstruction(step, context);
expect(result).toContain('このワークフローは2ステップで構成されています:');
expect(result).toContain('← 現在');
});
it('should not show current marker when currentStepIndex is -1', () => {
const step = createMinimalStep('Do work');
step.name = 'sub-step';
const context = createMinimalContext({
language: 'en',
workflowSteps: [
{ name: 'plan' },
{ name: 'implement' },
],
currentStepIndex: -1,
});
const result = buildInstruction(step, context);
expect(result).toContain('This workflow consists of 2 steps:');
expect(result).not.toContain('← current');
});
}); });
describe('buildInstruction report-free (phase separation)', () => { describe('buildInstruction report-free (phase separation)', () => {

View File

@ -133,6 +133,7 @@ export const ParallelSubStepRawSchema = z.object({
/** Workflow step schema - raw YAML format */ /** Workflow step schema - raw YAML format */
export const WorkflowStepRawSchema = z.object({ export const WorkflowStepRawSchema = z.object({
name: z.string().min(1), name: z.string().min(1),
description: z.string().optional(),
/** Agent is required for normal steps, optional for parallel container steps */ /** Agent is required for normal steps, optional for parallel container steps */
agent: z.string().optional(), agent: z.string().optional(),
/** Session handling for this step */ /** Session handling for this step */

View File

@ -53,6 +53,8 @@ export interface ReportObjectConfig {
/** Single step in a workflow */ /** Single step in a workflow */
export interface WorkflowStep { export interface WorkflowStep {
name: string; name: string;
/** Brief description of this step's role in the workflow */
description?: string;
/** Agent name, path, or inline prompt as specified in workflow YAML. Undefined when step runs without an agent. */ /** Agent name, path, or inline prompt as specified in workflow YAML. Undefined when step runs without an agent. */
agent?: string; agent?: string;
/** Session handling for this step */ /** Session handling for this step */

View File

@ -31,6 +31,7 @@ export interface StepExecutorDeps {
readonly getReportDir: () => string; readonly getReportDir: () => string;
readonly getLanguage: () => Language | undefined; readonly getLanguage: () => Language | undefined;
readonly getInteractive: () => boolean; readonly getInteractive: () => boolean;
readonly getWorkflowSteps: () => ReadonlyArray<{ name: string; description?: string }>;
readonly detectRuleIndex: (content: string, stepName: string) => number; readonly detectRuleIndex: (content: string, stepName: string) => number;
readonly callAiJudge: ( readonly callAiJudge: (
agentOutput: string, agentOutput: string,
@ -52,6 +53,7 @@ export class StepExecutor {
task: string, task: string,
maxIterations: number, maxIterations: number,
): string { ): string {
const workflowSteps = this.deps.getWorkflowSteps();
return new InstructionBuilder(step, { return new InstructionBuilder(step, {
task, task,
iteration: state.iteration, iteration: state.iteration,
@ -64,6 +66,8 @@ export class StepExecutor {
reportDir: join(this.deps.getProjectCwd(), this.deps.getReportDir()), reportDir: join(this.deps.getProjectCwd(), this.deps.getReportDir()),
language: this.deps.getLanguage(), language: this.deps.getLanguage(),
interactive: this.deps.getInteractive(), interactive: this.deps.getInteractive(),
workflowSteps,
currentStepIndex: workflowSteps.findIndex(s => s.name === step.name),
}).build(); }).build();
} }

View File

@ -101,6 +101,7 @@ export class WorkflowEngine extends EventEmitter {
getReportDir: () => this.reportDir, getReportDir: () => this.reportDir,
getLanguage: () => this.options.language, getLanguage: () => this.options.language,
getInteractive: () => this.options.interactive === true, getInteractive: () => this.options.interactive === true,
getWorkflowSteps: () => this.config.steps.map(s => ({ name: s.name, description: s.description })),
detectRuleIndex: this.detectRuleIndex, detectRuleIndex: this.detectRuleIndex,
callAiJudge: this.callAiJudge, callAiJudge: this.callAiJudge,
}); });

View File

@ -26,6 +26,8 @@ export function isReportObjectConfig(report: string | ReportConfig[] | ReportObj
/** Shape of localized section strings */ /** Shape of localized section strings */
interface SectionStrings { interface SectionStrings {
workflowContext: string; workflowContext: string;
workflowStructure: string;
currentStepMarker: string;
iteration: string; iteration: string;
iterationWorkflowWide: string; iterationWorkflowWide: string;
stepIteration: string; stepIteration: string;
@ -127,12 +129,23 @@ export class InstructionBuilder {
private renderWorkflowContext(language: Language): string { private renderWorkflowContext(language: Language): string {
const s = getPromptObject<SectionStrings>('instruction.sections', language); const s = getPromptObject<SectionStrings>('instruction.sections', language);
const lines: string[] = [ const lines: string[] = [s.workflowContext];
s.workflowContext,
`- ${s.iteration}: ${this.context.iteration}/${this.context.maxIterations}${s.iterationWorkflowWide}`, // Workflow structure (if workflow steps info is available)
`- ${s.stepIteration}: ${this.context.stepIteration}${s.stepIterationTimes}`, if (this.context.workflowSteps && this.context.workflowSteps.length > 0) {
`- ${s.step}: ${this.step.name}`, lines.push(s.workflowStructure.replace('{count}', String(this.context.workflowSteps.length)));
]; this.context.workflowSteps.forEach((ws, index) => {
const isCurrent = index === this.context.currentStepIndex;
const marker = isCurrent ? `${s.currentStepMarker}` : '';
const desc = ws.description ? `${ws.description}` : '';
lines.push(`- Step ${index + 1}: ${ws.name}${desc}${marker}`);
});
lines.push('');
}
lines.push(`- ${s.iteration}: ${this.context.iteration}/${this.context.maxIterations}${s.iterationWorkflowWide}`);
lines.push(`- ${s.stepIteration}: ${this.context.stepIteration}${s.stepIterationTimes}`);
lines.push(`- ${s.step}: ${this.step.name}`);
// If step has report config, include Report Directory path and phase note // If step has report config, include Report Directory path and phase note
if (this.step.report && this.context.reportDir) { if (this.step.report && this.context.reportDir) {

View File

@ -34,6 +34,10 @@ export interface InstructionContext {
language?: Language; language?: Language;
/** Whether interactive-only rules are enabled */ /** Whether interactive-only rules are enabled */
interactive?: boolean; interactive?: boolean;
/** Top-level workflow steps for workflow structure display */
workflowSteps?: ReadonlyArray<{ name: string; description?: string }>;
/** Index of the current step in workflowSteps (0-based) */
currentStepIndex?: number;
} }
/** Execution environment metadata prepended to agent instructions */ /** Execution environment metadata prepended to agent instructions */

View File

@ -185,6 +185,7 @@ function normalizeStepFromRaw(step: RawStep, workflowDir: string): WorkflowStep
const result: WorkflowStep = { const result: WorkflowStep = {
name: step.name, name: step.name,
description: step.description,
agent: agentSpec, agent: agentSpec,
session: step.session, session: step.session,
agentDisplayName: step.agent_name || (agentSpec ? extractAgentDisplayName(agentSpec) : step.name), agentDisplayName: step.agent_name || (agentSpec ? extractAgentDisplayName(agentSpec) : step.name),

View File

@ -133,6 +133,8 @@ instruction:
sections: sections:
workflowContext: "## Workflow Context" workflowContext: "## Workflow Context"
workflowStructure: "This workflow consists of {count} steps:"
currentStepMarker: "current"
iteration: "Iteration" iteration: "Iteration"
iterationWorkflowWide: "(workflow-wide)" iterationWorkflowWide: "(workflow-wide)"
stepIteration: "Step Iteration" stepIteration: "Step Iteration"

View File

@ -146,6 +146,8 @@ instruction:
sections: sections:
workflowContext: "## Workflow Context" workflowContext: "## Workflow Context"
workflowStructure: "このワークフローは{count}ステップで構成されています:"
currentStepMarker: "現在"
iteration: "Iteration" iteration: "Iteration"
iterationWorkflowWide: "(ワークフロー全体)" iterationWorkflowWide: "(ワークフロー全体)"
stepIteration: "Step Iteration" stepIteration: "Step Iteration"