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:
@@ -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,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -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";
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
392
src/services/multi-agent/parallel-exploration.ts
Normal file
392
src/services/multi-agent/parallel-exploration.ts
Normal 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 });
|
||||
};
|
||||
534
src/services/plan-mode/plan-service.ts
Normal file
534
src/services/plan-mode/plan-service.ts
Normal 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);
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user