feat(core): 实现动态提示词模板系统
- 新增 prompt-template 模块,支持运行时变量替换
- 支持 ${variable}、${obj.prop}、${cond ? "a" : "b"} 语法
- AgentInfo 新增 promptTemplate 字段标记动态模板
- Plan Agent 提示词改用模板语法,支持动态工具名和计划文件路径
- AgentExecutor.buildSystemPrompt 集成模板渲染
- 新增 27 个单元测试验证模板功能
This commit is contained in:
@@ -17,6 +17,7 @@ import type {
|
||||
} from './types.js';
|
||||
import { checkBashPermission, isPathInAllowedWritePaths } from './permission-merger.js';
|
||||
import { getProviderRegistry } from '../provider/index.js';
|
||||
import { renderTemplate, createPlanContext } from './prompt-template/index.js';
|
||||
|
||||
/**
|
||||
* Agent 执行器
|
||||
@@ -286,10 +287,19 @@ export class AgentExecutor {
|
||||
|
||||
/**
|
||||
* 构建系统提示词
|
||||
* 如果 Agent 启用了 promptTemplate,则动态渲染模板变量
|
||||
*/
|
||||
private buildSystemPrompt(): string {
|
||||
// 如果 Agent 有自定义 prompt,使用它
|
||||
// 如果 Agent 有自定义 prompt
|
||||
if (this.agentInfo.prompt) {
|
||||
// 如果启用了模板渲染,动态解析变量
|
||||
if (this.agentInfo.promptTemplate) {
|
||||
const context = createPlanContext({
|
||||
workdir: process.cwd(),
|
||||
isSubagent: this.agentInfo.mode === 'subagent',
|
||||
});
|
||||
return renderTemplate(this.agentInfo.prompt, context);
|
||||
}
|
||||
return this.agentInfo.prompt;
|
||||
}
|
||||
|
||||
|
||||
@@ -60,3 +60,28 @@ export {
|
||||
|
||||
// System Prompt
|
||||
export { SystemPrompt } from './system-prompt.js';
|
||||
|
||||
// Prompt Template
|
||||
export {
|
||||
renderTemplate,
|
||||
render,
|
||||
createDefaultContext,
|
||||
createPlanContext,
|
||||
checkPlanFileExists,
|
||||
DEFAULT_TOOL_NAMES,
|
||||
generatePlanPrompt,
|
||||
resolveAgentPrompt,
|
||||
PLAN_MODE_TEMPLATE,
|
||||
PLAN_MODE_SUBAGENT_TEMPLATE,
|
||||
PLAN_MODE_REMINDER_TEMPLATE,
|
||||
} from './prompt-template/index.js';
|
||||
|
||||
export type {
|
||||
PromptContext,
|
||||
PromptTemplate,
|
||||
RenderOptions,
|
||||
ToolNameMapping,
|
||||
PlanModeContext,
|
||||
EnvContext,
|
||||
AgentContext,
|
||||
} from './prompt-template/index.js';
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import type { AgentInfo } from '../types.js';
|
||||
|
||||
/**
|
||||
* Plan Agent 专用提示词
|
||||
* Plan Agent 专用提示词模板
|
||||
*
|
||||
* 变量映射:
|
||||
* - GLOB_TOOL_NAME -> glob
|
||||
* - GREP_TOOL_NAME -> grep_content
|
||||
* - READ_TOOL_NAME -> read_file
|
||||
* - BASH_TOOL_NAME -> bash
|
||||
* 使用 ${variable} 语法支持动态变量替换:
|
||||
* - ${tools.glob} -> glob
|
||||
* - ${tools.grep} -> grep_content
|
||||
* - ${tools.read} -> read_file
|
||||
* - ${tools.bash} -> bash
|
||||
* - ${plan.planFilePath} -> 计划文件路径
|
||||
* - ${plan.planExists ? "..." : "..."} -> 条件渲染
|
||||
*/
|
||||
const PLAN_PROMPT = `You are a software architect and planning specialist for Claude Code. Your role is to explore the codebase and design implementation plans.
|
||||
const PLAN_PROMPT_TEMPLATE = `You are a software architect and planning specialist for Claude Code. Your role is to explore the codebase and design implementation plans.
|
||||
|
||||
=== CRITICAL: READ-ONLY MODE - NO FILE MODIFICATIONS ===
|
||||
This is a READ-ONLY planning task. You are STRICTLY PROHIBITED from:
|
||||
@@ -23,6 +25,11 @@ This is a READ-ONLY planning task. You are STRICTLY PROHIBITED from:
|
||||
|
||||
Your role is EXCLUSIVELY to explore the codebase and design implementation plans. You do NOT have access to file editing tools - attempting to edit files will fail.
|
||||
|
||||
## Plan File Info
|
||||
\${plan.planExists ? "A plan file already exists at \${plan.planFilePath}. You can read it and make incremental edits using the \${tools.edit} tool." : "No plan file exists yet. You should create your plan at \${plan.planFilePath} using the \${tools.write} tool."}
|
||||
|
||||
You should build your plan incrementally by writing to or editing this file. NOTE that this is the only file you are allowed to edit - other than this you are only allowed to take READ-ONLY actions.
|
||||
|
||||
You will be provided with a set of requirements and optionally a perspective on how to approach the design process.
|
||||
|
||||
## Your Process
|
||||
@@ -31,12 +38,12 @@ You will be provided with a set of requirements and optionally a perspective on
|
||||
|
||||
2. **Explore Thoroughly**:
|
||||
- Read any files provided to you in the initial prompt
|
||||
- Find existing patterns and conventions using glob, grep_content, and read_file
|
||||
- Find existing patterns and conventions using \${tools.glob}, \${tools.grep}, and \${tools.read}
|
||||
- Understand the current architecture
|
||||
- Identify similar features as reference
|
||||
- Trace through relevant code paths
|
||||
- Use bash ONLY for read-only operations (ls, git status, git log, git diff, find, cat, head, tail)
|
||||
- NEVER use bash for: mkdir, touch, rm, cp, mv, git add, git commit, npm install, pip install, or any file creation/modification
|
||||
- Use \${tools.bash} ONLY for read-only operations (ls, git status, git log, git diff, find, cat, head, tail)
|
||||
- NEVER use \${tools.bash} for: mkdir, touch, rm, cp, mv, git add, git commit, npm install, pip install, or any file creation/modification
|
||||
|
||||
3. **Design Solution**:
|
||||
- Create implementation approach based on your assigned perspective
|
||||
@@ -58,13 +65,16 @@ List 3-5 files most critical for implementing this plan:
|
||||
- path/to/file2.ts - [Brief reason: e.g., "Interfaces to implement"]
|
||||
- path/to/file3.ts - [Brief reason: e.g., "Pattern to follow"]
|
||||
|
||||
REMEMBER: You can ONLY explore and plan. You CANNOT and MUST NOT write, edit, or modify any files. You do NOT have access to file editing tools.`;
|
||||
Answer the user's query comprehensively, using the \${tools.askUserQuestion} tool if you need to ask clarifying questions.
|
||||
|
||||
REMEMBER: You can ONLY explore and plan. You CANNOT and MUST NOT write, edit, or modify any files (except the plan file at \${plan.planFilePath}).`;
|
||||
|
||||
/**
|
||||
* 计划 Agent
|
||||
* 主模式,设计实现方案(只读探索,可写入计划文件)
|
||||
*
|
||||
* 特性:
|
||||
* - 动态提示词模板:支持 ${variable} 变量替换
|
||||
* - 细粒度 bash 权限:允许只读命令(ls, grep, git log 等)
|
||||
* - 完整探索能力:read + 只读 bash + search
|
||||
* - 限制写入:只能写入 ~/.ai-terminal-assistant/plan/ 目录
|
||||
@@ -72,7 +82,8 @@ REMEMBER: You can ONLY explore and plan. You CANNOT and MUST NOT write, edit, or
|
||||
export const planAgent: Omit<AgentInfo, 'name'> = {
|
||||
description: '计划模式,设计实现方案(只读探索,可写入计划文件)',
|
||||
mode: 'primary',
|
||||
prompt: PLAN_PROMPT,
|
||||
prompt: PLAN_PROMPT_TEMPLATE,
|
||||
promptTemplate: true, // 启用动态模板渲染
|
||||
tools: {
|
||||
enabled: [
|
||||
// 文件操作,限制只能写入 plan 目录
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* 提示词模板系统
|
||||
*
|
||||
* 提供动态提示词生成能力,支持:
|
||||
* - ${variable} 变量替换
|
||||
* - ${obj.prop} 嵌套属性访问
|
||||
* - ${condition ? "trueValue" : "falseValue"} 条件表达式
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { renderTemplate, createPlanContext } from './prompt-template';
|
||||
*
|
||||
* const context = createPlanContext({ workdir: '/path/to/project' });
|
||||
* const prompt = renderTemplate(PLAN_MODE_TEMPLATE.template, context);
|
||||
* ```
|
||||
*/
|
||||
|
||||
// 类型导出
|
||||
export type {
|
||||
PromptContext,
|
||||
PromptTemplate,
|
||||
RenderOptions,
|
||||
ToolNameMapping,
|
||||
PlanModeContext,
|
||||
EnvContext,
|
||||
AgentContext,
|
||||
} from './types.js';
|
||||
|
||||
// 渲染器导出
|
||||
export {
|
||||
renderTemplate,
|
||||
render,
|
||||
createDefaultContext,
|
||||
createPlanContext,
|
||||
checkPlanFileExists,
|
||||
DEFAULT_TOOL_NAMES,
|
||||
} from './renderer.js';
|
||||
|
||||
// 模板导出
|
||||
export {
|
||||
PLAN_MODE_TEMPLATE,
|
||||
PLAN_MODE_SUBAGENT_TEMPLATE,
|
||||
PLAN_MODE_REMINDER_TEMPLATE,
|
||||
} from './templates/index.js';
|
||||
|
||||
// 便捷函数
|
||||
import { renderTemplate, createPlanContext } from './renderer.js';
|
||||
import { PLAN_MODE_TEMPLATE, PLAN_MODE_SUBAGENT_TEMPLATE } from './templates/index.js';
|
||||
|
||||
/**
|
||||
* 生成 Plan 模式提示词
|
||||
*
|
||||
* @param options 配置选项
|
||||
* @returns 渲染后的提示词
|
||||
*/
|
||||
export function generatePlanPrompt(options: {
|
||||
isSubagent?: boolean;
|
||||
workdir?: string;
|
||||
planFilePath?: string;
|
||||
}): string {
|
||||
const context = createPlanContext(options);
|
||||
const template = options.isSubagent ? PLAN_MODE_SUBAGENT_TEMPLATE : PLAN_MODE_TEMPLATE;
|
||||
return renderTemplate(template.template, context);
|
||||
}
|
||||
|
||||
/**
|
||||
* 动态解析 Agent 提示词
|
||||
* 在运行时根据上下文生成最终提示词
|
||||
*
|
||||
* @param basePrompt 基础提示词(可能包含模板变量)
|
||||
* @param options 上下文选项
|
||||
* @returns 渲染后的提示词
|
||||
*/
|
||||
export function resolveAgentPrompt(
|
||||
basePrompt: string,
|
||||
options: {
|
||||
workdir?: string;
|
||||
planFilePath?: string;
|
||||
isSubagent?: boolean;
|
||||
isPlanMode?: boolean;
|
||||
}
|
||||
): string {
|
||||
const context = createPlanContext({
|
||||
workdir: options.workdir,
|
||||
planFilePath: options.planFilePath,
|
||||
isSubagent: options.isSubagent,
|
||||
});
|
||||
|
||||
// 如果不是 Plan 模式,设置 plan.isActive = false
|
||||
if (!options.isPlanMode) {
|
||||
context.plan.isActive = false;
|
||||
}
|
||||
|
||||
return renderTemplate(basePrompt, context);
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
/**
|
||||
* 提示词模板渲染器
|
||||
*
|
||||
* 支持的语法:
|
||||
* - ${variable} - 简单变量替换
|
||||
* - ${obj.prop} - 嵌套属性访问
|
||||
* - ${condition ? "trueValue" : "falseValue"} - 条件表达式
|
||||
*/
|
||||
|
||||
import type { PromptContext, PromptTemplate, RenderOptions, ToolNameMapping } from './types.js';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 获取嵌套属性值
|
||||
*/
|
||||
function getNestedValue(obj: Record<string, unknown>, pathStr: string): unknown {
|
||||
const parts = pathStr.split('.');
|
||||
let current: unknown = obj;
|
||||
|
||||
for (const part of parts) {
|
||||
if (current === null || current === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof current === 'object') {
|
||||
current = (current as Record<string, unknown>)[part];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全的表达式求值器
|
||||
* 支持简单的属性访问和三元运算符
|
||||
*/
|
||||
function evaluateExpression(
|
||||
expr: string,
|
||||
context: PromptContext,
|
||||
options: RenderOptions = {}
|
||||
): string {
|
||||
// 移除首尾空格
|
||||
expr = expr.trim();
|
||||
|
||||
// 处理三元运算符: condition ? trueValue : falseValue
|
||||
// 需要处理嵌套的情况,使用更精确的匹配
|
||||
const ternaryMatch = findTernaryOperator(expr);
|
||||
if (ternaryMatch) {
|
||||
const { condition, trueValue, falseValue } = ternaryMatch;
|
||||
const conditionResult = evaluateExpression(condition, context, options);
|
||||
// 将结果转换为布尔值
|
||||
const isTruthy =
|
||||
conditionResult !== '' &&
|
||||
conditionResult !== 'false' &&
|
||||
conditionResult !== 'undefined' &&
|
||||
conditionResult !== 'null' &&
|
||||
conditionResult !== '0';
|
||||
return isTruthy
|
||||
? evaluateExpression(trueValue, context, options)
|
||||
: evaluateExpression(falseValue, context, options);
|
||||
}
|
||||
|
||||
// 处理字符串字面量: "string" 或 'string' 或 `string`
|
||||
const stringMatch = expr.match(/^["'`]([\s\S]*)["'`]$/);
|
||||
if (stringMatch) {
|
||||
// 递归处理字符串中的变量插值
|
||||
return renderTemplate(stringMatch[1], context, options);
|
||||
}
|
||||
|
||||
// 处理布尔字面量
|
||||
if (expr === 'true') return 'true';
|
||||
if (expr === 'false') return '';
|
||||
|
||||
// 处理属性访问: obj.prop.subprop
|
||||
const value = getNestedValue(context as unknown as Record<string, unknown>, expr);
|
||||
|
||||
if (value === undefined) {
|
||||
if (options.throwOnUndefined) {
|
||||
throw new Error(`Undefined variable: ${expr}`);
|
||||
}
|
||||
if (options.debug) {
|
||||
console.warn(`[PromptTemplate] Undefined variable: ${expr}`);
|
||||
}
|
||||
return options.undefinedValue ?? '';
|
||||
}
|
||||
|
||||
// 布尔值转换
|
||||
if (typeof value === 'boolean') {
|
||||
return value ? 'true' : '';
|
||||
}
|
||||
|
||||
return String(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找三元运算符的各个部分
|
||||
* 处理嵌套引号的情况
|
||||
*/
|
||||
function findTernaryOperator(
|
||||
expr: string
|
||||
): { condition: string; trueValue: string; falseValue: string } | null {
|
||||
let depth = 0;
|
||||
let inString: string | null = null;
|
||||
let questionPos = -1;
|
||||
let colonPos = -1;
|
||||
|
||||
for (let i = 0; i < expr.length; i++) {
|
||||
const char = expr[i];
|
||||
const prevChar = i > 0 ? expr[i - 1] : '';
|
||||
|
||||
// 处理字符串边界
|
||||
if ((char === '"' || char === "'" || char === '`') && prevChar !== '\\') {
|
||||
if (inString === null) {
|
||||
inString = char;
|
||||
} else if (inString === char) {
|
||||
inString = null;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// 如果在字符串中,跳过
|
||||
if (inString !== null) continue;
|
||||
|
||||
// 处理括号深度
|
||||
if (char === '(' || char === '[' || char === '{') {
|
||||
depth++;
|
||||
} else if (char === ')' || char === ']' || char === '}') {
|
||||
depth--;
|
||||
}
|
||||
|
||||
// 只在顶层寻找 ? 和 :
|
||||
if (depth === 0) {
|
||||
if (char === '?' && questionPos === -1) {
|
||||
questionPos = i;
|
||||
} else if (char === ':' && questionPos !== -1 && colonPos === -1) {
|
||||
colonPos = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (questionPos === -1 || colonPos === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
condition: expr.substring(0, questionPos).trim(),
|
||||
trueValue: expr.substring(questionPos + 1, colonPos).trim(),
|
||||
falseValue: expr.substring(colonPos + 1).trim(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染提示词模板
|
||||
*
|
||||
* @param template 模板字符串
|
||||
* @param context 上下文变量
|
||||
* @param options 渲染选项
|
||||
* @returns 渲染后的字符串
|
||||
*/
|
||||
export function renderTemplate(
|
||||
template: string,
|
||||
context: PromptContext,
|
||||
options: RenderOptions = {}
|
||||
): string {
|
||||
// 匹配 ${...} 模式,支持嵌套括号和字符串
|
||||
const result: string[] = [];
|
||||
let i = 0;
|
||||
|
||||
while (i < template.length) {
|
||||
// 查找 ${
|
||||
const start = template.indexOf('${', i);
|
||||
if (start === -1) {
|
||||
result.push(template.substring(i));
|
||||
break;
|
||||
}
|
||||
|
||||
// 添加 ${ 之前的文本
|
||||
result.push(template.substring(i, start));
|
||||
|
||||
// 查找匹配的 }
|
||||
let depth = 1;
|
||||
let j = start + 2;
|
||||
let inString: string | null = null;
|
||||
|
||||
while (j < template.length && depth > 0) {
|
||||
const char = template[j];
|
||||
const prevChar = j > 0 ? template[j - 1] : '';
|
||||
|
||||
// 处理字符串边界
|
||||
if ((char === '"' || char === "'" || char === '`') && prevChar !== '\\') {
|
||||
if (inString === null) {
|
||||
inString = char;
|
||||
} else if (inString === char) {
|
||||
inString = null;
|
||||
}
|
||||
} else if (inString === null) {
|
||||
if (char === '{') depth++;
|
||||
else if (char === '}') depth--;
|
||||
}
|
||||
|
||||
j++;
|
||||
}
|
||||
|
||||
if (depth !== 0) {
|
||||
// 未找到匹配的 },保留原文
|
||||
result.push(template.substring(start, j));
|
||||
} else {
|
||||
// 提取表达式并求值
|
||||
const expression = template.substring(start + 2, j - 1);
|
||||
try {
|
||||
const value = evaluateExpression(expression, context, options);
|
||||
result.push(value);
|
||||
} catch (error) {
|
||||
if (options.debug) {
|
||||
console.warn(`[PromptTemplate] Expression evaluation failed: ${expression}`, error);
|
||||
}
|
||||
result.push(`\${${expression}}`); // 保留原始文本
|
||||
}
|
||||
}
|
||||
|
||||
i = j;
|
||||
}
|
||||
|
||||
return result.join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染 PromptTemplate 对象
|
||||
*/
|
||||
export function render(
|
||||
promptTemplate: PromptTemplate,
|
||||
context: PromptContext,
|
||||
options?: RenderOptions
|
||||
): string {
|
||||
return renderTemplate(promptTemplate.template, context, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认工具名称映射
|
||||
*/
|
||||
export const DEFAULT_TOOL_NAMES: ToolNameMapping = {
|
||||
glob: 'glob',
|
||||
grep: 'grep_content',
|
||||
read: 'read_file',
|
||||
write: 'write_file',
|
||||
edit: 'edit_file',
|
||||
bash: 'bash',
|
||||
askUserQuestion: 'ask_user_question',
|
||||
exitPlanMode: 'exit_plan_mode',
|
||||
enterPlanMode: 'enter_plan_mode',
|
||||
task: 'task',
|
||||
todoRead: 'todoread',
|
||||
todoWrite: 'todowrite',
|
||||
webSearch: 'web_search',
|
||||
webExtract: 'web_extract',
|
||||
gitStatus: 'git_status',
|
||||
gitDiff: 'git_diff',
|
||||
gitLog: 'git_log',
|
||||
gitBranch: 'git_branch',
|
||||
repoMap: 'repo_map',
|
||||
};
|
||||
|
||||
/**
|
||||
* 创建默认的提示词上下文
|
||||
*/
|
||||
export function createDefaultContext(overrides?: Partial<PromptContext>): PromptContext {
|
||||
const homeDir = process.env.HOME || process.env.USERPROFILE || '';
|
||||
const workdir = process.cwd();
|
||||
|
||||
return {
|
||||
tools: {
|
||||
...DEFAULT_TOOL_NAMES,
|
||||
...overrides?.tools,
|
||||
},
|
||||
plan: {
|
||||
isActive: false,
|
||||
planExists: false,
|
||||
planFilePath: path.join(homeDir, '.ai-terminal-assistant', 'plan', 'plan.md'),
|
||||
allowedWritePaths: [path.join(homeDir, '.ai-terminal-assistant', 'plan', '*')],
|
||||
...overrides?.plan,
|
||||
},
|
||||
env: {
|
||||
workdir,
|
||||
isGitRepo: fs.existsSync(path.join(workdir, '.git')),
|
||||
platform: process.platform,
|
||||
date: new Date().toISOString().split('T')[0],
|
||||
homeDir,
|
||||
...overrides?.env,
|
||||
},
|
||||
agent: {
|
||||
name: 'build',
|
||||
mode: 'primary',
|
||||
isSubagent: false,
|
||||
...overrides?.agent,
|
||||
},
|
||||
custom: overrides?.custom,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查计划文件是否存在
|
||||
*/
|
||||
export function checkPlanFileExists(planFilePath: string): boolean {
|
||||
try {
|
||||
fs.accessSync(planFilePath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 Plan 模式上下文
|
||||
*/
|
||||
export function createPlanContext(options: {
|
||||
planFilePath?: string;
|
||||
workdir?: string;
|
||||
isSubagent?: boolean;
|
||||
}): PromptContext {
|
||||
const homeDir = process.env.HOME || process.env.USERPROFILE || '';
|
||||
const planPath =
|
||||
options.planFilePath || path.join(homeDir, '.ai-terminal-assistant', 'plan', 'plan.md');
|
||||
const workdir = options.workdir || process.cwd();
|
||||
|
||||
return createDefaultContext({
|
||||
plan: {
|
||||
isActive: true,
|
||||
planExists: checkPlanFileExists(planPath),
|
||||
planFilePath: planPath,
|
||||
allowedWritePaths: [path.join(homeDir, '.ai-terminal-assistant', 'plan', '*')],
|
||||
},
|
||||
env: {
|
||||
workdir,
|
||||
isGitRepo: fs.existsSync(path.join(workdir, '.git')),
|
||||
platform: process.platform,
|
||||
date: new Date().toISOString().split('T')[0],
|
||||
homeDir,
|
||||
},
|
||||
agent: {
|
||||
name: 'plan',
|
||||
mode: options.isSubagent ? 'subagent' : 'primary',
|
||||
isSubagent: options.isSubagent || false,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* 提示词模板导出
|
||||
*/
|
||||
|
||||
export {
|
||||
PLAN_MODE_TEMPLATE,
|
||||
PLAN_MODE_SUBAGENT_TEMPLATE,
|
||||
PLAN_MODE_REMINDER_TEMPLATE,
|
||||
} from './plan-mode.js';
|
||||
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* Plan 模式提示词模板
|
||||
*
|
||||
* 使用 ${variable} 语法支持动态变量替换
|
||||
*/
|
||||
|
||||
import type { PromptTemplate } from '../types.js';
|
||||
|
||||
/**
|
||||
* Plan 模式主提示词模板
|
||||
*/
|
||||
export const PLAN_MODE_TEMPLATE: PromptTemplate = {
|
||||
name: 'plan-mode',
|
||||
description: 'Plan 模式主提示词 - 用于代码探索和实现方案设计',
|
||||
requiredVariables: ['tools', 'plan'],
|
||||
template: `You are a software architect and planning specialist for Claude Code. Your role is to explore the codebase and design implementation plans.
|
||||
|
||||
=== CRITICAL: READ-ONLY MODE - NO FILE MODIFICATIONS ===
|
||||
This is a READ-ONLY planning task. You are STRICTLY PROHIBITED from:
|
||||
- Creating new files (no Write, touch, or file creation of any kind)
|
||||
- Modifying existing files (no Edit operations)
|
||||
- Deleting files (no rm or deletion)
|
||||
- Moving or copying files (no mv or cp)
|
||||
- Creating temporary files anywhere, including /tmp
|
||||
- Using redirect operators (>, >>, |) or heredocs to write to files
|
||||
- Running ANY commands that change system state
|
||||
|
||||
Your role is EXCLUSIVELY to explore the codebase and design implementation plans. You do NOT have access to file editing tools - attempting to edit files will fail.
|
||||
|
||||
## Plan File Info
|
||||
\${plan.planExists ? "A plan file already exists at \${plan.planFilePath}. You can read it and make incremental edits using the \${tools.edit} tool." : "No plan file exists yet. You should create your plan at \${plan.planFilePath} using the \${tools.write} tool."}
|
||||
|
||||
You should build your plan incrementally by writing to or editing this file. NOTE that this is the only file you are allowed to edit - other than this you are only allowed to take READ-ONLY actions.
|
||||
|
||||
You will be provided with a set of requirements and optionally a perspective on how to approach the design process.
|
||||
|
||||
## Your Process
|
||||
|
||||
1. **Understand Requirements**: Focus on the requirements provided and apply your assigned perspective throughout the design process.
|
||||
|
||||
2. **Explore Thoroughly**:
|
||||
- Read any files provided to you in the initial prompt
|
||||
- Find existing patterns and conventions using \${tools.glob}, \${tools.grep}, and \${tools.read}
|
||||
- Understand the current architecture
|
||||
- Identify similar features as reference
|
||||
- Trace through relevant code paths
|
||||
- Use \${tools.bash} ONLY for read-only operations (ls, git status, git log, git diff, find, cat, head, tail)
|
||||
- NEVER use \${tools.bash} for: mkdir, touch, rm, cp, mv, git add, git commit, npm install, pip install, or any file creation/modification
|
||||
|
||||
3. **Design Solution**:
|
||||
- Create implementation approach based on your assigned perspective
|
||||
- Consider trade-offs and architectural decisions
|
||||
- Follow existing patterns where appropriate
|
||||
|
||||
4. **Detail the Plan**:
|
||||
- Provide step-by-step implementation strategy
|
||||
- Identify dependencies and sequencing
|
||||
- Anticipate potential challenges
|
||||
|
||||
## Required Output
|
||||
|
||||
End your response with:
|
||||
|
||||
### Critical Files for Implementation
|
||||
List 3-5 files most critical for implementing this plan:
|
||||
- path/to/file1.ts - [Brief reason: e.g., "Core logic to modify"]
|
||||
- path/to/file2.ts - [Brief reason: e.g., "Interfaces to implement"]
|
||||
- path/to/file3.ts - [Brief reason: e.g., "Pattern to follow"]
|
||||
|
||||
Answer the user's query comprehensively, using the \${tools.askUserQuestion} tool if you need to ask clarifying questions.
|
||||
|
||||
REMEMBER: You can ONLY explore and plan. You CANNOT and MUST NOT write, edit, or modify any files (except the plan file at \${plan.planFilePath}).`,
|
||||
};
|
||||
|
||||
/**
|
||||
* Plan 模式子代理提示词模板(简化版)
|
||||
* 用于作为子代理时的精简提示
|
||||
*/
|
||||
export const PLAN_MODE_SUBAGENT_TEMPLATE: PromptTemplate = {
|
||||
name: 'plan-mode-subagent',
|
||||
description: 'Plan 模式子代理提示词(简化版)',
|
||||
requiredVariables: ['tools', 'plan'],
|
||||
template: `Plan mode is active. The user indicated that they do not want you to execute yet -- you MUST NOT make any edits, run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supercedes any other instructions you have received.
|
||||
|
||||
## Plan File Info
|
||||
\${plan.planExists ? "A plan file already exists at \${plan.planFilePath}. You can read it and make incremental edits using the \${tools.edit} tool if you need to." : "No plan file exists yet. You should create your plan at \${plan.planFilePath} using the \${tools.write} tool if you need to."}
|
||||
|
||||
You should build your plan incrementally by writing to or editing this file. NOTE that this is the only file you are allowed to edit - other than this you are only allowed to take READ-ONLY actions.
|
||||
|
||||
## Available Tools
|
||||
- Use \${tools.glob}, \${tools.grep}, and \${tools.read} for code exploration
|
||||
- Use \${tools.bash} ONLY for read-only operations
|
||||
- Use \${tools.askUserQuestion} to ask clarifying questions
|
||||
|
||||
Answer the user's query comprehensively, exploring the codebase as needed.
|
||||
|
||||
REMEMBER: You can ONLY explore and plan. Do NOT modify any files except the plan file.`,
|
||||
};
|
||||
|
||||
/**
|
||||
* Plan 模式系统提醒模板(用于注入到消息中)
|
||||
*/
|
||||
export const PLAN_MODE_REMINDER_TEMPLATE: PromptTemplate = {
|
||||
name: 'plan-mode-reminder',
|
||||
description: 'Plan 模式系统提醒',
|
||||
template: `<system-reminder>
|
||||
Plan mode is active. The user indicated that they do not want you to execute yet -- you MUST NOT make any edits (with the exception of the plan file mentioned below), run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supercedes any other instructions you have received.
|
||||
|
||||
## Plan File Info:
|
||||
\${plan.planExists ? "A plan file already exists at \${plan.planFilePath}. You can read it and make incremental edits." : "No plan file exists yet. You should create your plan at \${plan.planFilePath}."}
|
||||
You should build your plan incrementally by writing to or editing this file. NOTE that this is the only file you are allowed to edit - other than this you are only allowed to take READ-ONLY actions.
|
||||
</system-reminder>`,
|
||||
};
|
||||
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* 提示词模板系统 - 类型定义
|
||||
*
|
||||
* 支持动态变量替换和条件表达式的提示词模板系统
|
||||
*/
|
||||
|
||||
/**
|
||||
* 工具名称映射
|
||||
* 用于在模板中引用工具名称,便于重命名
|
||||
*/
|
||||
export interface ToolNameMapping {
|
||||
glob: string;
|
||||
grep: string;
|
||||
read: string;
|
||||
write: string;
|
||||
edit: string;
|
||||
bash: string;
|
||||
askUserQuestion: string;
|
||||
exitPlanMode: string;
|
||||
enterPlanMode: string;
|
||||
task: string;
|
||||
todoRead: string;
|
||||
todoWrite: string;
|
||||
webSearch: string;
|
||||
webExtract: string;
|
||||
gitStatus: string;
|
||||
gitDiff: string;
|
||||
gitLog: string;
|
||||
gitBranch: string;
|
||||
repoMap: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Plan 模式上下文
|
||||
*/
|
||||
export interface PlanModeContext {
|
||||
/** 是否处于 Plan 模式 */
|
||||
isActive: boolean;
|
||||
/** 计划文件是否存在 */
|
||||
planExists: boolean;
|
||||
/** 计划文件路径 */
|
||||
planFilePath: string;
|
||||
/** 允许写入的路径列表 */
|
||||
allowedWritePaths: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 环境信息上下文
|
||||
*/
|
||||
export interface EnvContext {
|
||||
/** 工作目录 */
|
||||
workdir: string;
|
||||
/** 是否是 Git 仓库 */
|
||||
isGitRepo: boolean;
|
||||
/** 操作系统平台 */
|
||||
platform: string;
|
||||
/** 当前日期 */
|
||||
date: string;
|
||||
/** 主目录 */
|
||||
homeDir: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Agent 信息上下文
|
||||
*/
|
||||
export interface AgentContext {
|
||||
/** Agent 名称 */
|
||||
name: string;
|
||||
/** Agent 模式 */
|
||||
mode: 'primary' | 'subagent' | 'all' | 'internal';
|
||||
/** 是否是子代理 */
|
||||
isSubagent: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提示词模板上下文 - 运行时可用的变量
|
||||
*/
|
||||
export interface PromptContext {
|
||||
/** 工具名称映射 */
|
||||
tools: ToolNameMapping;
|
||||
|
||||
/** Plan 模式相关 */
|
||||
plan: PlanModeContext;
|
||||
|
||||
/** 环境信息 */
|
||||
env: EnvContext;
|
||||
|
||||
/** Agent 信息 */
|
||||
agent: AgentContext;
|
||||
|
||||
/** 自定义扩展变量 */
|
||||
custom?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提示词模板定义
|
||||
*/
|
||||
export interface PromptTemplate {
|
||||
/** 模板名称 */
|
||||
name: string;
|
||||
/** 模板描述 */
|
||||
description?: string;
|
||||
/** 模板内容(支持变量插值) */
|
||||
template: string;
|
||||
/** 所需变量列表(用于验证) */
|
||||
requiredVariables?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 模板渲染选项
|
||||
*/
|
||||
export interface RenderOptions {
|
||||
/** 遇到未定义变量时是否抛出错误 */
|
||||
throwOnUndefined?: boolean;
|
||||
/** 未定义变量的默认值 */
|
||||
undefinedValue?: string;
|
||||
/** 是否启用调试日志 */
|
||||
debug?: boolean;
|
||||
}
|
||||
@@ -106,6 +106,12 @@ export interface AgentInfo {
|
||||
mode: AgentMode;
|
||||
/** 自定义 System Prompt */
|
||||
prompt?: string;
|
||||
/**
|
||||
* 是否将 prompt 作为模板处理
|
||||
* 如果为 true,prompt 中的 ${variable} 语法会在运行时动态渲染
|
||||
* 支持:${variable}, ${obj.prop}, ${condition ? "true" : "false"}
|
||||
*/
|
||||
promptTemplate?: boolean;
|
||||
/** 模型配置 */
|
||||
model?: AgentModelConfig;
|
||||
/** 工具配置 */
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import {
|
||||
renderTemplate,
|
||||
createDefaultContext,
|
||||
createPlanContext,
|
||||
DEFAULT_TOOL_NAMES,
|
||||
generatePlanPrompt,
|
||||
resolveAgentPrompt,
|
||||
} from '../../../src/agent/prompt-template/index.js';
|
||||
import type { PromptContext } from '../../../src/agent/prompt-template/types.js';
|
||||
|
||||
describe('Prompt Template System', () => {
|
||||
let context: PromptContext;
|
||||
|
||||
beforeEach(() => {
|
||||
context = createDefaultContext({
|
||||
plan: {
|
||||
isActive: true,
|
||||
planExists: false,
|
||||
planFilePath: '/tmp/test-plan.md',
|
||||
allowedWritePaths: ['/tmp/*'],
|
||||
},
|
||||
env: {
|
||||
workdir: '/test/project',
|
||||
isGitRepo: true,
|
||||
platform: 'darwin',
|
||||
date: '2024-01-01',
|
||||
homeDir: '/home/test',
|
||||
},
|
||||
agent: {
|
||||
name: 'test-agent',
|
||||
mode: 'primary',
|
||||
isSubagent: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
describe('renderTemplate', () => {
|
||||
it('should replace simple variables', () => {
|
||||
const template = 'Use ${tools.glob} to find files';
|
||||
const result = renderTemplate(template, context);
|
||||
expect(result).toBe('Use glob to find files');
|
||||
});
|
||||
|
||||
it('should replace nested properties', () => {
|
||||
const template = 'Plan file: ${plan.planFilePath}';
|
||||
const result = renderTemplate(template, context);
|
||||
expect(result).toBe('Plan file: /tmp/test-plan.md');
|
||||
});
|
||||
|
||||
it('should handle multiple variables in one template', () => {
|
||||
const template = 'Use ${tools.glob}, ${tools.grep}, and ${tools.read}';
|
||||
const result = renderTemplate(template, context);
|
||||
expect(result).toBe('Use glob, grep_content, and read_file');
|
||||
});
|
||||
|
||||
it('should handle ternary expressions - false condition', () => {
|
||||
const template = '${plan.planExists ? "exists" : "not exists"}';
|
||||
const result = renderTemplate(template, context);
|
||||
expect(result).toBe('not exists');
|
||||
});
|
||||
|
||||
it('should handle ternary expressions - true condition', () => {
|
||||
context.plan.planExists = true;
|
||||
const template = '${plan.planExists ? "exists" : "not exists"}';
|
||||
const result = renderTemplate(template, context);
|
||||
expect(result).toBe('exists');
|
||||
});
|
||||
|
||||
it('should handle nested variables in ternary expressions', () => {
|
||||
const template =
|
||||
'${plan.planExists ? "File at ${plan.planFilePath}" : "Create at ${plan.planFilePath}"}';
|
||||
const result = renderTemplate(template, context);
|
||||
expect(result).toBe('Create at /tmp/test-plan.md');
|
||||
});
|
||||
|
||||
it('should handle boolean values', () => {
|
||||
const template = 'Is git repo: ${env.isGitRepo}';
|
||||
const result = renderTemplate(template, context);
|
||||
expect(result).toBe('Is git repo: true');
|
||||
});
|
||||
|
||||
it('should handle undefined variables gracefully', () => {
|
||||
const template = 'Value: ${custom.undefined}';
|
||||
const result = renderTemplate(template, context);
|
||||
expect(result).toBe('Value: ');
|
||||
});
|
||||
|
||||
it('should preserve text without variables', () => {
|
||||
const template = 'No variables here';
|
||||
const result = renderTemplate(template, context);
|
||||
expect(result).toBe('No variables here');
|
||||
});
|
||||
|
||||
it('should handle complex nested ternary with tool references', () => {
|
||||
context.plan.planExists = true;
|
||||
const template =
|
||||
'${plan.planExists ? "Edit with ${tools.edit}" : "Write with ${tools.write}"}';
|
||||
const result = renderTemplate(template, context);
|
||||
expect(result).toBe('Edit with edit_file');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createDefaultContext', () => {
|
||||
it('should create context with default tool names', () => {
|
||||
const ctx = createDefaultContext();
|
||||
expect(ctx.tools.glob).toBe('glob');
|
||||
expect(ctx.tools.grep).toBe('grep_content');
|
||||
expect(ctx.tools.read).toBe('read_file');
|
||||
expect(ctx.tools.write).toBe('write_file');
|
||||
expect(ctx.tools.bash).toBe('bash');
|
||||
});
|
||||
|
||||
it('should allow overriding tool names', () => {
|
||||
const ctx = createDefaultContext({
|
||||
tools: {
|
||||
...DEFAULT_TOOL_NAMES,
|
||||
glob: 'custom_glob',
|
||||
},
|
||||
});
|
||||
expect(ctx.tools.glob).toBe('custom_glob');
|
||||
expect(ctx.tools.grep).toBe('grep_content'); // Others unchanged
|
||||
});
|
||||
|
||||
it('should set default plan values', () => {
|
||||
const ctx = createDefaultContext();
|
||||
expect(ctx.plan.isActive).toBe(false);
|
||||
expect(ctx.plan.planExists).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createPlanContext', () => {
|
||||
it('should create context with plan mode active', () => {
|
||||
const ctx = createPlanContext({});
|
||||
expect(ctx.plan.isActive).toBe(true);
|
||||
expect(ctx.agent.name).toBe('plan');
|
||||
});
|
||||
|
||||
it('should set isSubagent correctly', () => {
|
||||
const ctx = createPlanContext({ isSubagent: true });
|
||||
expect(ctx.agent.isSubagent).toBe(true);
|
||||
expect(ctx.agent.mode).toBe('subagent');
|
||||
});
|
||||
|
||||
it('should use custom workdir', () => {
|
||||
const ctx = createPlanContext({ workdir: '/custom/path' });
|
||||
expect(ctx.env.workdir).toBe('/custom/path');
|
||||
});
|
||||
|
||||
it('should use custom plan file path', () => {
|
||||
const ctx = createPlanContext({ planFilePath: '/custom/plan.md' });
|
||||
expect(ctx.plan.planFilePath).toBe('/custom/plan.md');
|
||||
});
|
||||
});
|
||||
|
||||
describe('generatePlanPrompt', () => {
|
||||
it('should generate prompt with tool names resolved', () => {
|
||||
const prompt = generatePlanPrompt({});
|
||||
expect(prompt).toContain('glob');
|
||||
expect(prompt).toContain('grep_content');
|
||||
expect(prompt).toContain('read_file');
|
||||
});
|
||||
|
||||
it('should include plan file path', () => {
|
||||
const prompt = generatePlanPrompt({ planFilePath: '/test/plan.md' });
|
||||
expect(prompt).toContain('/test/plan.md');
|
||||
});
|
||||
|
||||
it('should generate shorter prompt for subagent', () => {
|
||||
const mainPrompt = generatePlanPrompt({ isSubagent: false });
|
||||
const subPrompt = generatePlanPrompt({ isSubagent: true });
|
||||
expect(subPrompt.length).toBeLessThan(mainPrompt.length);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveAgentPrompt', () => {
|
||||
it('should resolve template variables in prompt', () => {
|
||||
const basePrompt = 'Use ${tools.glob} to search';
|
||||
const resolved = resolveAgentPrompt(basePrompt, {});
|
||||
expect(resolved).toBe('Use glob to search');
|
||||
});
|
||||
|
||||
it('should handle isPlanMode option', () => {
|
||||
const basePrompt = '${plan.isActive ? "Plan mode" : "Normal mode"}';
|
||||
const planResolved = resolveAgentPrompt(basePrompt, { isPlanMode: true });
|
||||
expect(planResolved).toBe('Plan mode');
|
||||
|
||||
const normalResolved = resolveAgentPrompt(basePrompt, { isPlanMode: false });
|
||||
expect(normalResolved).toBe('Normal mode');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle empty template', () => {
|
||||
const result = renderTemplate('', context);
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('should handle template with only variable', () => {
|
||||
const result = renderTemplate('${tools.bash}', context);
|
||||
expect(result).toBe('bash');
|
||||
});
|
||||
|
||||
it('should handle unclosed variable syntax', () => {
|
||||
const result = renderTemplate('${tools.bash', context);
|
||||
expect(result).toBe('${tools.bash');
|
||||
});
|
||||
|
||||
it('should handle escaped-like patterns (no real escaping)', () => {
|
||||
// Note: This system doesn't support escaping, so $$ or similar won't work
|
||||
const result = renderTemplate('Price: $100', context);
|
||||
expect(result).toBe('Price: $100');
|
||||
});
|
||||
|
||||
it('should handle deeply nested properties', () => {
|
||||
const result = renderTemplate('${agent.mode}', context);
|
||||
expect(result).toBe('primary');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user