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このステップの実行回数');
});
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)', () => {

View File

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

View File

@ -53,6 +53,8 @@ export interface ReportObjectConfig {
/** Single step in a workflow */
export interface WorkflowStep {
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?: string;
/** Session handling for this step */

View File

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

View File

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

View File

@ -26,6 +26,8 @@ export function isReportObjectConfig(report: string | ReportConfig[] | ReportObj
/** Shape of localized section strings */
interface SectionStrings {
workflowContext: string;
workflowStructure: string;
currentStepMarker: string;
iteration: string;
iterationWorkflowWide: string;
stepIteration: string;
@ -127,12 +129,23 @@ export class InstructionBuilder {
private renderWorkflowContext(language: Language): string {
const s = getPromptObject<SectionStrings>('instruction.sections', language);
const lines: string[] = [
s.workflowContext,
`- ${s.iteration}: ${this.context.iteration}/${this.context.maxIterations}${s.iterationWorkflowWide}`,
`- ${s.stepIteration}: ${this.context.stepIteration}${s.stepIterationTimes}`,
`- ${s.step}: ${this.step.name}`,
];
const lines: string[] = [s.workflowContext];
// Workflow structure (if workflow steps info is available)
if (this.context.workflowSteps && this.context.workflowSteps.length > 0) {
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 (this.step.report && this.context.reportDir) {

View File

@ -34,6 +34,10 @@ export interface InstructionContext {
language?: Language;
/** Whether interactive-only rules are enabled */
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 */

View File

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

View File

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

View File

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