feat: implement system prompt builder with modes, tiers, and providers

- Added a system prompt builder to create dynamic prompts based on different modes (ask, code review, composer, debug, implement, plan, refactor).
- Introduced prompt tiers (balanced, fast, thorough) to tailor responses based on user needs
- Integrated multiple AI providers (Anthropic, Copilot, Google, Ollama, OpenAI) for flexible backend support.
- Updated agent and multi-agent services to utilize the new prompt system.
This commit is contained in:
2026-02-04 23:01:34 -05:00
parent db79856b08
commit b519f2e8a7
30 changed files with 5625 additions and 64 deletions

View File

@@ -85,13 +85,15 @@ const callLLM = async (
return msg;
});
// Call provider with tools
// Call provider with tools and model-specific params
const response = await providerChat(
state.options.provider,
providerMessages as Message[],
{
model: state.options.model,
tools: toolDefs,
temperature: state.options.modelParams?.temperature,
maxTokens: state.options.modelParams?.maxTokens,
},
);

View File

@@ -297,33 +297,157 @@ const executeSingleAgent = async (
/**
* Execute the actual agent task
* This is a placeholder - actual implementation would integrate with
* the chat/provider system
* Integrates with the core agent system for LLM interaction
*/
const executeAgentTask = async (
instance: AgentInstance,
config: AgentSpawnConfig,
_options: MultiAgentExecutorOptions,
options: MultiAgentExecutorOptions,
): Promise<AgentExecutionResult> => {
const startTime = Date.now();
// This is where the actual agent execution would happen
// For now, return a placeholder result
// In real implementation, this would:
// 1. Build system prompt from agent definition
// 2. Send task to LLM provider
// 3. Handle tool calls
// 4. Track file modifications
// 5. Return result
// Dynamic imports to avoid circular dependencies
const { runAgent } = await import("@services/core/agent");
const { buildSystemPromptWithInfo } = await import("@prompts/system/builder");
// Placeholder implementation
return {
success: true,
output: `Agent ${instance.definition.name} completed task: ${config.task}`,
filesModified: [],
toolCallCount: 0,
duration: Date.now() - startTime,
// Get model based on tier
const modelId = getModelForTier(instance.definition.tier);
// Build system prompt based on agent definition
const context = {
workingDir: process.cwd(),
isGitRepo: true,
platform: process.platform,
today: new Date().toISOString().split("T")[0],
modelId,
customInstructions: instance.definition.systemPrompt,
};
// Get prompt with tier/provider info and model params
const { prompt: systemPrompt, params } = buildSystemPromptWithInfo(context);
// Log tier detection for debugging (only in verbose mode)
if (options.onEvent) {
options.onEvent({
type: "agent_started",
agentId: instance.id,
timestamp: Date.now(),
});
}
// Build enhanced task prompt with agent context
const enhancedTask = buildAgentTaskPrompt(instance, config);
const filesModified: string[] = [];
let toolCallCount = 0;
try {
const result = await runAgent(enhancedTask, systemPrompt, {
provider: "copilot",
model: modelId,
autoApprove: true,
maxIterations: instance.definition.maxTurns ?? 10,
verbose: false,
modelParams: {
temperature: params.temperature,
topP: params.topP,
maxTokens: params.maxTokens,
},
onToolCall: (toolCall) => {
toolCallCount++;
const agentToolCall = {
id: toolCall.id,
toolName: toolCall.name,
args: toolCall.arguments,
timestamp: Date.now(),
};
multiAgentStore.addToolCall(instance.id, agentToolCall);
options.onToolCall?.(instance.id, agentToolCall);
},
onToolResult: (_toolCallId, toolResult) => {
// Track file modifications
if (toolResult.success && toolResult.output?.includes("File written:")) {
const match = toolResult.output.match(/File written: (.+)/);
if (match) {
filesModified.push(match[1]);
multiAgentStore.addModifiedFile(instance.id, match[1]);
}
}
},
onText: (text) => {
const message = {
role: "assistant" as const,
content: text,
timestamp: Date.now(),
};
multiAgentStore.addAgentMessage(instance.id, message);
options.onAgentMessage?.(instance.id, message);
},
});
return {
success: result.success,
output: result.finalResponse,
filesModified,
toolCallCount,
duration: Date.now() - startTime,
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
success: false,
error: errorMessage,
filesModified,
toolCallCount,
duration: Date.now() - startTime,
};
}
};
/**
* Build enhanced task prompt with agent context
*/
const buildAgentTaskPrompt = (
instance: AgentInstance,
config: AgentSpawnConfig,
): string => {
const parts: string[] = [];
// Agent identity
parts.push(`## Agent: ${instance.definition.name}`);
parts.push(`Description: ${instance.definition.description}`);
// Available tools
if (instance.definition.tools.length > 0) {
parts.push(`\nAvailable tools: ${instance.definition.tools.join(", ")}`);
}
// Task
parts.push(`\n## Task\n${config.task}`);
// Context files
if (config.contextFiles?.length) {
parts.push(`\n## Context Files\n${config.contextFiles.map(f => `- ${f}`).join("\n")}`);
}
// System prompt override as additional instructions
if (config.systemPromptOverride) {
parts.push(`\n## Additional Instructions\n${config.systemPromptOverride}`);
}
return parts.join("\n");
};
/**
* Get model ID for agent tier
*/
const getModelForTier = (tier: string): string => {
const tierModels: Record<string, string> = {
fast: "gpt-4o-mini",
balanced: "gpt-4o",
thorough: "o1",
};
return tierModels[tier] ?? "gpt-4o";
};
/**

View File

@@ -0,0 +1,392 @@
/**
* Parallel Exploration Service
*
* Launches multiple exploration agents in parallel to quickly
* understand a codebase before planning or implementing changes.
*/
import type { AgentResult } from "@interfaces/AgentResult";
import { runAgent } from "@services/core/agent";
import { buildSystemPromptWithInfo } from "@prompts/system/builder";
import type { PromptContext } from "@prompts/system/builder";
import type { ProviderName } from "@/types/providers";
/**
* Exploration task definition
*/
export interface ExplorationTask {
id: string;
description: string;
searchPatterns?: string[];
filePatterns?: string[];
keywords?: string[];
}
/**
* Exploration result from a single agent
*/
export interface ExplorationResult {
taskId: string;
success: boolean;
findings: string;
filesExamined: string[];
relevantCode: Array<{
file: string;
line: number;
snippet: string;
}>;
duration: number;
}
/**
* Aggregated exploration results
*/
export interface ExplorationSummary {
tasks: ExplorationResult[];
synthesizedFindings: string;
keyFiles: string[];
totalDuration: number;
}
/**
* Options for parallel exploration
*/
export interface ParallelExplorationOptions {
maxConcurrent?: number;
timeout?: number;
modelId: string;
provider: ProviderName;
workingDir: string;
onProgress?: (completed: number, total: number) => void;
onTaskComplete?: (result: ExplorationResult) => void;
}
/**
* Default exploration tasks for understanding a codebase
*/
export const DEFAULT_EXPLORATION_TASKS: ExplorationTask[] = [
{
id: "structure",
description: "Analyze project structure and understand the directory layout",
filePatterns: ["package.json", "tsconfig.json", "*.config.*"],
},
{
id: "entry-points",
description: "Find main entry points and understand the application flow",
filePatterns: ["**/main.*", "**/index.*", "**/app.*"],
keywords: ["export", "main", "start"],
},
{
id: "types",
description: "Analyze type definitions and interfaces",
filePatterns: ["**/types/**", "**/interfaces/**", "**/*.d.ts"],
},
];
/**
* Build exploration prompt for a task
*/
const buildExplorationPrompt = (task: ExplorationTask): string => {
const parts = [`## Exploration Task: ${task.description}`];
if (task.searchPatterns?.length) {
parts.push(`\n### Search for patterns:\n${task.searchPatterns.map(p => `- ${p}`).join("\n")}`);
}
if (task.filePatterns?.length) {
parts.push(`\n### Look in files matching:\n${task.filePatterns.map(p => `- ${p}`).join("\n")}`);
}
if (task.keywords?.length) {
parts.push(`\n### Keywords to search:\n${task.keywords.map(k => `- ${k}`).join("\n")}`);
}
parts.push(`
### Instructions:
1. Use glob to find relevant files
2. Use grep to search for patterns
3. Use read to examine key files
4. Summarize your findings concisely
### Output Format:
Provide a structured summary:
- Key files found
- Important patterns discovered
- Relevant code locations (file:line)
- Dependencies and relationships
`);
return parts.join("\n");
};
/**
* Build exploration system prompt with params
*/
const buildExplorationSystemPrompt = (
context: PromptContext,
): { prompt: string; params: { temperature?: number; topP?: number; maxTokens?: number } } => {
const { prompt: basePrompt, params } = buildSystemPromptWithInfo(context);
const fullPrompt = `${basePrompt}
## Exploration Mode
You are in EXPLORATION MODE. Your goal is to quickly understand the codebase.
### Exploration Rules:
1. USE ONLY read-only tools: glob, grep, read
2. DO NOT modify any files
3. DO NOT run bash commands that modify state
4. Focus on understanding, not changing
### Tool Usage:
- glob: Find files by pattern
- grep: Search file contents
- read: Examine file contents
### Output:
Provide clear, structured findings that help understand:
- What the code does
- How it's organized
- Key patterns and conventions
- Dependencies and relationships`;
return { prompt: fullPrompt, params };
};
/**
* Run a single exploration task
*/
const runExplorationTask = async (
task: ExplorationTask,
options: ParallelExplorationOptions,
): Promise<ExplorationResult> => {
const startTime = Date.now();
const context: PromptContext = {
workingDir: options.workingDir,
isGitRepo: true,
platform: process.platform,
today: new Date().toISOString().split("T")[0],
modelId: options.modelId,
};
const { prompt: systemPrompt, params } = buildExplorationSystemPrompt(context);
const userPrompt = buildExplorationPrompt(task);
try {
const result = await runAgent(userPrompt, systemPrompt, {
provider: options.provider,
model: options.modelId,
autoApprove: true,
maxIterations: 10,
modelParams: {
temperature: params.temperature,
topP: params.topP,
maxTokens: params.maxTokens,
},
});
// Extract findings from result
const filesExamined = extractFilesFromResult(result);
const relevantCode = extractCodeLocations(result);
return {
taskId: task.id,
success: result.success,
findings: result.finalResponse,
filesExamined,
relevantCode,
duration: Date.now() - startTime,
};
} catch (error) {
return {
taskId: task.id,
success: false,
findings: error instanceof Error ? error.message : String(error),
filesExamined: [],
relevantCode: [],
duration: Date.now() - startTime,
};
}
};
/**
* Extract file paths from agent result
*/
const extractFilesFromResult = (result: AgentResult): string[] => {
const files = new Set<string>();
for (const { call } of result.toolCalls) {
if (call.name === "read" && call.arguments.file_path) {
files.add(String(call.arguments.file_path));
}
if (call.name === "glob" && call.arguments.pattern) {
// Files are in the result, not the call
}
}
// Also try to extract from the response text
const filePattern = /(?:^|\s)([a-zA-Z0-9_\-/.]+\.[a-zA-Z]+)(?:\s|$|:)/g;
let match;
while ((match = filePattern.exec(result.finalResponse)) !== null) {
if (match[1] && !match[1].startsWith("http")) {
files.add(match[1]);
}
}
return Array.from(files);
};
/**
* Extract code locations from agent result
*/
const extractCodeLocations = (result: AgentResult): Array<{
file: string;
line: number;
snippet: string;
}> => {
const locations: Array<{ file: string; line: number; snippet: string }> = [];
// Pattern: file.ts:123 or file.ts:123-456
const locationPattern = /([a-zA-Z0-9_\-/.]+\.[a-zA-Z]+):(\d+)/g;
let match;
while ((match = locationPattern.exec(result.finalResponse)) !== null) {
locations.push({
file: match[1],
line: parseInt(match[2], 10),
snippet: "",
});
}
return locations;
};
/**
* Synthesize findings from multiple exploration results
*/
const synthesizeFindings = (results: ExplorationResult[]): string => {
const sections: string[] = [];
sections.push("# Exploration Summary\n");
// Key findings from each task
for (const result of results) {
if (result.success) {
sections.push(`## ${result.taskId}\n`);
sections.push(result.findings);
sections.push("");
}
}
// Aggregate key files
const allFiles = new Set<string>();
for (const result of results) {
result.filesExamined.forEach(f => allFiles.add(f));
}
if (allFiles.size > 0) {
sections.push("## Key Files Discovered\n");
sections.push(Array.from(allFiles).map(f => `- ${f}`).join("\n"));
}
return sections.join("\n");
};
/**
* Run parallel exploration with multiple agents
*/
export const runParallelExploration = async (
tasks: ExplorationTask[],
options: ParallelExplorationOptions,
): Promise<ExplorationSummary> => {
const startTime = Date.now();
const maxConcurrent = options.maxConcurrent ?? 3;
const results: ExplorationResult[] = [];
const pending = [...tasks];
const running: Promise<ExplorationResult>[] = [];
let completed = 0;
while (pending.length > 0 || running.length > 0) {
// Start new tasks up to maxConcurrent
while (pending.length > 0 && running.length < maxConcurrent) {
const task = pending.shift()!;
running.push(
runExplorationTask(task, options).then(result => {
completed++;
options.onProgress?.(completed, tasks.length);
options.onTaskComplete?.(result);
return result;
}),
);
}
// Wait for at least one to complete
if (running.length > 0) {
const result = await Promise.race(
running.map((p, i) => p.then(r => ({ result: r, index: i }))),
);
results.push(result.result);
running.splice(result.index, 1);
}
}
// Aggregate key files
const keyFiles = new Set<string>();
for (const result of results) {
result.filesExamined.forEach(f => keyFiles.add(f));
}
return {
tasks: results,
synthesizedFindings: synthesizeFindings(results),
keyFiles: Array.from(keyFiles),
totalDuration: Date.now() - startTime,
};
};
/**
* Create exploration tasks for a specific goal
*/
export const createExplorationTasks = (
goal: string,
keywords: string[] = [],
): ExplorationTask[] => {
return [
{
id: "goal-search",
description: `Find code related to: ${goal}`,
keywords: [goal, ...keywords],
},
{
id: "related-files",
description: `Find files that might be affected by changes to: ${goal}`,
keywords: keywords.length > 0 ? keywords : [goal],
},
{
id: "dependencies",
description: `Understand dependencies and imports related to: ${goal}`,
keywords: ["import", "require", "from", goal],
},
];
};
/**
* Quick exploration for a specific file or pattern
*/
export const quickExplore = async (
pattern: string,
options: Omit<ParallelExplorationOptions, "maxConcurrent">,
): Promise<ExplorationResult> => {
const task: ExplorationTask = {
id: "quick-explore",
description: `Find and understand: ${pattern}`,
filePatterns: [pattern],
};
return runExplorationTask(task, { ...options, maxConcurrent: 1 });
};

View File

@@ -0,0 +1,534 @@
/**
* Plan Mode Service
*
* Manages the plan approval workflow for complex tasks.
* Implements the pattern from claude-code and opencode where
* complex operations require user approval before execution.
*/
import { v4 as uuidv4 } from "uuid";
import type {
ImplementationPlan,
PlanStep,
PlanApprovalCriteria,
TaskAnalysis,
TaskComplexity,
} from "@/types/plan-mode";
import { DEFAULT_PLAN_APPROVAL_CRITERIA } from "@/types/plan-mode";
/**
* Active plans storage
*/
const activePlans = new Map<string, ImplementationPlan>();
/**
* Plan event callbacks
*/
type PlanEventCallback = (plan: ImplementationPlan) => void;
const planListeners = new Map<string, Set<PlanEventCallback>>();
/**
* Keywords indicating complexity
*/
const COMPLEXITY_KEYWORDS = {
critical: [
"security",
"authentication",
"authorization",
"password",
"secret",
"database migration",
"production",
"deploy",
],
complex: [
"refactor",
"architecture",
"restructure",
"redesign",
"multiple files",
"system",
"integration",
],
moderate: [
"feature",
"component",
"service",
"api",
"endpoint",
"module",
],
};
/**
* Analyze a task to determine if it needs plan approval
*/
export const analyzeTask = (
taskDescription: string,
criteria: PlanApprovalCriteria = DEFAULT_PLAN_APPROVAL_CRITERIA,
): TaskAnalysis => {
const lowerTask = taskDescription.toLowerCase();
const reasons: string[] = [];
// Check for critical keywords
const hasCriticalKeyword = COMPLEXITY_KEYWORDS.critical.some(k =>
lowerTask.includes(k),
);
if (hasCriticalKeyword) {
reasons.push("Task involves critical/sensitive operations");
}
// Check for complex keywords
const hasComplexKeyword = COMPLEXITY_KEYWORDS.complex.some(k =>
lowerTask.includes(k),
);
if (hasComplexKeyword) {
reasons.push("Task involves architectural changes");
}
// Check for always-require operations
const matchesAlwaysRequire = criteria.alwaysRequireFor.some(op => {
const opKeywords: Record<string, string[]> = {
delete: ["delete", "remove", "drop"],
refactor: ["refactor", "restructure", "rewrite"],
architecture: ["architecture", "system design", "redesign"],
security: ["security", "auth", "permission", "access"],
database: ["database", "migration", "schema"],
config: ["config", "environment", "settings"],
};
return opKeywords[op]?.some(k => lowerTask.includes(k));
});
if (matchesAlwaysRequire) {
reasons.push("Task matches always-require-approval criteria");
}
// Check for skip-approval patterns
const matchesSkipApproval = criteria.skipApprovalFor.some(op => {
const skipPatterns: Record<string, RegExp> = {
single_file_edit: /^(fix|update|change|modify)\s+(the\s+)?(\w+\.\w+)$/i,
add_comment: /^add\s+(a\s+)?comment/i,
fix_typo: /^fix\s+(a\s+)?typo/i,
format: /^format\s+(the\s+)?(code|file)/i,
};
return skipPatterns[op]?.test(taskDescription);
});
// Determine complexity
let complexity: TaskComplexity;
if (hasCriticalKeyword) {
complexity = "critical";
} else if (hasComplexKeyword || matchesAlwaysRequire) {
complexity = "complex";
} else if (COMPLEXITY_KEYWORDS.moderate.some(k => lowerTask.includes(k))) {
complexity = "moderate";
} else {
complexity = "simple";
}
// Determine if plan approval is required
const requiresPlanApproval =
!matchesSkipApproval &&
(complexity === "critical" || complexity === "complex" || reasons.length > 0);
// Suggest approach
const suggestedApproach = requiresPlanApproval
? "Create a detailed implementation plan for user approval before proceeding"
: "Execute directly with standard verification";
return {
complexity,
requiresPlanApproval,
reasons,
suggestedApproach,
};
};
/**
* Create a new implementation plan
*/
export const createPlan = (
title: string,
summary: string,
): ImplementationPlan => {
const plan: ImplementationPlan = {
id: uuidv4(),
title,
summary,
context: {
filesAnalyzed: [],
currentArchitecture: "",
dependencies: [],
},
steps: [],
risks: [],
testingStrategy: "",
rollbackPlan: "",
estimatedChanges: {
filesCreated: 0,
filesModified: 0,
filesDeleted: 0,
},
status: "drafting",
createdAt: Date.now(),
};
activePlans.set(plan.id, plan);
emitPlanEvent(plan.id, plan);
return plan;
};
/**
* Add a step to a plan
*/
export const addPlanStep = (
planId: string,
step: Omit<PlanStep, "id" | "status">,
): PlanStep | null => {
const plan = activePlans.get(planId);
if (!plan || plan.status !== "drafting") {
return null;
}
const newStep: PlanStep = {
...step,
id: uuidv4(),
status: "pending",
};
plan.steps.push(newStep);
emitPlanEvent(planId, plan);
return newStep;
};
/**
* Update plan context
*/
export const updatePlanContext = (
planId: string,
context: Partial<ImplementationPlan["context"]>,
): boolean => {
const plan = activePlans.get(planId);
if (!plan) {
return false;
}
plan.context = { ...plan.context, ...context };
emitPlanEvent(planId, plan);
return true;
};
/**
* Add a risk to the plan
*/
export const addPlanRisk = (
planId: string,
risk: ImplementationPlan["risks"][0],
): boolean => {
const plan = activePlans.get(planId);
if (!plan) {
return false;
}
plan.risks.push(risk);
emitPlanEvent(planId, plan);
return true;
};
/**
* Finalize plan and submit for approval
*/
export const submitPlanForApproval = (
planId: string,
testingStrategy: string,
rollbackPlan: string,
): boolean => {
const plan = activePlans.get(planId);
if (!plan || plan.status !== "drafting") {
return false;
}
plan.testingStrategy = testingStrategy;
plan.rollbackPlan = rollbackPlan;
plan.status = "pending";
// Calculate estimated changes
const filesCreated = new Set<string>();
const filesModified = new Set<string>();
const filesDeleted = new Set<string>();
for (const step of plan.steps) {
for (const file of step.filesAffected) {
if (step.title.toLowerCase().includes("create") || step.title.toLowerCase().includes("add")) {
filesCreated.add(file);
} else if (step.title.toLowerCase().includes("delete") || step.title.toLowerCase().includes("remove")) {
filesDeleted.add(file);
} else {
filesModified.add(file);
}
}
}
plan.estimatedChanges = {
filesCreated: filesCreated.size,
filesModified: filesModified.size,
filesDeleted: filesDeleted.size,
};
emitPlanEvent(planId, plan);
return true;
};
/**
* Approve a plan
*/
export const approvePlan = (
planId: string,
message?: string,
): boolean => {
const plan = activePlans.get(planId);
if (!plan || plan.status !== "pending") {
return false;
}
plan.status = "approved";
plan.approvedAt = Date.now();
plan.approvalMessage = message;
emitPlanEvent(planId, plan);
return true;
};
/**
* Reject a plan
*/
export const rejectPlan = (
planId: string,
reason: string,
): boolean => {
const plan = activePlans.get(planId);
if (!plan || plan.status !== "pending") {
return false;
}
plan.status = "rejected";
plan.rejectionReason = reason;
emitPlanEvent(planId, plan);
return true;
};
/**
* Start executing a plan
*/
export const startPlanExecution = (planId: string): boolean => {
const plan = activePlans.get(planId);
if (!plan || plan.status !== "approved") {
return false;
}
plan.status = "executing";
emitPlanEvent(planId, plan);
return true;
};
/**
* Update step status during execution
*/
export const updateStepStatus = (
planId: string,
stepId: string,
status: PlanStep["status"],
output?: string,
error?: string,
): boolean => {
const plan = activePlans.get(planId);
if (!plan) {
return false;
}
const step = plan.steps.find(s => s.id === stepId);
if (!step) {
return false;
}
step.status = status;
if (output) step.output = output;
if (error) step.error = error;
emitPlanEvent(planId, plan);
return true;
};
/**
* Complete plan execution
*/
export const completePlanExecution = (
planId: string,
success: boolean,
): boolean => {
const plan = activePlans.get(planId);
if (!plan || plan.status !== "executing") {
return false;
}
plan.status = success ? "completed" : "failed";
plan.completedAt = Date.now();
emitPlanEvent(planId, plan);
return true;
};
/**
* Get a plan by ID
*/
export const getPlan = (planId: string): ImplementationPlan | undefined => {
return activePlans.get(planId);
};
/**
* Get all active plans
*/
export const getActivePlans = (): ImplementationPlan[] => {
return Array.from(activePlans.values()).filter(
p => p.status !== "completed" && p.status !== "failed" && p.status !== "rejected",
);
};
/**
* Format a plan for display
*/
export const formatPlanForDisplay = (plan: ImplementationPlan): string => {
const lines: string[] = [];
lines.push(`# Implementation Plan: ${plan.title}`);
lines.push("");
lines.push(`## Summary`);
lines.push(plan.summary);
lines.push("");
if (plan.context.filesAnalyzed.length > 0) {
lines.push(`## Files Analyzed`);
plan.context.filesAnalyzed.forEach(f => lines.push(`- ${f}`));
lines.push("");
}
if (plan.context.currentArchitecture) {
lines.push(`## Current Architecture`);
lines.push(plan.context.currentArchitecture);
lines.push("");
}
lines.push(`## Implementation Steps`);
plan.steps.forEach((step, i) => {
const riskIcon = step.riskLevel === "high" ? "⚠️" : step.riskLevel === "medium" ? "⚡" : "✓";
lines.push(`${i + 1}. ${riskIcon} **${step.title}**`);
lines.push(` ${step.description}`);
if (step.filesAffected.length > 0) {
lines.push(` Files: ${step.filesAffected.join(", ")}`);
}
});
lines.push("");
if (plan.risks.length > 0) {
lines.push(`## Risks`);
plan.risks.forEach(risk => {
lines.push(`- **${risk.impact.toUpperCase()}**: ${risk.description}`);
lines.push(` Mitigation: ${risk.mitigation}`);
});
lines.push("");
}
lines.push(`## Testing Strategy`);
lines.push(plan.testingStrategy || "TBD");
lines.push("");
lines.push(`## Rollback Plan`);
lines.push(plan.rollbackPlan || "TBD");
lines.push("");
lines.push(`## Estimated Changes`);
lines.push(`- Files to create: ${plan.estimatedChanges.filesCreated}`);
lines.push(`- Files to modify: ${plan.estimatedChanges.filesModified}`);
lines.push(`- Files to delete: ${plan.estimatedChanges.filesDeleted}`);
lines.push("");
lines.push("---");
lines.push("**Awaiting approval to proceed with implementation.**");
lines.push("Reply with 'proceed', 'approve', or 'go ahead' to start execution.");
lines.push("Reply with 'stop', 'cancel', or provide feedback to modify the plan.");
return lines.join("\n");
};
/**
* Check if a message approves a plan
*/
export const isApprovalMessage = (message: string): boolean => {
const approvalPatterns = [
/^(proceed|go\s*ahead|approve|yes|ok|lgtm|looks\s*good|do\s*it|start|execute)$/i,
/^(sounds\s*good|that\s*works|perfect|great)$/i,
/proceed\s*with/i,
/go\s*ahead\s*(with|and)/i,
];
return approvalPatterns.some(p => p.test(message.trim()));
};
/**
* Check if a message rejects a plan
*/
export const isRejectionMessage = (message: string): boolean => {
const rejectionPatterns = [
/^(stop|cancel|no|abort|don't|wait)$/i,
/^(hold\s*on|not\s*yet|let\s*me)$/i,
/don't\s*(proceed|do|execute)/i,
];
return rejectionPatterns.some(p => p.test(message.trim()));
};
/**
* Subscribe to plan events
*/
export const subscribeToPlan = (
planId: string,
callback: PlanEventCallback,
): () => void => {
if (!planListeners.has(planId)) {
planListeners.set(planId, new Set());
}
planListeners.get(planId)!.add(callback);
return () => {
planListeners.get(planId)?.delete(callback);
};
};
/**
* Emit a plan event
*/
const emitPlanEvent = (planId: string, plan: ImplementationPlan): void => {
const listeners = planListeners.get(planId);
if (listeners) {
listeners.forEach(callback => callback(plan));
}
};
/**
* Clean up a completed/rejected plan
*/
export const cleanupPlan = (planId: string): void => {
activePlans.delete(planId);
planListeners.delete(planId);
};

View File

@@ -3,16 +3,30 @@
*
* Builds and manages system prompts based on interaction mode.
* Handles mode switching and context injection.
* Uses tier-aware mode composer for model-specific prompts.
*/
import { buildAgenticPrompt } from "@prompts/system/agent";
import { buildAskPrompt } from "@prompts/system/ask";
import { buildCodeReviewPrompt } from "@prompts/system/code-review";
import {
composePrompt,
type ModePromptContext,
type ComposedPrompt,
} from "@prompts/system/modes/composer";
import type { PromptMode } from "@prompts/system/modes/mode-types";
import type { ModelTier, ModelProvider, ModelParams } from "@prompts/system/builder";
import { buildSystemPromptWithRules } from "@services/rules-service";
import { projectConfig } from "@services/project-config";
import { getProjectContextForAskMode } from "@services/context-gathering";
import type { InteractionMode } from "@/types/tui";
/**
* Map interaction mode to prompt mode
*/
const INTERACTION_TO_PROMPT_MODE: Record<InteractionMode, PromptMode> = {
agent: "agent",
ask: "ask",
"code-review": "code-review",
};
export interface PromptContext {
workingDir: string;
isGitRepo: boolean;
@@ -24,6 +38,8 @@ export interface PromptContext {
recentCommits?: string[];
projectContext?: string;
prContext?: string;
debugContext?: string;
planContext?: string;
}
export interface PromptBuilderState {
@@ -31,46 +47,45 @@ export interface PromptBuilderState {
basePrompt: string;
fullPrompt: string;
context: PromptContext;
tier: ModelTier;
provider: ModelProvider;
params: ModelParams;
}
const MODE_PROMPT_BUILDERS: Record<
InteractionMode,
(context: PromptContext) => string
> = {
agent: (ctx) =>
buildAgenticPrompt({
workingDir: ctx.workingDir,
isGitRepo: ctx.isGitRepo,
platform: ctx.platform,
today: ctx.today,
model: ctx.model,
gitBranch: ctx.gitBranch,
gitStatus: ctx.gitStatus,
recentCommits: ctx.recentCommits,
}),
/**
* Build mode prompt using tier-aware composer
*/
const buildModePromptWithTier = (
mode: InteractionMode,
context: PromptContext,
): ComposedPrompt => {
const promptMode = INTERACTION_TO_PROMPT_MODE[mode];
ask: (ctx) => {
const projectContext =
ctx.projectContext ?? getProjectContextForAskMode(ctx.workingDir);
return buildAskPrompt({
workingDir: ctx.workingDir,
isGitRepo: ctx.isGitRepo,
platform: ctx.platform,
today: ctx.today,
model: ctx.model,
projectContext,
});
},
// Build context for ask mode
const projectContext =
mode === "ask"
? context.projectContext ?? getProjectContextForAskMode(context.workingDir)
: undefined;
"code-review": (ctx) =>
buildCodeReviewPrompt({
workingDir: ctx.workingDir,
isGitRepo: ctx.isGitRepo,
platform: ctx.platform,
today: ctx.today,
model: ctx.model,
prContext: ctx.prContext,
}),
// Create mode-aware prompt context
const modeContext: ModePromptContext = {
workingDir: context.workingDir,
isGitRepo: context.isGitRepo,
platform: context.platform,
today: context.today,
modelId: context.model || "gpt-4o", // Default to balanced model
gitBranch: context.gitBranch,
gitStatus: context.gitStatus,
recentCommits: context.recentCommits,
projectRules: undefined, // Will be added by rules-service
customInstructions: projectContext,
mode: promptMode,
prContext: context.prContext,
debugContext: context.debugContext,
planContext: context.planContext,
};
return composePrompt(modeContext);
};
/**
@@ -155,8 +170,18 @@ export const buildModePrompt = (
mode: InteractionMode,
context: PromptContext,
): string => {
const builder = MODE_PROMPT_BUILDERS[mode];
return builder(context);
const composed = buildModePromptWithTier(mode, context);
return composed.prompt;
};
/**
* Build the base prompt with full tier/provider info
*/
export const buildModePromptWithInfo = (
mode: InteractionMode,
context: PromptContext,
): ComposedPrompt => {
return buildModePromptWithTier(mode, context);
};
/**
@@ -198,12 +223,16 @@ export const createPromptBuilder = (initialModel?: string) => {
): Promise<string> => {
const context = await buildBaseContext(initialModel);
const { prompt } = await buildCompletePrompt(mode, context, appendPrompt);
const composed = buildModePromptWithTier(mode, context);
state = {
currentMode: mode,
basePrompt: buildModePrompt(mode, context),
basePrompt: composed.prompt,
fullPrompt: prompt,
context,
tier: composed.tier,
provider: composed.provider,
params: composed.params,
};
return prompt;
@@ -226,12 +255,16 @@ export const createPromptBuilder = (initialModel?: string) => {
state.context,
appendPrompt,
);
const composed = buildModePromptWithTier(newMode, state.context);
state = {
currentMode: newMode,
basePrompt: buildModePrompt(newMode, state.context),
basePrompt: composed.prompt,
fullPrompt: prompt,
context: state.context,
tier: composed.tier,
provider: composed.provider,
params: composed.params,
};
return prompt;
@@ -242,6 +275,15 @@ export const createPromptBuilder = (initialModel?: string) => {
const getCurrentMode = (): InteractionMode | null =>
state?.currentMode ?? null;
const getModelInfo = (): { tier: ModelTier; provider: ModelProvider; params: ModelParams } | null => {
if (!state) return null;
return {
tier: state.tier,
provider: state.provider,
params: state.params,
};
};
const updateContext = async (
updates: Partial<PromptContext>,
appendPrompt?: string,
@@ -256,11 +298,15 @@ export const createPromptBuilder = (initialModel?: string) => {
newContext,
appendPrompt,
);
const composed = buildModePromptWithTier(state.currentMode, newContext);
state = {
...state,
context: newContext,
fullPrompt: prompt,
tier: composed.tier,
provider: composed.provider,
params: composed.params,
};
return prompt;
@@ -271,6 +317,7 @@ export const createPromptBuilder = (initialModel?: string) => {
switchMode,
getCurrentPrompt,
getCurrentMode,
getModelInfo,
updateContext,
};
};