220 lines
8.1 KiB
TypeScript
220 lines
8.1 KiB
TypeScript
/**
|
||
* Phase 1 instruction builder
|
||
*
|
||
* Builds the instruction string for main agent execution by:
|
||
* 1. Auto-injecting standard sections (Execution Context, Workflow Context,
|
||
* User Request, Previous Response, Additional User Inputs, Instructions header,
|
||
* Status Output Rules)
|
||
* 2. Replacing template placeholders with actual values
|
||
*/
|
||
|
||
import type { WorkflowStep, Language, ReportConfig, ReportObjectConfig } from '../../models/types.js';
|
||
import { hasTagBasedRules } from '../evaluation/rule-utils.js';
|
||
import type { InstructionContext } from './instruction-context.js';
|
||
import { buildExecutionMetadata, renderExecutionMetadata } from './instruction-context.js';
|
||
import { generateStatusRulesFromRules } from './status-rules.js';
|
||
import { escapeTemplateChars, replaceTemplatePlaceholders } from './escape.js';
|
||
|
||
/**
|
||
* Check if a report config is the object form (ReportObjectConfig).
|
||
*/
|
||
export function isReportObjectConfig(report: string | ReportConfig[] | ReportObjectConfig): report is ReportObjectConfig {
|
||
return typeof report === 'object' && !Array.isArray(report) && 'name' in report;
|
||
}
|
||
|
||
/** Localized strings for auto-injected sections */
|
||
const SECTION_STRINGS = {
|
||
en: {
|
||
workflowContext: '## Workflow Context',
|
||
iteration: 'Iteration',
|
||
iterationWorkflowWide: '(workflow-wide)',
|
||
stepIteration: 'Step Iteration',
|
||
stepIterationTimes: '(times this step has run)',
|
||
step: 'Step',
|
||
reportDirectory: 'Report Directory',
|
||
reportFile: 'Report File',
|
||
reportFiles: 'Report Files',
|
||
phaseNote: '**Note:** This is Phase 1 (main work). After you complete your work, Phase 2 will automatically generate the report based on your findings.',
|
||
userRequest: '## User Request',
|
||
previousResponse: '## Previous Response',
|
||
additionalUserInputs: '## Additional User Inputs',
|
||
instructions: '## Instructions',
|
||
},
|
||
ja: {
|
||
workflowContext: '## Workflow Context',
|
||
iteration: 'Iteration',
|
||
iterationWorkflowWide: '(ワークフロー全体)',
|
||
stepIteration: 'Step Iteration',
|
||
stepIterationTimes: '(このステップの実行回数)',
|
||
step: 'Step',
|
||
reportDirectory: 'Report Directory',
|
||
reportFile: 'Report File',
|
||
reportFiles: 'Report Files',
|
||
phaseNote: '**注意:** これはPhase 1(本来の作業)です。作業完了後、Phase 2で自動的にレポートを生成します。',
|
||
userRequest: '## User Request',
|
||
previousResponse: '## Previous Response',
|
||
additionalUserInputs: '## Additional User Inputs',
|
||
instructions: '## Instructions',
|
||
},
|
||
} as const;
|
||
|
||
/** Localized strings for auto-generated report output instructions */
|
||
const REPORT_OUTPUT_STRINGS = {
|
||
en: {
|
||
singleHeading: '**Report output:** Output to the `Report File` specified above.',
|
||
multiHeading: '**Report output:** Output to the `Report Files` specified above.',
|
||
createRule: '- If file does not exist: Create new file',
|
||
appendRule: '- If file exists: Append with `## Iteration {step_iteration}` section',
|
||
},
|
||
ja: {
|
||
singleHeading: '**レポート出力:** `Report File` に出力してください。',
|
||
multiHeading: '**レポート出力:** Report Files に出力してください。',
|
||
createRule: '- ファイルが存在しない場合: 新規作成',
|
||
appendRule: '- ファイルが存在する場合: `## Iteration {step_iteration}` セクションを追記',
|
||
},
|
||
} as const;
|
||
|
||
/**
|
||
* Builds Phase 1 instructions for agent execution.
|
||
*
|
||
* Stateless builder — all data is passed via constructor context.
|
||
*/
|
||
export class InstructionBuilder {
|
||
constructor(
|
||
private readonly step: WorkflowStep,
|
||
private readonly context: InstructionContext,
|
||
) {}
|
||
|
||
/**
|
||
* Build the complete instruction string.
|
||
*
|
||
* Generates a complete instruction by auto-injecting standard sections
|
||
* around the step-specific instruction_template content.
|
||
*/
|
||
build(): string {
|
||
const language = this.context.language ?? 'en';
|
||
const s = SECTION_STRINGS[language];
|
||
const sections: string[] = [];
|
||
|
||
// 1. Execution context metadata (working directory + rules + edit permission)
|
||
const metadata = buildExecutionMetadata(this.context, this.step.edit);
|
||
sections.push(renderExecutionMetadata(metadata));
|
||
|
||
// 2. Workflow Context (iteration, step, report info)
|
||
sections.push(this.renderWorkflowContext(language));
|
||
|
||
// Skip auto-injection for sections whose placeholders exist in the template,
|
||
// to avoid duplicate content.
|
||
const tmpl = this.step.instructionTemplate;
|
||
const hasTaskPlaceholder = tmpl.includes('{task}');
|
||
const hasPreviousResponsePlaceholder = tmpl.includes('{previous_response}');
|
||
const hasUserInputsPlaceholder = tmpl.includes('{user_inputs}');
|
||
|
||
// 3. User Request (skip if template embeds {task} directly)
|
||
if (!hasTaskPlaceholder) {
|
||
sections.push(`${s.userRequest}\n${escapeTemplateChars(this.context.task)}`);
|
||
}
|
||
|
||
// 4. Previous Response (skip if template embeds {previous_response} directly)
|
||
if (this.step.passPreviousResponse && this.context.previousOutput && !hasPreviousResponsePlaceholder) {
|
||
sections.push(
|
||
`${s.previousResponse}\n${escapeTemplateChars(this.context.previousOutput.content)}`,
|
||
);
|
||
}
|
||
|
||
// 5. Additional User Inputs (skip if template embeds {user_inputs} directly)
|
||
if (!hasUserInputsPlaceholder) {
|
||
const userInputsStr = this.context.userInputs.join('\n');
|
||
sections.push(`${s.additionalUserInputs}\n${escapeTemplateChars(userInputsStr)}`);
|
||
}
|
||
|
||
// 6. Instructions header + instruction_template content
|
||
const processedTemplate = replaceTemplatePlaceholders(
|
||
this.step.instructionTemplate,
|
||
this.step,
|
||
this.context,
|
||
);
|
||
sections.push(`${s.instructions}\n${processedTemplate}`);
|
||
|
||
// 7. Status Output Rules (for tag-based detection in Phase 1)
|
||
if (hasTagBasedRules(this.step)) {
|
||
const statusRulesPrompt = generateStatusRulesFromRules(
|
||
this.step.name,
|
||
this.step.rules!,
|
||
language,
|
||
{ interactive: this.context.interactive },
|
||
);
|
||
sections.push(statusRulesPrompt);
|
||
}
|
||
|
||
return sections.join('\n\n');
|
||
}
|
||
|
||
private renderWorkflowContext(language: Language): string {
|
||
const s = SECTION_STRINGS[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}`,
|
||
];
|
||
|
||
// If step has report config, include Report Directory path and phase note
|
||
if (this.step.report && this.context.reportDir) {
|
||
const reportContext = renderReportContext(this.step.report, this.context.reportDir, language);
|
||
lines.push(reportContext);
|
||
lines.push('');
|
||
lines.push(s.phaseNote);
|
||
}
|
||
|
||
return lines.join('\n');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Render report context info for Workflow Context section.
|
||
* Used by ReportInstructionBuilder.
|
||
*/
|
||
export function renderReportContext(
|
||
report: string | ReportConfig[] | ReportObjectConfig,
|
||
reportDir: string,
|
||
language: Language,
|
||
): string {
|
||
const s = SECTION_STRINGS[language];
|
||
const lines: string[] = [
|
||
`- ${s.reportDirectory}: ${reportDir}/`,
|
||
];
|
||
|
||
if (typeof report === 'string') {
|
||
lines.push(`- ${s.reportFile}: ${reportDir}/${report}`);
|
||
} else if (isReportObjectConfig(report)) {
|
||
lines.push(`- ${s.reportFile}: ${reportDir}/${report.name}`);
|
||
} else {
|
||
lines.push(`- ${s.reportFiles}:`);
|
||
for (const file of report) {
|
||
lines.push(` - ${file.label}: ${reportDir}/${file.path}`);
|
||
}
|
||
}
|
||
|
||
return lines.join('\n');
|
||
}
|
||
|
||
/**
|
||
* Generate report output instructions from step.report config.
|
||
* Returns undefined if step has no report or no reportDir.
|
||
*/
|
||
export function renderReportOutputInstruction(
|
||
step: WorkflowStep,
|
||
context: InstructionContext,
|
||
language: Language,
|
||
): string | undefined {
|
||
if (!step.report || !context.reportDir) return undefined;
|
||
|
||
const s = REPORT_OUTPUT_STRINGS[language];
|
||
const isMulti = Array.isArray(step.report);
|
||
const heading = isMulti ? s.multiHeading : s.singleHeading;
|
||
const appendRule = s.appendRule.replace('{step_iteration}', String(context.stepIteration));
|
||
|
||
return [heading, s.createRule, appendRule].join('\n');
|
||
}
|