111 lines
3.8 KiB
TypeScript
111 lines
3.8 KiB
TypeScript
/**
|
||
* Instruction template builder for workflow steps
|
||
*
|
||
* Builds the instruction string for agent execution by replacing
|
||
* template placeholders with actual values.
|
||
*/
|
||
|
||
import { join } from 'node:path';
|
||
import type { WorkflowStep, AgentResponse } from '../models/types.js';
|
||
import { getGitDiff } from '../agents/runner.js';
|
||
|
||
/**
|
||
* Context for building instruction from template.
|
||
*/
|
||
export interface InstructionContext {
|
||
/** The main task/prompt */
|
||
task: string;
|
||
/** Current iteration number (workflow-wide turn count) */
|
||
iteration: number;
|
||
/** Maximum iterations allowed */
|
||
maxIterations: number;
|
||
/** Current step's iteration number (how many times this step has been executed) */
|
||
stepIteration: number;
|
||
/** Working directory (agent work dir, may be a worktree) */
|
||
cwd: string;
|
||
/** Project root directory (where .takt/ lives). Defaults to cwd. */
|
||
projectCwd?: string;
|
||
/** User inputs accumulated during workflow */
|
||
userInputs: string[];
|
||
/** Previous step output if available */
|
||
previousOutput?: AgentResponse;
|
||
/** Report directory path */
|
||
reportDir?: string;
|
||
}
|
||
|
||
/**
|
||
* Escape special characters in dynamic content to prevent template injection.
|
||
*/
|
||
function escapeTemplateChars(str: string): string {
|
||
return str.replace(/\{/g, '{').replace(/\}/g, '}');
|
||
}
|
||
|
||
/**
|
||
* Build instruction from template with context values.
|
||
*
|
||
* Supported placeholders:
|
||
* - {task} - The main task/prompt
|
||
* - {iteration} - Current iteration number (workflow-wide turn count)
|
||
* - {max_iterations} - Maximum iterations allowed
|
||
* - {step_iteration} - Current step's iteration number (how many times this step has been executed)
|
||
* - {previous_response} - Output from previous step (if passPreviousResponse is true)
|
||
* - {git_diff} - Current git diff output
|
||
* - {user_inputs} - Accumulated user inputs
|
||
* - {report_dir} - Report directory name (e.g., "20250126-143052-task-summary")
|
||
*/
|
||
export function buildInstruction(
|
||
step: WorkflowStep,
|
||
context: InstructionContext
|
||
): string {
|
||
let instruction = step.instructionTemplate;
|
||
|
||
// Replace {task}
|
||
instruction = instruction.replace(/\{task\}/g, escapeTemplateChars(context.task));
|
||
|
||
// Replace {iteration}, {max_iterations}, and {step_iteration}
|
||
instruction = instruction.replace(/\{iteration\}/g, String(context.iteration));
|
||
instruction = instruction.replace(/\{max_iterations\}/g, String(context.maxIterations));
|
||
instruction = instruction.replace(/\{step_iteration\}/g, String(context.stepIteration));
|
||
|
||
// Replace {previous_response}
|
||
if (step.passPreviousResponse) {
|
||
if (context.previousOutput) {
|
||
instruction = instruction.replace(
|
||
/\{previous_response\}/g,
|
||
escapeTemplateChars(context.previousOutput.content)
|
||
);
|
||
} else {
|
||
instruction = instruction.replace(/\{previous_response\}/g, '');
|
||
}
|
||
}
|
||
|
||
// Replace {git_diff}
|
||
const gitDiff = getGitDiff(context.cwd);
|
||
instruction = instruction.replace(/\{git_diff\}/g, gitDiff);
|
||
|
||
// Replace {user_inputs}
|
||
const userInputsStr = context.userInputs.join('\n');
|
||
instruction = instruction.replace(
|
||
/\{user_inputs\}/g,
|
||
escapeTemplateChars(userInputsStr)
|
||
);
|
||
|
||
// Replace .takt/reports/{report_dir} with absolute path first,
|
||
// then replace standalone {report_dir} with the directory name.
|
||
// This ensures agents always use the correct project root for reports,
|
||
// even when their cwd is a worktree.
|
||
if (context.reportDir) {
|
||
const projectRoot = context.projectCwd ?? context.cwd;
|
||
const reportDirFullPath = join(projectRoot, '.takt', 'reports', context.reportDir);
|
||
instruction = instruction.replace(/\.takt\/reports\/\{report_dir\}/g, reportDirFullPath);
|
||
instruction = instruction.replace(/\{report_dir\}/g, context.reportDir);
|
||
}
|
||
|
||
// Append status_rules_prompt if present
|
||
if (step.statusRulesPrompt) {
|
||
instruction = `${instruction}\n\n${step.statusRulesPrompt}`;
|
||
}
|
||
|
||
return instruction;
|
||
}
|