diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index 62b7eb28a1a..10971b01c92 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -13,6 +13,7 @@ import { experimentsSchema } from "./experiment.js" import { telemetrySettingsSchema } from "./telemetry.js" import { modeConfigSchema } from "./mode.js" import { customModePromptsSchema, customSupportPromptsSchema } from "./mode.js" +import { personalitySchema } from "./personality.js" import { languagesSchema } from "./vscode.js" /** @@ -181,6 +182,12 @@ export const globalSettingsSchema = z.object({ customModes: z.array(modeConfigSchema).optional(), customModePrompts: customModePromptsSchema.optional(), customSupportPrompts: customSupportPromptsSchema.optional(), + /** + * Global agent personality preference. + * This personality will be applied across all modes unless a mode + * has its own defaultPersonality set. + */ + agentPersonality: personalitySchema.optional(), enhancementApiConfigId: z.string().optional(), includeTaskHistoryInEnhance: z.boolean().optional(), historyPreviewCollapsed: z.boolean().optional(), diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 996ee781b28..284e252c5e1 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -17,6 +17,7 @@ export * from "./marketplace.js" export * from "./mcp.js" export * from "./message.js" export * from "./mode.js" +export * from "./personality.js" export * from "./model.js" export * from "./provider-settings.js" export * from "./task.js" diff --git a/packages/types/src/mode.ts b/packages/types/src/mode.ts index c02c47c1345..a65c5fb7a78 100644 --- a/packages/types/src/mode.ts +++ b/packages/types/src/mode.ts @@ -1,5 +1,6 @@ import { z } from "zod" +import { personalitySchema, instructionsTemplateSchema } from "./personality.js" import { toolGroupsSchema } from "./tool.js" /** @@ -70,6 +71,18 @@ export const modeConfigSchema = z.object({ customInstructions: z.string().optional(), groups: groupEntryArraySchema, source: z.enum(["global", "project"]).optional(), + /** + * Default personality for this mode. + * When set, this personality will be used for the mode unless + * overridden by the user's global personality preference. + */ + defaultPersonality: personalitySchema.optional(), + /** + * Template for rendering personality-aware instructions. + * When set and personality is provided, the template's {{ personality_message }} + * placeholder will be replaced with the appropriate personality content. + */ + instructionsTemplate: instructionsTemplateSchema.optional(), }) export type ModeConfig = z.infer @@ -131,6 +144,10 @@ export type CustomSupportPrompts = z.infer /** * DEFAULT_MODES + * + * The built-in modes that ship with the extension. Each mode has a roleDefinition + * that includes explicit identity disambiguation to prevent confusion with + * similarly-named systems or products. */ export const DEFAULT_MODES: readonly ModeConfig[] = [ @@ -138,7 +155,7 @@ export const DEFAULT_MODES: readonly ModeConfig[] = [ slug: "architect", name: "🏗️ Architect", roleDefinition: - "You are Roo, an experienced technical leader who is inquisitive and an excellent planner. Your goal is to gather information and get context to create a detailed plan for accomplishing the user's task, which the user will review and approve before they switch into another mode to implement the solution.", + "You are Roo, an experienced technical leader who is inquisitive and an excellent planner. Within this context, Roo refers specifically to this AI coding assistant integrated with VS Code (not any other system or product with a similar name). Your goal is to gather information and get context to create a detailed plan for accomplishing the user's task, which the user will review and approve before they switch into another mode to implement the solution.", whenToUse: "Use this mode when you need to plan, design, or strategize before implementation. Perfect for breaking down complex problems, creating technical specifications, designing system architecture, or brainstorming solutions before coding.", description: "Plan and design before implementation", @@ -150,7 +167,7 @@ export const DEFAULT_MODES: readonly ModeConfig[] = [ slug: "code", name: "💻 Code", roleDefinition: - "You are Roo, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices.", + "You are Roo, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices. Within this context, Roo refers specifically to this AI coding assistant integrated with VS Code (not any other system or product with a similar name).", whenToUse: "Use this mode when you need to write, modify, or refactor code. Ideal for implementing features, fixing bugs, creating new files, or making code improvements across any programming language or framework.", description: "Write, modify, and refactor code", @@ -160,7 +177,7 @@ export const DEFAULT_MODES: readonly ModeConfig[] = [ slug: "ask", name: "❓ Ask", roleDefinition: - "You are Roo, a knowledgeable technical assistant focused on answering questions and providing information about software development, technology, and related topics.", + "You are Roo, a knowledgeable technical assistant focused on answering questions and providing information about software development, technology, and related topics. Within this context, Roo refers specifically to this AI coding assistant integrated with VS Code (not any other system or product with a similar name).", whenToUse: "Use this mode when you need explanations, documentation, or answers to technical questions. Best for understanding concepts, analyzing existing code, getting recommendations, or learning about technologies without making changes.", description: "Get answers and explanations", @@ -172,7 +189,7 @@ export const DEFAULT_MODES: readonly ModeConfig[] = [ slug: "debug", name: "🪲 Debug", roleDefinition: - "You are Roo, an expert software debugger specializing in systematic problem diagnosis and resolution.", + "You are Roo, an expert software debugger specializing in systematic problem diagnosis and resolution. Within this context, Roo refers specifically to this AI coding assistant integrated with VS Code (not any other system or product with a similar name).", whenToUse: "Use this mode when you're troubleshooting issues, investigating errors, or diagnosing problems. Specialized in systematic debugging, adding logging, analyzing stack traces, and identifying root causes before applying fixes.", description: "Diagnose and fix software issues", @@ -184,7 +201,7 @@ export const DEFAULT_MODES: readonly ModeConfig[] = [ slug: "orchestrator", name: "🪃 Orchestrator", roleDefinition: - "You are Roo, a strategic workflow orchestrator who coordinates complex tasks by delegating them to appropriate specialized modes. You have a comprehensive understanding of each mode's capabilities and limitations, allowing you to effectively break down complex problems into discrete tasks that can be solved by different specialists.", + "You are Roo, a strategic workflow orchestrator who coordinates complex tasks by delegating them to appropriate specialized modes. Within this context, Roo refers specifically to this AI coding assistant integrated with VS Code (not any other system or product with a similar name). You have a comprehensive understanding of each mode's capabilities and limitations, allowing you to effectively break down complex problems into discrete tasks that can be solved by different specialists.", whenToUse: "Use this mode for complex, multi-step projects that require coordination across different specialties. Ideal when you need to break down large tasks into subtasks, manage workflows, or coordinate work that spans multiple domains or expertise areas.", description: "Coordinate tasks across multiple modes", diff --git a/packages/types/src/personality.ts b/packages/types/src/personality.ts new file mode 100644 index 00000000000..7e3a904d232 --- /dev/null +++ b/packages/types/src/personality.ts @@ -0,0 +1,175 @@ +import { z } from "zod" + +/** + * Personality enum defines the available communication styles for the agent. + * Each personality represents a distinct approach to interacting with users, + * allowing for customizable agent behavior while maintaining consistent capabilities. + * + * @remarks + * Personalities affect only the communication style and tone, not the agent's + * technical abilities or available tools. A friendly agent and a pragmatic agent + * have the same capabilities, but differ in how they present information. + * + * @example + * ```typescript + * import { Personality } from "@roo-code/types" + * + * const userPreference: Personality = Personality.Friendly + * ``` + */ +export enum Personality { + /** + * Warm, supportive, and team-oriented communication style. + * Uses encouraging language, acknowledges user efforts, and creates + * a collaborative atmosphere. + */ + Friendly = "friendly", + + /** + * Direct, efficient, and results-focused communication style. + * Minimizes pleasantries in favor of clear, actionable information + * and concise responses. + */ + Pragmatic = "pragmatic", +} + +/** + * Zod schema for validating Personality enum values. + * Use this for runtime validation of personality settings. + * + * @example + * ```typescript + * const result = personalitySchema.safeParse("friendly") + * if (result.success) { + * // result.data is Personality.Friendly + * } + * ``` + */ +export const personalitySchema = z.nativeEnum(Personality) + +/** + * Type representing a mapping from each personality to its associated message content. + * Used for storing personality-specific prompts, greetings, or other text content. + * + * @remarks + * This type ensures that all defined personalities have corresponding message content, + * preventing incomplete personality configurations. + * + * @example + * ```typescript + * const greetings: PersonalityMessages = { + * [Personality.Friendly]: "Hey there! I'm excited to help you today!", + * [Personality.Pragmatic]: "Ready. What do you need?" + * } + * ``` + */ +export type PersonalityMessages = Record + +/** + * Array of all available personality values. + * Useful for iteration, validation, and UI components that need to display all options. + * + * @example + * ```typescript + * PERSONALITIES.forEach(personality => { + * console.log(`Available: ${personality}`) + * }) + * ``` + */ +export const PERSONALITIES = Object.values(Personality) as Personality[] + +/** + * Default personality used when no user preference is set. + */ +export const DEFAULT_PERSONALITY = Personality.Friendly + +/** + * Zod schema for validating PersonalityMessages. + * Ensures all personality keys are present. + */ +export const personalityMessagesSchema = z.record(personalitySchema, z.string()) + +/** + * Zod schema for validating InstructionsTemplate. + */ +export const instructionsTemplateSchema = z.object({ + /** Template string containing {{ personality_message }} placeholder */ + template: z.string(), + /** Optional map of personality identifiers to personality-specific instruction fragments */ + personality_messages: personalityMessagesSchema.optional(), +}) + +/** + * Interface representing a template for rendering personality-aware instructions. + * Templates can contain `{{ personality_message }}` placeholders that will be + * substituted with personality-specific content at runtime. + * + * @remarks + * When `personality_messages` is not provided, the system will attempt to load + * personality content from the default markdown files in the personalities directory. + * + * @example + * ```typescript + * const template: InstructionsTemplate = { + * template: "You are an assistant.\n\n{{ personality_message }}\n\nBe helpful.", + * personality_messages: { + * [Personality.Friendly]: "Be warm and encouraging!", + * [Personality.Pragmatic]: "Be direct and efficient." + * } + * } + * ``` + */ +export type InstructionsTemplate = z.infer + +/** + * Zod schema for validating PersonalityUpdateMessage role. + * Restricts to "system" or "developer" roles. + */ +export const personalityUpdateMessageRoleSchema = z.enum(["system", "developer"]) + +/** + * Zod schema for validating PersonalityUpdateMessage. + */ +export const personalityUpdateMessageSchema = z.object({ + /** + * The role for the message. Use "system" for most APIs, + * "developer" for APIs that support developer messages. + */ + role: personalityUpdateMessageRoleSchema, + /** + * The content containing the personality update instructions, + * wrapped in `` tags. + */ + content: z.string(), +}) + +/** + * Message structure for mid-session personality updates. + * + * This message type is injected into the conversation when the user + * changes their personality preference mid-session, allowing the agent + * to adopt a new communication style without regenerating the entire + * system prompt. + * + * The role should be "system" or "developer" depending on the API being used: + * - "system": For APIs that support system messages (OpenAI, most providers) + * - "developer": For APIs that use developer messages (some Anthropic contexts) + * + * @remarks + * This is a lightweight approach to personality changes that avoids + * regenerating the entire system prompt. The message is injected into + * the conversation history to guide the model's communication style. + * + * @example + * ```typescript + * const updateMessage: PersonalityUpdateMessage = { + * role: "system", + * content: ` + * The user has requested a new communication style. + * + * Be warm and encouraging... + * ` + * } + * ``` + */ +export type PersonalityUpdateMessage = z.infer diff --git a/packages/types/src/vscode-extension-host.ts b/packages/types/src/vscode-extension-host.ts index f7d034f4fa8..735416779bc 100644 --- a/packages/types/src/vscode-extension-host.ts +++ b/packages/types/src/vscode-extension-host.ts @@ -595,6 +595,7 @@ export interface WebviewMessage { | "refreshCustomTools" | "requestModes" | "switchMode" + | "changePersonality" | "debugSetting" // Worktree messages | "listWorktrees" @@ -629,6 +630,7 @@ export interface WebviewMessage { alwaysAllow?: boolean isEnabled?: boolean mode?: string + personality?: string promptMode?: string | "enhance" customPrompt?: PromptComponent dataUrls?: string[] diff --git a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/architect-mode-prompt.snap b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/architect-mode-prompt.snap index 70cccc68f02..506b2d767a5 100644 --- a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/architect-mode-prompt.snap +++ b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/architect-mode-prompt.snap @@ -1,4 +1,4 @@ -You are Roo, an experienced technical leader who is inquisitive and an excellent planner. Your goal is to gather information and get context to create a detailed plan for accomplishing the user's task, which the user will review and approve before they switch into another mode to implement the solution. +You are Roo, an experienced technical leader who is inquisitive and an excellent planner. Within this context, Roo refers specifically to this AI coding assistant integrated with VS Code (not any other system or product with a similar name). Your goal is to gather information and get context to create a detailed plan for accomplishing the user's task, which the user will review and approve before they switch into another mode to implement the solution. ==== diff --git a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/ask-mode-prompt.snap b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/ask-mode-prompt.snap index ee604b3036f..619c84f565b 100644 --- a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/ask-mode-prompt.snap +++ b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/ask-mode-prompt.snap @@ -1,4 +1,4 @@ -You are Roo, a knowledgeable technical assistant focused on answering questions and providing information about software development, technology, and related topics. +You are Roo, a knowledgeable technical assistant focused on answering questions and providing information about software development, technology, and related topics. Within this context, Roo refers specifically to this AI coding assistant integrated with VS Code (not any other system or product with a similar name). ==== diff --git a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-disabled.snap b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-disabled.snap index 70cccc68f02..506b2d767a5 100644 --- a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-disabled.snap +++ b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-disabled.snap @@ -1,4 +1,4 @@ -You are Roo, an experienced technical leader who is inquisitive and an excellent planner. Your goal is to gather information and get context to create a detailed plan for accomplishing the user's task, which the user will review and approve before they switch into another mode to implement the solution. +You are Roo, an experienced technical leader who is inquisitive and an excellent planner. Within this context, Roo refers specifically to this AI coding assistant integrated with VS Code (not any other system or product with a similar name). Your goal is to gather information and get context to create a detailed plan for accomplishing the user's task, which the user will review and approve before they switch into another mode to implement the solution. ==== diff --git a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-enabled.snap b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-enabled.snap index 51fd18172bb..4dd1729e335 100644 --- a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-enabled.snap +++ b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-enabled.snap @@ -1,4 +1,4 @@ -You are Roo, an experienced technical leader who is inquisitive and an excellent planner. Your goal is to gather information and get context to create a detailed plan for accomplishing the user's task, which the user will review and approve before they switch into another mode to implement the solution. +You are Roo, an experienced technical leader who is inquisitive and an excellent planner. Within this context, Roo refers specifically to this AI coding assistant integrated with VS Code (not any other system or product with a similar name). Your goal is to gather information and get context to create a detailed plan for accomplishing the user's task, which the user will review and approve before they switch into another mode to implement the solution. ==== diff --git a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/partial-reads-enabled.snap b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/partial-reads-enabled.snap index 70cccc68f02..506b2d767a5 100644 --- a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/partial-reads-enabled.snap +++ b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/partial-reads-enabled.snap @@ -1,4 +1,4 @@ -You are Roo, an experienced technical leader who is inquisitive and an excellent planner. Your goal is to gather information and get context to create a detailed plan for accomplishing the user's task, which the user will review and approve before they switch into another mode to implement the solution. +You are Roo, an experienced technical leader who is inquisitive and an excellent planner. Within this context, Roo refers specifically to this AI coding assistant integrated with VS Code (not any other system or product with a similar name). Your goal is to gather information and get context to create a detailed plan for accomplishing the user's task, which the user will review and approve before they switch into another mode to implement the solution. ==== diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/consistent-system-prompt.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/consistent-system-prompt.snap index 5305987e28a..28a34c289e7 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/consistent-system-prompt.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/consistent-system-prompt.snap @@ -1,4 +1,4 @@ -You are Roo, an experienced technical leader who is inquisitive and an excellent planner. Your goal is to gather information and get context to create a detailed plan for accomplishing the user's task, which the user will review and approve before they switch into another mode to implement the solution. +You are Roo, an experienced technical leader who is inquisitive and an excellent planner. Within this context, Roo refers specifically to this AI coding assistant integrated with VS Code (not any other system or product with a similar name). Your goal is to gather information and get context to create a detailed plan for accomplishing the user's task, which the user will review and approve before they switch into another mode to implement the solution. ==== diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-computer-use-support.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-computer-use-support.snap index 5305987e28a..28a34c289e7 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-computer-use-support.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-computer-use-support.snap @@ -1,4 +1,4 @@ -You are Roo, an experienced technical leader who is inquisitive and an excellent planner. Your goal is to gather information and get context to create a detailed plan for accomplishing the user's task, which the user will review and approve before they switch into another mode to implement the solution. +You are Roo, an experienced technical leader who is inquisitive and an excellent planner. Within this context, Roo refers specifically to this AI coding assistant integrated with VS Code (not any other system or product with a similar name). Your goal is to gather information and get context to create a detailed plan for accomplishing the user's task, which the user will review and approve before they switch into another mode to implement the solution. ==== diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-false.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-false.snap index 5305987e28a..28a34c289e7 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-false.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-false.snap @@ -1,4 +1,4 @@ -You are Roo, an experienced technical leader who is inquisitive and an excellent planner. Your goal is to gather information and get context to create a detailed plan for accomplishing the user's task, which the user will review and approve before they switch into another mode to implement the solution. +You are Roo, an experienced technical leader who is inquisitive and an excellent planner. Within this context, Roo refers specifically to this AI coding assistant integrated with VS Code (not any other system or product with a similar name). Your goal is to gather information and get context to create a detailed plan for accomplishing the user's task, which the user will review and approve before they switch into another mode to implement the solution. ==== diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-true.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-true.snap index 5305987e28a..28a34c289e7 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-true.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-true.snap @@ -1,4 +1,4 @@ -You are Roo, an experienced technical leader who is inquisitive and an excellent planner. Your goal is to gather information and get context to create a detailed plan for accomplishing the user's task, which the user will review and approve before they switch into another mode to implement the solution. +You are Roo, an experienced technical leader who is inquisitive and an excellent planner. Within this context, Roo refers specifically to this AI coding assistant integrated with VS Code (not any other system or product with a similar name). Your goal is to gather information and get context to create a detailed plan for accomplishing the user's task, which the user will review and approve before they switch into another mode to implement the solution. ==== diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-undefined.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-undefined.snap index 5305987e28a..28a34c289e7 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-undefined.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-undefined.snap @@ -1,4 +1,4 @@ -You are Roo, an experienced technical leader who is inquisitive and an excellent planner. Your goal is to gather information and get context to create a detailed plan for accomplishing the user's task, which the user will review and approve before they switch into another mode to implement the solution. +You are Roo, an experienced technical leader who is inquisitive and an excellent planner. Within this context, Roo refers specifically to this AI coding assistant integrated with VS Code (not any other system or product with a similar name). Your goal is to gather information and get context to create a detailed plan for accomplishing the user's task, which the user will review and approve before they switch into another mode to implement the solution. ==== diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-different-viewport-size.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-different-viewport-size.snap index 5305987e28a..28a34c289e7 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-different-viewport-size.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-different-viewport-size.snap @@ -1,4 +1,4 @@ -You are Roo, an experienced technical leader who is inquisitive and an excellent planner. Your goal is to gather information and get context to create a detailed plan for accomplishing the user's task, which the user will review and approve before they switch into another mode to implement the solution. +You are Roo, an experienced technical leader who is inquisitive and an excellent planner. Within this context, Roo refers specifically to this AI coding assistant integrated with VS Code (not any other system or product with a similar name). Your goal is to gather information and get context to create a detailed plan for accomplishing the user's task, which the user will review and approve before they switch into another mode to implement the solution. ==== diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-mcp-hub-provided.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-mcp-hub-provided.snap index baa8d519d86..e52d61f9b8e 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-mcp-hub-provided.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-mcp-hub-provided.snap @@ -1,4 +1,4 @@ -You are Roo, an experienced technical leader who is inquisitive and an excellent planner. Your goal is to gather information and get context to create a detailed plan for accomplishing the user's task, which the user will review and approve before they switch into another mode to implement the solution. +You are Roo, an experienced technical leader who is inquisitive and an excellent planner. Within this context, Roo refers specifically to this AI coding assistant integrated with VS Code (not any other system or product with a similar name). Your goal is to gather information and get context to create a detailed plan for accomplishing the user's task, which the user will review and approve before they switch into another mode to implement the solution. ==== diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-undefined-mcp-hub.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-undefined-mcp-hub.snap index 5305987e28a..28a34c289e7 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-undefined-mcp-hub.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-undefined-mcp-hub.snap @@ -1,4 +1,4 @@ -You are Roo, an experienced technical leader who is inquisitive and an excellent planner. Your goal is to gather information and get context to create a detailed plan for accomplishing the user's task, which the user will review and approve before they switch into another mode to implement the solution. +You are Roo, an experienced technical leader who is inquisitive and an excellent planner. Within this context, Roo refers specifically to this AI coding assistant integrated with VS Code (not any other system or product with a similar name). Your goal is to gather information and get context to create a detailed plan for accomplishing the user's task, which the user will review and approve before they switch into another mode to implement the solution. ==== diff --git a/src/core/prompts/identity/disambiguation.ts b/src/core/prompts/identity/disambiguation.ts new file mode 100644 index 00000000000..2af940b9433 --- /dev/null +++ b/src/core/prompts/identity/disambiguation.ts @@ -0,0 +1,52 @@ +/** + * Identity Disambiguation Module + * + * This module provides constants and utilities for explicit identity disambiguation + * in agent role definitions. The disambiguation statement prevents confusion between + * this AI coding assistant and similarly-named systems, products, or other AIs. + * + * @module identity/disambiguation + */ + +/** + * Standard disambiguation statement for Roo's identity. + * + * This statement explicitly clarifies that "Roo" refers to this specific AI coding + * assistant integrated with VS Code, not any other system or product with a similar name. + * It should be included in role definitions after the initial identity declaration. + * + * @example + * // Usage in a role definition: + * const roleDefinition = `You are Roo, a skilled engineer. ${IDENTITY_DISAMBIGUATION} Your goal is...`; + */ +export const IDENTITY_DISAMBIGUATION = + "Within this context, Roo refers specifically to this AI coding assistant integrated with VS Code (not any other system or product with a similar name)." + +/** + * Builds a complete role definition with identity disambiguation. + * + * This helper function constructs a role definition string that includes: + * 1. The base identity statement (e.g., "You are Roo, a skilled engineer") + * 2. The standard disambiguation statement + * 3. The detailed role description + * + * @param baseIdentity - The initial identity statement (e.g., "You are Roo, a highly skilled software engineer") + * @param roleDescription - The detailed description of the role's responsibilities and goals + * @returns A complete role definition string with disambiguation included + * + * @example + * const roleDefinition = buildRoleDefinitionWithDisambiguation( + * "You are Roo, an experienced technical leader", + * "Your goal is to gather information and create a detailed plan..." + * ); + * // Returns: "You are Roo, an experienced technical leader. Within this context, Roo refers specifically to this AI coding assistant integrated with VS Code (not any other system or product with a similar name). Your goal is to gather information and create a detailed plan..." + */ +export function buildRoleDefinitionWithDisambiguation(baseIdentity: string, roleDescription: string): string { + // Ensure baseIdentity ends with a period for proper sentence structure + const normalizedIdentity = baseIdentity.trimEnd().endsWith(".") ? baseIdentity.trimEnd() : `${baseIdentity.trim()}.` + + // Ensure roleDescription starts with a capital letter for proper sentence structure + const normalizedDescription = roleDescription.trim() + + return `${normalizedIdentity} ${IDENTITY_DISAMBIGUATION} ${normalizedDescription}` +} diff --git a/src/core/prompts/identity/index.ts b/src/core/prompts/identity/index.ts new file mode 100644 index 00000000000..9bb156faabc --- /dev/null +++ b/src/core/prompts/identity/index.ts @@ -0,0 +1,13 @@ +/** + * Identity Module + * + * This module provides identity-related utilities for the Roo AI coding assistant, + * including disambiguation statements and role definition builders. + * + * The identity system ensures that the agent's role definitions clearly establish + * what Roo is and distinguish it from other similarly-named systems or products. + * + * @module identity + */ + +export { IDENTITY_DISAMBIGUATION, buildRoleDefinitionWithDisambiguation } from "./disambiguation.js" diff --git a/src/core/prompts/personalities/friendly.md b/src/core/prompts/personalities/friendly.md new file mode 100644 index 00000000000..992dcd5ece3 --- /dev/null +++ b/src/core/prompts/personalities/friendly.md @@ -0,0 +1,43 @@ +# Friendly Personality + +You communicate with warmth, enthusiasm, and a collaborative spirit. Your goal is to make users feel supported and encouraged throughout their development journey. + +## Communication Style + +- Use inclusive language like "we", "let's", and "together" to foster collaboration +- Acknowledge the user's ideas and efforts with genuine appreciation +- Express enthusiasm for interesting problems and creative solutions +- Offer encouragement when tackling challenging tasks +- Celebrate successes, both big and small + +## Tone Guidelines + +- Be warm and approachable, like a supportive teammate +- Show genuine interest in understanding the user's goals +- Use friendly greetings and sign-offs when appropriate +- Express empathy when users encounter difficulties +- Maintain a positive, can-do attitude while being realistic + +## Response Characteristics + +- Begin responses by acknowledging what the user is trying to accomplish +- Explain your reasoning in an accessible, non-intimidating way +- Offer multiple options when possible, respecting user autonomy +- Use encouraging phrases like "Great question!", "That's a clever approach", or "Let's figure this out together" +- End with clear next steps and an offer to continue helping + +## Example Phrases + +- "I'd be happy to help with that!" +- "That's a great approach, and here's how we can make it even better..." +- "Don't worry, this is a common challenge—let me show you a clean solution." +- "Nice work on getting this far! Let's tackle the next part together." +- "I totally understand what you're going for here." + +## When Things Go Wrong + +- Acknowledge frustration without being dismissive +- Frame errors as learning opportunities +- Take a "we're in this together" approach to debugging +- Offer reassurance that the problem is solvable +- Provide clear, step-by-step guidance to resolve issues diff --git a/src/core/prompts/personalities/pragmatic.md b/src/core/prompts/personalities/pragmatic.md new file mode 100644 index 00000000000..0b72afa71fd --- /dev/null +++ b/src/core/prompts/personalities/pragmatic.md @@ -0,0 +1,51 @@ +# Pragmatic Personality + +You communicate with directness, efficiency, and a focus on results. Your goal is to provide clear, actionable information with minimal overhead. + +## Communication Style + +- Get straight to the point without unnecessary preamble +- Provide concise answers that directly address the question +- Use clear, technical language appropriate for the context +- Present information in a structured, scannable format +- Prioritize accuracy and completeness over pleasantries + +## Tone Guidelines + +- Be professional and matter-of-fact +- Avoid filler words and excessive qualifiers +- Use active voice and direct statements +- Remain neutral and objective in assessments +- Be honest about limitations and trade-offs + +## Response Characteristics + +- Lead with the answer or solution +- Use bullet points and numbered lists for clarity +- Include only relevant context and explanations +- Provide code examples without lengthy setup explanations +- State assumptions and prerequisites upfront + +## Example Phrases + +- "Here's the solution:" +- "The issue is X. Fix: Y." +- "Two options: A (faster, less flexible) or B (more setup, better long-term)." +- "This approach has trade-offs: [list]" +- "Done. Next steps: [list]" + +## When Things Go Wrong + +- State the error clearly and its likely cause +- Provide the most probable fix first +- List alternative causes if the first fix fails +- Skip the sympathy—focus on resolution +- Include diagnostic steps only when necessary + +## Efficiency Principles + +- One clear recommendation over multiple maybes +- Working code over theoretical explanations +- Specific commands over general guidance +- Measurable outcomes over vague descriptions +- Actionable feedback over observations diff --git a/src/core/prompts/personality/__tests__/load-personalities.spec.ts b/src/core/prompts/personality/__tests__/load-personalities.spec.ts new file mode 100644 index 00000000000..16cadfcf1f5 --- /dev/null +++ b/src/core/prompts/personality/__tests__/load-personalities.spec.ts @@ -0,0 +1,203 @@ +import * as fs from "fs" +import * as path from "path" + +import { Personality, PERSONALITIES } from "@roo-code/types" + +import { + loadPersonalityContent, + loadSinglePersonalityContent, + clearPersonalityCache, + getPersonalitiesDirectory, +} from "../load-personalities" + +// Mock the fs module +vi.mock("fs") + +describe("load-personalities", () => { + const mockFriendlyContent = "# Friendly Personality\n\nBe warm and supportive." + const mockPragmaticContent = "# Pragmatic Personality\n\nBe direct and efficient." + + beforeEach(() => { + vi.clearAllMocks() + // Clear the cache before each test + clearPersonalityCache() + }) + + describe("getPersonalitiesDirectory", () => { + it("should return the personalities directory path", () => { + const dir = getPersonalitiesDirectory() + expect(dir).toContain("personalities") + }) + }) + + describe("loadPersonalityContent", () => { + it("should load all personality files successfully", () => { + vi.mocked(fs.existsSync).mockReturnValue(true) + vi.mocked(fs.readFileSync).mockImplementation((filePath) => { + const pathStr = filePath.toString() + if (pathStr.includes("friendly.md")) { + return mockFriendlyContent + } + if (pathStr.includes("pragmatic.md")) { + return mockPragmaticContent + } + return "" + }) + + const result = loadPersonalityContent() + + expect(result).not.toBeUndefined() + expect(result![Personality.Friendly]).toBe(mockFriendlyContent) + expect(result![Personality.Pragmatic]).toBe(mockPragmaticContent) + }) + + it("should cache results on subsequent calls", () => { + vi.mocked(fs.existsSync).mockReturnValue(true) + vi.mocked(fs.readFileSync).mockImplementation((filePath) => { + const pathStr = filePath.toString() + if (pathStr.includes("friendly.md")) { + return mockFriendlyContent + } + if (pathStr.includes("pragmatic.md")) { + return mockPragmaticContent + } + return "" + }) + + // First call + loadPersonalityContent() + // Second call + loadPersonalityContent() + + // readFileSync should only be called once per file due to caching + expect(fs.readFileSync).toHaveBeenCalledTimes(PERSONALITIES.length) + }) + + it("should return undefined when a personality file is missing", () => { + vi.mocked(fs.existsSync).mockImplementation((filePath) => { + const pathStr = filePath.toString() + // Only friendly.md exists + return pathStr.includes("friendly.md") + }) + + const result = loadPersonalityContent() + + expect(result).toBeUndefined() + }) + + it("should return undefined when file content is empty", () => { + vi.mocked(fs.existsSync).mockReturnValue(true) + vi.mocked(fs.readFileSync).mockImplementation((filePath) => { + const pathStr = filePath.toString() + if (pathStr.includes("friendly.md")) { + return " " // Only whitespace + } + if (pathStr.includes("pragmatic.md")) { + return mockPragmaticContent + } + return "" + }) + + const result = loadPersonalityContent() + + expect(result).toBeUndefined() + }) + + it("should return undefined when readFileSync throws an error", () => { + vi.mocked(fs.existsSync).mockReturnValue(true) + vi.mocked(fs.readFileSync).mockImplementation(() => { + throw new Error("File read error") + }) + + const result = loadPersonalityContent() + + expect(result).toBeUndefined() + }) + + it("should trim whitespace from loaded content", () => { + vi.mocked(fs.existsSync).mockReturnValue(true) + vi.mocked(fs.readFileSync).mockImplementation((filePath) => { + const pathStr = filePath.toString() + if (pathStr.includes("friendly.md")) { + return ` ${mockFriendlyContent} \n\n` + } + if (pathStr.includes("pragmatic.md")) { + return mockPragmaticContent + } + return "" + }) + + const result = loadPersonalityContent() + + expect(result).not.toBeUndefined() + expect(result![Personality.Friendly]).toBe(mockFriendlyContent) + }) + }) + + describe("loadSinglePersonalityContent", () => { + it("should load a single personality file", () => { + vi.mocked(fs.existsSync).mockReturnValue(true) + vi.mocked(fs.readFileSync).mockReturnValue(mockFriendlyContent) + + const result = loadSinglePersonalityContent(Personality.Friendly) + + expect(result).toBe(mockFriendlyContent) + }) + + it("should return undefined when file does not exist", () => { + vi.mocked(fs.existsSync).mockReturnValue(false) + + const result = loadSinglePersonalityContent(Personality.Friendly) + + expect(result).toBeUndefined() + }) + + it("should return undefined when readFileSync throws", () => { + vi.mocked(fs.existsSync).mockReturnValue(true) + vi.mocked(fs.readFileSync).mockImplementation(() => { + throw new Error("Read error") + }) + + const result = loadSinglePersonalityContent(Personality.Friendly) + + expect(result).toBeUndefined() + }) + + it("should trim whitespace from content", () => { + vi.mocked(fs.existsSync).mockReturnValue(true) + vi.mocked(fs.readFileSync).mockReturnValue(` ${mockFriendlyContent} `) + + const result = loadSinglePersonalityContent(Personality.Friendly) + + expect(result).toBe(mockFriendlyContent) + }) + }) + + describe("clearPersonalityCache", () => { + it("should clear cached content", () => { + // First, load content + vi.mocked(fs.existsSync).mockReturnValue(true) + vi.mocked(fs.readFileSync).mockImplementation((filePath) => { + const pathStr = filePath.toString() + if (pathStr.includes("friendly.md")) { + return mockFriendlyContent + } + if (pathStr.includes("pragmatic.md")) { + return mockPragmaticContent + } + return "" + }) + + loadPersonalityContent() + vi.mocked(fs.readFileSync).mockClear() + + // Clear cache + clearPersonalityCache() + + // Load again - should read from files again + loadPersonalityContent() + + expect(fs.readFileSync).toHaveBeenCalledTimes(PERSONALITIES.length) + }) + }) +}) diff --git a/src/core/prompts/personality/__tests__/template-renderer.spec.ts b/src/core/prompts/personality/__tests__/template-renderer.spec.ts new file mode 100644 index 00000000000..63bef09a456 --- /dev/null +++ b/src/core/prompts/personality/__tests__/template-renderer.spec.ts @@ -0,0 +1,283 @@ +import { Personality, type PersonalityMessages, type InstructionsTemplate } from "@roo-code/types" + +import { + PERSONALITY_PLACEHOLDER, + hasPersonalityPlaceholder, + renderPersonalityTemplate, + getAgentInstructions, + createTemplateFromInstructions, + type AgentInfo, +} from "../template-renderer" +import * as loadPersonalitiesModule from "../load-personalities" + +describe("template-renderer", () => { + describe("PERSONALITY_PLACEHOLDER", () => { + it("should have the correct placeholder value", () => { + expect(PERSONALITY_PLACEHOLDER).toBe("{{ personality_message }}") + }) + }) + + describe("hasPersonalityPlaceholder", () => { + it("should return true when template contains the placeholder", () => { + const template = "Hello {{ personality_message }} world" + expect(hasPersonalityPlaceholder(template)).toBe(true) + }) + + it("should return false when template does not contain the placeholder", () => { + const template = "Hello world" + expect(hasPersonalityPlaceholder(template)).toBe(false) + }) + + it("should return true when placeholder is at the start", () => { + const template = "{{ personality_message }} instructions" + expect(hasPersonalityPlaceholder(template)).toBe(true) + }) + + it("should return true when placeholder is at the end", () => { + const template = "Instructions {{ personality_message }}" + expect(hasPersonalityPlaceholder(template)).toBe(true) + }) + + it("should return false for empty string", () => { + expect(hasPersonalityPlaceholder("")).toBe(false) + }) + + it("should return false for partial placeholder", () => { + expect(hasPersonalityPlaceholder("{{ personality")).toBe(false) + expect(hasPersonalityPlaceholder("personality_message }}")).toBe(false) + }) + }) + + describe("renderPersonalityTemplate", () => { + const personalityMessages: PersonalityMessages = { + [Personality.Friendly]: "Be warm and supportive!", + [Personality.Pragmatic]: "Be direct and efficient.", + } + + it("should replace placeholder with friendly personality content", () => { + const template = "You are an assistant.\n\n{{ personality_message }}\n\nBe helpful." + const result = renderPersonalityTemplate(template, Personality.Friendly, personalityMessages) + expect(result).toBe("You are an assistant.\n\nBe warm and supportive!\n\nBe helpful.") + }) + + it("should replace placeholder with pragmatic personality content", () => { + const template = "You are an assistant.\n\n{{ personality_message }}\n\nBe helpful." + const result = renderPersonalityTemplate(template, Personality.Pragmatic, personalityMessages) + expect(result).toBe("You are an assistant.\n\nBe direct and efficient.\n\nBe helpful.") + }) + + it("should handle template with placeholder at start", () => { + const template = "{{ personality_message }} and then proceed." + const result = renderPersonalityTemplate(template, Personality.Friendly, personalityMessages) + expect(result).toBe("Be warm and supportive! and then proceed.") + }) + + it("should handle template with placeholder at end", () => { + const template = "Instructions: {{ personality_message }}" + const result = renderPersonalityTemplate(template, Personality.Pragmatic, personalityMessages) + expect(result).toBe("Instructions: Be direct and efficient.") + }) + + it("should replace only the first occurrence of placeholder", () => { + const template = "{{ personality_message }} first, {{ personality_message }} second" + const result = renderPersonalityTemplate(template, Personality.Friendly, personalityMessages) + expect(result).toBe("Be warm and supportive! first, {{ personality_message }} second") + }) + }) + + describe("getAgentInstructions", () => { + const mockPersonalityMessages: PersonalityMessages = { + [Personality.Friendly]: "Friendly content from inline", + [Personality.Pragmatic]: "Pragmatic content from inline", + } + + const mockLogger = { + warn: vi.fn(), + } + + beforeEach(() => { + vi.clearAllMocks() + }) + + describe("without personality", () => { + it("should return base instructions when personality is undefined", () => { + const agentInfo: AgentInfo = { + base_instructions: "Default instructions", + instructions_template: { + template: "{{ personality_message }} template", + personality_messages: mockPersonalityMessages, + }, + } + const result = getAgentInstructions(agentInfo, undefined) + expect(result).toBe("Default instructions") + }) + }) + + describe("without template", () => { + it("should return base instructions when template is undefined", () => { + const agentInfo: AgentInfo = { + base_instructions: "Default instructions", + } + const result = getAgentInstructions(agentInfo, Personality.Friendly) + expect(result).toBe("Default instructions") + }) + }) + + describe("with valid template and personality", () => { + it("should render template with friendly personality", () => { + const agentInfo: AgentInfo = { + base_instructions: "Default instructions", + instructions_template: { + template: "Prefix {{ personality_message }} Suffix", + personality_messages: mockPersonalityMessages, + }, + } + const result = getAgentInstructions(agentInfo, Personality.Friendly) + expect(result).toBe("Prefix Friendly content from inline Suffix") + }) + + it("should render template with pragmatic personality", () => { + const agentInfo: AgentInfo = { + base_instructions: "Default instructions", + instructions_template: { + template: "Prefix {{ personality_message }} Suffix", + personality_messages: mockPersonalityMessages, + }, + } + const result = getAgentInstructions(agentInfo, Personality.Pragmatic) + expect(result).toBe("Prefix Pragmatic content from inline Suffix") + }) + }) + + describe("template without placeholder", () => { + it("should warn and return base instructions when template has no placeholder", () => { + const agentInfo: AgentInfo = { + base_instructions: "Default instructions", + instructions_template: { + template: "No placeholder here", + personality_messages: mockPersonalityMessages, + }, + } + const result = getAgentInstructions(agentInfo, Personality.Friendly, { logger: mockLogger }) + expect(result).toBe("Default instructions") + expect(mockLogger.warn).toHaveBeenCalledWith( + expect.stringContaining("Template does not contain placeholder"), + ) + }) + }) + + describe("loading from files", () => { + it("should load personality content from files when not provided inline", () => { + const loadedMessages: PersonalityMessages = { + [Personality.Friendly]: "Content from file", + [Personality.Pragmatic]: "Pragmatic from file", + } + vi.spyOn(loadPersonalitiesModule, "loadPersonalityContent").mockReturnValue(loadedMessages) + + const agentInfo: AgentInfo = { + base_instructions: "Default instructions", + instructions_template: { + template: "Prefix {{ personality_message }} Suffix", + // No personality_messages provided + }, + } + const result = getAgentInstructions(agentInfo, Personality.Friendly) + expect(result).toBe("Prefix Content from file Suffix") + }) + + it("should warn and return base instructions when file loading fails", () => { + vi.spyOn(loadPersonalitiesModule, "loadPersonalityContent").mockReturnValue(undefined) + + const agentInfo: AgentInfo = { + base_instructions: "Default instructions", + instructions_template: { + template: "Prefix {{ personality_message }} Suffix", + }, + } + const result = getAgentInstructions(agentInfo, Personality.Friendly, { logger: mockLogger }) + expect(result).toBe("Default instructions") + expect(mockLogger.warn).toHaveBeenCalledWith( + expect.stringContaining("Failed to load personality content"), + ) + }) + }) + + describe("missing personality content", () => { + it("should warn and return base instructions when personality content is missing", () => { + // Mock loadPersonalityContent to return messages missing the requested personality + const loadedMessages: PersonalityMessages = { + [Personality.Friendly]: "Content from file", + [Personality.Pragmatic]: "", // Empty content for pragmatic + } + vi.spyOn(loadPersonalitiesModule, "loadPersonalityContent").mockReturnValue(loadedMessages) + + const agentInfo: AgentInfo = { + base_instructions: "Default instructions", + instructions_template: { + template: "Prefix {{ personality_message }} Suffix", + // No personality_messages provided, will load from files + }, + } + const result = getAgentInstructions(agentInfo, Personality.Pragmatic, { logger: mockLogger }) + expect(result).toBe("Default instructions") + expect(mockLogger.warn).toHaveBeenCalledWith( + expect.stringContaining("No content found for personality"), + ) + }) + + it("should fall back to loading from files when inline messages are incomplete", () => { + // When inline messages don't have all personalities, they fail validation + // and the system falls back to loading from files + const loadedMessages: PersonalityMessages = { + [Personality.Friendly]: "Content from file", + [Personality.Pragmatic]: "Pragmatic from file", + } + vi.spyOn(loadPersonalitiesModule, "loadPersonalityContent").mockReturnValue(loadedMessages) + + const incompleteMessages = { + [Personality.Friendly]: "Only friendly", + } as unknown as PersonalityMessages + + const agentInfo: AgentInfo = { + base_instructions: "Default instructions", + instructions_template: { + template: "Prefix {{ personality_message }} Suffix", + personality_messages: incompleteMessages, + }, + } + const result = getAgentInstructions(agentInfo, Personality.Pragmatic) + // Should fall back to file-loaded content since inline messages are incomplete + expect(result).toBe("Prefix Pragmatic from file Suffix") + }) + }) + }) + + describe("createTemplateFromInstructions", () => { + it("should create template with placeholder before instructions by default", () => { + const result = createTemplateFromInstructions("Be helpful.") + expect(result.template).toBe("{{ personality_message }}\n\nBe helpful.") + expect(result.personality_messages).toBeUndefined() + }) + + it("should create template with placeholder before instructions when position is 'before'", () => { + const result = createTemplateFromInstructions("Be helpful.", "before") + expect(result.template).toBe("{{ personality_message }}\n\nBe helpful.") + }) + + it("should create template with placeholder after instructions when position is 'after'", () => { + const result = createTemplateFromInstructions("Be helpful.", "after") + expect(result.template).toBe("Be helpful.\n\n{{ personality_message }}") + }) + + it("should handle empty instructions", () => { + const result = createTemplateFromInstructions("") + expect(result.template).toBe("{{ personality_message }}\n\n") + }) + + it("should handle multiline instructions", () => { + const instructions = "Line 1\nLine 2\nLine 3" + const result = createTemplateFromInstructions(instructions, "before") + expect(result.template).toBe("{{ personality_message }}\n\nLine 1\nLine 2\nLine 3") + }) + }) +}) diff --git a/src/core/prompts/personality/__tests__/update-message.test.ts b/src/core/prompts/personality/__tests__/update-message.test.ts new file mode 100644 index 00000000000..5f5715ee372 --- /dev/null +++ b/src/core/prompts/personality/__tests__/update-message.test.ts @@ -0,0 +1,197 @@ +import { Personality, PersonalityMessages, PersonalityUpdateMessage } from "@roo-code/types" +import { + buildPersonalityUpdateMessage, + buildPersonalityTransitionMessage, + formatPersonalityName, + PERSONALITY_SPEC_TAG, +} from "../update-message" + +describe("update-message", () => { + // Sample personality messages for testing + const mockPersonalityMessages: PersonalityMessages = { + [Personality.Friendly]: "You are warm, approachable, and use encouraging language.", + [Personality.Pragmatic]: "You focus on practical solutions and efficiency.", + } + + describe("buildPersonalityUpdateMessage", () => { + it("should build a message with system role by default", () => { + const result = buildPersonalityUpdateMessage(Personality.Friendly, mockPersonalityMessages) + + expect(result.role).toBe("system") + expect(result.content).toContain(`<${PERSONALITY_SPEC_TAG}>`) + expect(result.content).toContain(``) + }) + + it("should build a message with developer role when specified", () => { + const result = buildPersonalityUpdateMessage(Personality.Friendly, mockPersonalityMessages, { + role: "developer", + }) + + expect(result.role).toBe("developer") + }) + + it("should include the personality instructions in the message", () => { + const result = buildPersonalityUpdateMessage(Personality.Friendly, mockPersonalityMessages) + + expect(result.content).toContain(mockPersonalityMessages[Personality.Friendly]) + }) + + it("should include the communication style change notice", () => { + const result = buildPersonalityUpdateMessage(Personality.Pragmatic, mockPersonalityMessages) + + expect(result.content).toContain("The user has requested a new communication style") + }) + + it("should work with Friendly personality", () => { + const result = buildPersonalityUpdateMessage(Personality.Friendly, mockPersonalityMessages) + + expect(result.role).toBe("system") + expect(result.content).toContain(`<${PERSONALITY_SPEC_TAG}>`) + expect(result.content).toContain(mockPersonalityMessages[Personality.Friendly]) + }) + + it("should work with Pragmatic personality", () => { + const result = buildPersonalityUpdateMessage(Personality.Pragmatic, mockPersonalityMessages) + + expect(result.role).toBe("system") + expect(result.content).toContain(`<${PERSONALITY_SPEC_TAG}>`) + expect(result.content).toContain(mockPersonalityMessages[Personality.Pragmatic]) + }) + + it("should handle empty personality instructions", () => { + const emptyMessages: PersonalityMessages = { + [Personality.Friendly]: "", + [Personality.Pragmatic]: "Pragmatic instructions", + } + + const result = buildPersonalityUpdateMessage(Personality.Friendly, emptyMessages) + + expect(result.role).toBe("system") + expect(result.content).toContain(`<${PERSONALITY_SPEC_TAG}>`) + }) + + it("should produce valid PersonalityUpdateMessage interface", () => { + const result: PersonalityUpdateMessage = buildPersonalityUpdateMessage( + Personality.Pragmatic, + mockPersonalityMessages, + ) + + expect(result).toHaveProperty("role") + expect(result).toHaveProperty("content") + expect(typeof result.role).toBe("string") + expect(typeof result.content).toBe("string") + }) + }) + + describe("buildPersonalityTransitionMessage", () => { + it("should build a message with system role by default", () => { + const result = buildPersonalityTransitionMessage( + Personality.Friendly, + Personality.Pragmatic, + mockPersonalityMessages, + ) + + expect(result.role).toBe("system") + expect(result.content).toContain(`<${PERSONALITY_SPEC_TAG}>`) + expect(result.content).toContain(``) + }) + + it("should build a message with developer role when specified", () => { + const result = buildPersonalityTransitionMessage( + Personality.Friendly, + Personality.Pragmatic, + mockPersonalityMessages, + { role: "developer" }, + ) + + expect(result.role).toBe("developer") + }) + + it("should include both previous and new personality names", () => { + const result = buildPersonalityTransitionMessage( + Personality.Friendly, + Personality.Pragmatic, + mockPersonalityMessages, + ) + + expect(result.content).toContain("Friendly") + expect(result.content).toContain("Pragmatic") + }) + + it("should include the new personality instructions", () => { + const result = buildPersonalityTransitionMessage( + Personality.Friendly, + Personality.Pragmatic, + mockPersonalityMessages, + ) + + expect(result.content).toContain(mockPersonalityMessages[Personality.Pragmatic]) + }) + + it("should include the transition notice", () => { + const result = buildPersonalityTransitionMessage( + Personality.Pragmatic, + Personality.Friendly, + mockPersonalityMessages, + ) + + expect(result.content).toContain("communication style from") + }) + + it("should work with Friendly to Pragmatic transition", () => { + const result = buildPersonalityTransitionMessage( + Personality.Friendly, + Personality.Pragmatic, + mockPersonalityMessages, + ) + + expect(result.role).toBe("system") + expect(result.content).toContain(`<${PERSONALITY_SPEC_TAG}>`) + expect(result.content).toContain(mockPersonalityMessages[Personality.Pragmatic]) + }) + + it("should work with Pragmatic to Friendly transition", () => { + const result = buildPersonalityTransitionMessage( + Personality.Pragmatic, + Personality.Friendly, + mockPersonalityMessages, + ) + + expect(result.role).toBe("system") + expect(result.content).toContain(`<${PERSONALITY_SPEC_TAG}>`) + expect(result.content).toContain(mockPersonalityMessages[Personality.Friendly]) + }) + + it("should produce valid PersonalityUpdateMessage interface", () => { + const result: PersonalityUpdateMessage = buildPersonalityTransitionMessage( + Personality.Friendly, + Personality.Pragmatic, + mockPersonalityMessages, + ) + + expect(result).toHaveProperty("role") + expect(result).toHaveProperty("content") + expect(typeof result.role).toBe("string") + expect(typeof result.content).toBe("string") + }) + }) + + describe("formatPersonalityName", () => { + it("should capitalize Friendly personality", () => { + const result = formatPersonalityName(Personality.Friendly) + expect(result).toBe("Friendly") + }) + + it("should capitalize Pragmatic personality", () => { + const result = formatPersonalityName(Personality.Pragmatic) + expect(result).toBe("Pragmatic") + }) + }) + + describe("PERSONALITY_SPEC_TAG", () => { + it("should be a valid string", () => { + expect(typeof PERSONALITY_SPEC_TAG).toBe("string") + expect(PERSONALITY_SPEC_TAG).toBe("personality_spec") + }) + }) +}) diff --git a/src/core/prompts/personality/index.ts b/src/core/prompts/personality/index.ts new file mode 100644 index 00000000000..823dac76cfc --- /dev/null +++ b/src/core/prompts/personality/index.ts @@ -0,0 +1,47 @@ +/** + * Personality module for rendering personality-aware instructions. + * + * This module provides utilities for: + * - Loading personality content from markdown files + * - Rendering templates with personality-specific placeholders + * - Getting the appropriate instructions based on personality + * + * @example + * ```typescript + * import { + * getAgentInstructions, + * hasPersonalityPlaceholder, + * PERSONALITY_PLACEHOLDER + * } from "./personality" + * + * // Check if a template supports personality + * if (hasPersonalityPlaceholder(template)) { + * const instructions = getAgentInstructions(agentInfo, Personality.Friendly) + * } + * ``` + */ + +export { + PERSONALITY_PLACEHOLDER, + hasPersonalityPlaceholder, + renderPersonalityTemplate, + getAgentInstructions, + createTemplateFromInstructions, + type AgentInfo, + type GetAgentInstructionsOptions, +} from "./template-renderer" + +export { + loadPersonalityContent, + loadSinglePersonalityContent, + clearPersonalityCache, + getPersonalitiesDirectory, +} from "./load-personalities" + +export { + PERSONALITY_SPEC_TAG, + buildPersonalityUpdateMessage, + buildPersonalityTransitionMessage, + formatPersonalityName, + type BuildPersonalityUpdateMessageOptions, +} from "./update-message" diff --git a/src/core/prompts/personality/load-personalities.ts b/src/core/prompts/personality/load-personalities.ts new file mode 100644 index 00000000000..de16029ab57 --- /dev/null +++ b/src/core/prompts/personality/load-personalities.ts @@ -0,0 +1,115 @@ +import * as fs from "fs" +import * as path from "path" + +import { Personality, type PersonalityMessages, PERSONALITIES } from "@roo-code/types" + +/** + * Cache for loaded personality content to avoid repeated file reads. + */ +let cachedPersonalityContent: PersonalityMessages | undefined + +/** + * The directory path where personality markdown files are stored. + * Files should be named after the personality value (e.g., "friendly.md", "pragmatic.md"). + */ +const PERSONALITIES_DIR = path.join(__dirname, "..", "personalities") + +/** + * Loads personality content from the markdown files in the personalities directory. + * + * This function reads markdown files for each personality and caches the results. + * Files are expected to be named after the personality value (lowercase). + * + * @returns PersonalityMessages object if all personalities loaded successfully, + * undefined if any personality file is missing or unreadable + * + * @example + * ```typescript + * const content = loadPersonalityContent() + * if (content) { + * console.log(content[Personality.Friendly]) // Content from friendly.md + * } + * ``` + */ +export function loadPersonalityContent(): PersonalityMessages | undefined { + // Return cached content if available + if (cachedPersonalityContent) { + return cachedPersonalityContent + } + + try { + const messages: Partial> = {} + + for (const personality of PERSONALITIES) { + const filePath = path.join(PERSONALITIES_DIR, `${personality}.md`) + + if (!fs.existsSync(filePath)) { + console.warn(`[personality] Missing personality file: ${filePath}`) + return undefined + } + + const content = fs.readFileSync(filePath, "utf-8") + messages[personality] = content.trim() + } + + // Verify all personalities have content + for (const personality of PERSONALITIES) { + if (!messages[personality]) { + console.warn(`[personality] Empty content for personality: ${personality}`) + return undefined + } + } + + // Cache and return the result + cachedPersonalityContent = messages as PersonalityMessages + return cachedPersonalityContent + } catch (error) { + console.warn(`[personality] Failed to load personality content:`, error) + return undefined + } +} + +/** + * Loads content for a specific personality from its markdown file. + * + * @param personality - The personality to load content for + * @returns The content string, or undefined if loading fails + * + * @example + * ```typescript + * const friendlyContent = loadSinglePersonalityContent(Personality.Friendly) + * ``` + */ +export function loadSinglePersonalityContent(personality: Personality): string | undefined { + try { + const filePath = path.join(PERSONALITIES_DIR, `${personality}.md`) + + if (!fs.existsSync(filePath)) { + console.warn(`[personality] Missing personality file: ${filePath}`) + return undefined + } + + return fs.readFileSync(filePath, "utf-8").trim() + } catch (error) { + console.warn(`[personality] Failed to load personality content for "${personality}":`, error) + return undefined + } +} + +/** + * Clears the cached personality content. + * Useful for testing or when personality files are updated at runtime. + */ +export function clearPersonalityCache(): void { + cachedPersonalityContent = undefined +} + +/** + * Gets the directory path where personality files are stored. + * Useful for debugging or configuration verification. + * + * @returns The absolute path to the personalities directory + */ +export function getPersonalitiesDirectory(): string { + return PERSONALITIES_DIR +} diff --git a/src/core/prompts/personality/template-renderer.ts b/src/core/prompts/personality/template-renderer.ts new file mode 100644 index 00000000000..e795c7fa797 --- /dev/null +++ b/src/core/prompts/personality/template-renderer.ts @@ -0,0 +1,219 @@ +import { type Personality, type PersonalityMessages, type InstructionsTemplate, PERSONALITIES } from "@roo-code/types" + +import { loadPersonalityContent } from "./load-personalities" + +/** + * Validates that a partial personality messages object has content for all personalities. + * + * @param messages - Partial record that may be missing some personality keys + * @returns The messages as a complete PersonalityMessages if valid, undefined otherwise + */ +function validatePersonalityMessages( + messages: Partial> | undefined, +): PersonalityMessages | undefined { + if (!messages) { + return undefined + } + + // Check that all personalities have content + for (const personality of PERSONALITIES) { + if (!messages[personality]) { + return undefined + } + } + + return messages as PersonalityMessages +} + +/** + * The placeholder pattern used in templates for personality-specific content. + * Templates should include this exact string where personality content should be injected. + */ +export const PERSONALITY_PLACEHOLDER = "{{ personality_message }}" + +/** + * Checks if a template string contains the personality placeholder. + * + * @param template - The template string to check + * @returns True if the template contains the personality placeholder + * + * @example + * ```typescript + * hasPersonalityPlaceholder("Hello {{ personality_message }}") // true + * hasPersonalityPlaceholder("Hello world") // false + * ``` + */ +export function hasPersonalityPlaceholder(template: string): boolean { + return template.includes(PERSONALITY_PLACEHOLDER) +} + +/** + * Renders a template by replacing the personality placeholder with + * the appropriate personality-specific content. + * + * @param template - The template string containing the placeholder + * @param personality - The personality to use for content lookup + * @param personalityMessages - Map of personality types to their message content + * @returns The rendered template with personality content substituted + * + * @example + * ```typescript + * const template = "You are an assistant.\n\n{{ personality_message }}\n\nBe helpful." + * const messages: PersonalityMessages = { + * friendly: "Be warm and encouraging!", + * pragmatic: "Be direct and efficient." + * } + * renderPersonalityTemplate(template, Personality.Friendly, messages) + * // "You are an assistant.\n\nBe warm and encouraging!\n\nBe helpful." + * ``` + */ +export function renderPersonalityTemplate( + template: string, + personality: Personality, + personalityMessages: PersonalityMessages, +): string { + const personalityContent = personalityMessages[personality] ?? "" + return template.replace(PERSONALITY_PLACEHOLDER, personalityContent) +} + +/** + * Agent information structure containing base instructions and optional template. + */ +export interface AgentInfo { + /** Base instructions that apply regardless of personality */ + base_instructions: string + /** Optional template for personality-aware instructions */ + instructions_template?: InstructionsTemplate +} + +/** + * Options for getAgentInstructions function. + */ +export interface GetAgentInstructionsOptions { + /** Optional logger for warnings */ + logger?: { warn: (message: string) => void } +} + +/** + * Gets the final agent instructions, applying personality templating when available. + * + * This function handles the logic for determining which instructions to return: + * 1. If no personality is specified, returns base_instructions + * 2. If personality is specified but no template exists, returns base_instructions + * 3. If template exists but has no placeholder, logs warning and returns base_instructions + * 4. If template exists with placeholder, renders and returns personality-aware instructions + * + * When `personality_messages` is not provided in the template, it will attempt + * to load content from the default personality markdown files. + * + * @param agentInfo - Object containing base_instructions and optional instructions_template + * @param personality - Optional personality to apply + * @param options - Optional configuration (e.g., custom logger) + * @returns The appropriate instructions string for the agent + * + * @example + * ```typescript + * // With personality template + * const agentInfo: AgentInfo = { + * base_instructions: "Default instructions", + * instructions_template: { + * template: "Custom {{ personality_message }} instructions", + * personality_messages: { + * friendly: "friendly and warm", + * pragmatic: "direct and efficient" + * } + * } + * } + * getAgentInstructions(agentInfo, Personality.Friendly) + * // "Custom friendly and warm instructions" + * + * // Without template - falls back to base instructions + * getAgentInstructions({ base_instructions: "Default" }, Personality.Friendly) + * // "Default" + * ``` + */ +export function getAgentInstructions( + agentInfo: AgentInfo, + personality?: Personality, + options?: GetAgentInstructionsOptions, +): string { + const { base_instructions, instructions_template } = agentInfo + const logger = options?.logger ?? console + + // If no personality requested, return base instructions + if (!personality) { + return base_instructions + } + + // If no template available, return base instructions + if (!instructions_template) { + return base_instructions + } + + const { template, personality_messages } = instructions_template + + // If template doesn't have the placeholder, warn and return base instructions + if (!hasPersonalityPlaceholder(template)) { + logger.warn( + `[personality] Template does not contain placeholder "${PERSONALITY_PLACEHOLDER}". ` + + `Falling back to base_instructions.`, + ) + return base_instructions + } + + // Get personality messages - either from template or load from files + let messages: PersonalityMessages | undefined = validatePersonalityMessages(personality_messages) + + // If no inline messages provided, load from markdown files + if (!messages) { + messages = loadPersonalityContent() + + // If still no messages, warn and return base instructions + if (!messages) { + logger.warn( + `[personality] Failed to load personality content for "${personality}". ` + + `Falling back to base_instructions.`, + ) + return base_instructions + } + } + + // Ensure the requested personality has content + if (!messages[personality]) { + logger.warn( + `[personality] No content found for personality "${personality}". ` + `Falling back to base_instructions.`, + ) + return base_instructions + } + + return renderPersonalityTemplate(template, personality, messages) +} + +/** + * Creates an InstructionsTemplate from base instructions by wrapping them + * with the personality placeholder. + * + * This is useful for modes that want to prepend or append personality content + * to their existing instructions. + * + * @param baseInstructions - The original instructions to wrap + * @param position - Where to place the personality content ("before" or "after") + * @returns An InstructionsTemplate with the placeholder positioned appropriately + * + * @example + * ```typescript + * const template = createTemplateFromInstructions("Be helpful.", "before") + * // template.template = "{{ personality_message }}\n\nBe helpful." + * ``` + */ +export function createTemplateFromInstructions( + baseInstructions: string, + position: "before" | "after" = "before", +): InstructionsTemplate { + const template = + position === "before" + ? `${PERSONALITY_PLACEHOLDER}\n\n${baseInstructions}` + : `${baseInstructions}\n\n${PERSONALITY_PLACEHOLDER}` + + return { template } +} diff --git a/src/core/prompts/personality/update-message.ts b/src/core/prompts/personality/update-message.ts new file mode 100644 index 00000000000..e45cc71062d --- /dev/null +++ b/src/core/prompts/personality/update-message.ts @@ -0,0 +1,142 @@ +/** + * Utilities for building mid-session personality update messages. + * + * This module provides functions to create messages that can be injected + * into a conversation to change the agent's communication style without + * regenerating the entire system prompt. + * + * @module + */ + +import { Personality, PersonalityMessages, PersonalityUpdateMessage } from "@roo-code/types" + +/** + * XML tag used to wrap personality update instructions. + * This tag helps the model identify and parse personality directives. + */ +export const PERSONALITY_SPEC_TAG = "personality_spec" + +/** + * Options for building a personality update message. + */ +export interface BuildPersonalityUpdateMessageOptions { + /** + * The role to use for the message. + * - "system": For APIs that support system messages (OpenAI, most providers) + * - "developer": For APIs that use developer messages (some contexts) + * @default "system" + */ + role?: "system" | "developer" +} + +/** + * Builds a personality update message that can be injected into a conversation + * to change the agent's communication style mid-session. + * + * The message is wrapped in `` tags to clearly delineate + * the personality instructions from other content. + * + * @param newPersonality - The personality to switch to + * @param personalityMessages - A mapping of personalities to their instruction content + * @param options - Optional configuration for the message + * @returns A PersonalityUpdateMessage ready to be injected into the conversation + * + * @example + * ```typescript + * const message = buildPersonalityUpdateMessage( + * Personality.Pragmatic, + * { + * [Personality.Friendly]: "Be warm and encouraging...", + * [Personality.Pragmatic]: "Be direct and efficient..." + * } + * ) + * // Result: + * // { + * // role: "system", + * // content: "\nThe user has requested a new communication style.\n\nBe direct and efficient...\n" + * // } + * ``` + */ +export function buildPersonalityUpdateMessage( + newPersonality: Personality, + personalityMessages: PersonalityMessages, + options: BuildPersonalityUpdateMessageOptions = {}, +): PersonalityUpdateMessage { + const { role = "system" } = options + + const personalityInstructions = personalityMessages[newPersonality] + + const content = `<${PERSONALITY_SPEC_TAG}> +The user has requested a new communication style. + +${personalityInstructions} +` + + return { + role, + content, + } +} + +/** + * Formats a personality name for display in messages. + * Capitalizes the first letter of the personality identifier. + * + * @param personality - The personality to format + * @returns A formatted, human-readable personality name + * + * @example + * ```typescript + * formatPersonalityName(Personality.Friendly) // "Friendly" + * formatPersonalityName(Personality.Pragmatic) // "Pragmatic" + * ``` + */ +export function formatPersonalityName(personality: Personality): string { + return personality.charAt(0).toUpperCase() + personality.slice(1) +} + +/** + * Builds a personality update message with additional context about the transition. + * This variant includes information about both the previous and new personality + * for smoother transitions. + * + * @param previousPersonality - The personality the agent was using + * @param newPersonality - The personality to switch to + * @param personalityMessages - A mapping of personalities to their instruction content + * @param options - Optional configuration for the message + * @returns A PersonalityUpdateMessage with transition context + * + * @example + * ```typescript + * const message = buildPersonalityTransitionMessage( + * Personality.Friendly, + * Personality.Pragmatic, + * personalityMessages + * ) + * ``` + */ +export function buildPersonalityTransitionMessage( + previousPersonality: Personality, + newPersonality: Personality, + personalityMessages: PersonalityMessages, + options: BuildPersonalityUpdateMessageOptions = {}, +): PersonalityUpdateMessage { + const { role = "system" } = options + + const personalityInstructions = personalityMessages[newPersonality] + const previousName = formatPersonalityName(previousPersonality) + const newName = formatPersonalityName(newPersonality) + + const content = `<${PERSONALITY_SPEC_TAG}> +The user has requested a change in communication style from "${previousName}" to "${newName}". + +Please adopt the following communication approach going forward: + +${personalityInstructions} +` + + return { + role, + content, + } +} diff --git a/src/core/prompts/system.ts b/src/core/prompts/system.ts index 01f99570fbd..9d198528223 100644 --- a/src/core/prompts/system.ts +++ b/src/core/prompts/system.ts @@ -1,7 +1,13 @@ import * as vscode from "vscode" import * as os from "os" -import { type ModeConfig, type PromptComponent, type CustomModePrompts, type TodoItem } from "@roo-code/types" +import { + type ModeConfig, + type PromptComponent, + type CustomModePrompts, + type TodoItem, + type Personality, +} from "@roo-code/types" import { Mode, modes, defaultModeSlug, getModeBySlug, getGroupName, getModeSelection } from "../../shared/modes" import { DiffStrategy } from "../../shared/tools" @@ -63,6 +69,7 @@ async function generatePrompt( todoList?: TodoItem[], modelId?: string, skillsManager?: SkillsManager, + personality?: Personality, ): Promise { if (!context) { throw new Error("Extension context is required for generating system prompt") @@ -73,7 +80,9 @@ async function generatePrompt( // Get the full mode config to ensure we have the role definition (used for groups, etc.) const modeConfig = getModeBySlug(mode, customModeConfigs) || modes.find((m) => m.slug === mode) || modes[0] - const { roleDefinition, baseInstructions } = getModeSelection(mode, promptComponent, customModeConfigs) + const { roleDefinition, baseInstructions } = getModeSelection(mode, promptComponent, customModeConfigs, { + personality, + }) // Check if MCP functionality should be included const hasMcpGroup = modeConfig.groups.some((groupEntry) => getGroupName(groupEntry) === "mcp") @@ -143,6 +152,7 @@ export const SYSTEM_PROMPT = async ( todoList?: TodoItem[], modelId?: string, skillsManager?: SkillsManager, + personality?: Personality, ): Promise => { if (!context) { throw new Error("Extension context is required for generating system prompt") @@ -170,6 +180,7 @@ export const SYSTEM_PROMPT = async ( mode, promptComponent, customModes, + { personality }, ) const customInstructions = await addCustomInstructions( @@ -216,5 +227,6 @@ ${customInstructions}` todoList, modelId, skillsManager, + personality, ) } diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 5109f96507f..64e9e2f0ffc 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -35,6 +35,8 @@ import { type ModelInfo, type ClineApiReqCancelReason, type ClineApiReqInfo, + type PersonalityUpdateMessage, + Personality, RooCodeEventName, TelemetryEventName, TaskStatus, @@ -94,6 +96,7 @@ import { sanitizeToolUseId } from "../../utils/tool-id" // prompts import { formatResponse } from "../prompts/responses" import { SYSTEM_PROMPT } from "../prompts/system" +import { buildPersonalityTransitionMessage, loadPersonalityContent } from "../prompts/personality" import { buildNativeToolsArrayWithRestrictions } from "./build-tools" // core modules @@ -343,6 +346,19 @@ export class Task extends EventEmitter implements TaskLike { // Task Bridge enableBridge: boolean + // Personality + /** + * The current personality for this task's communication style. + * This is updated when the user requests a personality change mid-session. + */ + currentPersonality?: Personality + + /** + * The previous personality before the most recent change. + * Used to provide context during personality transitions. + */ + previousPersonality?: Personality + // Message Queue Service public readonly messageQueueService: MessageQueueService private messageQueueStateChangedHandler: (() => void) | undefined @@ -1563,6 +1579,83 @@ export class Task extends EventEmitter implements TaskLike { } } + /** + * Handles a mid-session personality change by injecting a personality update + * message into the conversation without regenerating the entire system prompt. + * + * This provides a lightweight approach to personality changes that: + * - Updates the task's personality tracking state + * - Loads the personality instructions from markdown files + * - Builds and injects a personality update message into the conversation + * - Does NOT regenerate the entire system prompt + * + * @param newPersonality - The personality to switch to + * @returns The PersonalityUpdateMessage that was injected + * + * @example + * ```typescript + * // Switch to pragmatic personality mid-session + * await task.handlePersonalityChange(Personality.Pragmatic) + * ``` + */ + public async handlePersonalityChange(newPersonality: Personality): Promise { + const provider = this.providerRef.deref() + if (!provider) { + throw new Error("Provider reference lost during personality change") + } + + // Update personality tracking state + this.previousPersonality = this.currentPersonality + this.currentPersonality = newPersonality + + // Load personality messages from markdown files + const personalityMessages = loadPersonalityContent() + if (!personalityMessages) { + throw new Error("Failed to load personality messages from markdown files") + } + + // Build the personality update message + // If we have a previous personality, use the transition message for smoother switching + let updateMessage: PersonalityUpdateMessage + if (this.previousPersonality) { + updateMessage = buildPersonalityTransitionMessage( + this.previousPersonality, + newPersonality, + personalityMessages, + ) + } else { + // First time setting personality - use the simpler update message + const { buildPersonalityUpdateMessage } = await import("../prompts/personality/update-message") + updateMessage = buildPersonalityUpdateMessage(newPersonality, personalityMessages) + } + + // Inject the personality update message into the conversation + // We add it as a user message containing the system-level personality directive + // This allows the model to see and adopt the new communication style + await this.addToApiConversationHistory({ + role: "user", + content: [ + { + type: "text", + text: updateMessage.content, + }, + ], + }) + + // Add a visible message to the UI so the user knows the personality changed + await this.say( + "text", + `Personality changed to "${newPersonality}". Communication style has been updated.`, + undefined, + false, + undefined, + undefined, + { isNonInteractive: true }, + ) + + return updateMessage + } + public async condenseContext(): Promise { // CRITICAL: Flush any pending tool results before condensing // to ensure tool_use/tool_result pairs are complete in history diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 41968ffb1fd..41ad8ce8962 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -1591,6 +1591,43 @@ export const webviewMessageHandler = async ( case "mode": await provider.handleModeSwitch(message.text as Mode) break + case "changePersonality": { + // Handle personality change for the current task + const { Personality } = await import("@roo-code/types") + const currentTask = provider.getCurrentTask() + + if (!currentTask) { + vscode.window.showErrorMessage(t("common:errors.no_active_task")) + break + } + + if (!message.personality) { + vscode.window.showErrorMessage("Personality value is required") + break + } + + // Validate the personality value + const validPersonalities = Object.values(Personality) + if (!validPersonalities.includes(message.personality as any)) { + vscode.window.showErrorMessage( + `Invalid personality: ${message.personality}. Valid values are: ${validPersonalities.join(", ")}`, + ) + break + } + + try { + // Call the task's handlePersonalityChange method + await currentTask.handlePersonalityChange( + message.personality as (typeof Personality)[keyof typeof Personality], + ) + provider.log(`Personality changed to: ${message.personality}`) + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + provider.log(`Error changing personality: ${errorMessage}`) + vscode.window.showErrorMessage(`Failed to change personality: ${errorMessage}`) + } + break + } case "updatePrompt": if (message.promptMode && message.customPrompt !== undefined) { const existingPrompts = getGlobalState("customModePrompts") ?? {} diff --git a/src/shared/__tests__/modes.spec.ts b/src/shared/__tests__/modes.spec.ts index 0282bd9515c..21ef4cd4395 100644 --- a/src/shared/__tests__/modes.spec.ts +++ b/src/shared/__tests__/modes.spec.ts @@ -619,7 +619,7 @@ describe("FileRestrictionError", () => { slug: "debug", name: "🪲 Debug", roleDefinition: - "You are Roo, an expert software debugger specializing in systematic problem diagnosis and resolution.", + "You are Roo, an expert software debugger specializing in systematic problem diagnosis and resolution. Within this context, Roo refers specifically to this AI coding assistant integrated with VS Code (not any other system or product with a similar name).", groups: ["read", "edit", "browser", "command", "mcp"], }) expect(debugMode?.customInstructions).toContain( @@ -640,7 +640,7 @@ describe("FileRestrictionError", () => { slug: "debug", name: "🪲 Debug", roleDefinition: - "You are Roo, an expert software debugger specializing in systematic problem diagnosis and resolution.", + "You are Roo, an expert software debugger specializing in systematic problem diagnosis and resolution. Within this context, Roo refers specifically to this AI coding assistant integrated with VS Code (not any other system or product with a similar name).", }) }) diff --git a/src/shared/modes.ts b/src/shared/modes.ts index a94aa47ed0b..c5fe7660f51 100644 --- a/src/shared/modes.ts +++ b/src/shared/modes.ts @@ -6,10 +6,12 @@ import { type CustomModePrompts, type ToolGroup, type PromptComponent, + type Personality, DEFAULT_MODES, } from "@roo-code/types" import { addCustomInstructions } from "../core/prompts/sections/custom-instructions" +import { getAgentInstructions } from "../core/prompts/personality" import { TOOL_GROUPS, ALWAYS_AVAILABLE_TOOLS } from "./tools" @@ -102,31 +104,75 @@ export function findModeBySlug(slug: string, modes: readonly ModeConfig[] | unde return modes?.find((mode) => mode.slug === slug) } +/** + * Options for getModeSelection function. + */ +export interface GetModeSelectionOptions { + /** Optional personality to apply to instructions */ + personality?: Personality +} + /** * Get the mode selection based on the provided mode slug, prompt component, and custom modes. * If a custom mode is found, it takes precedence over the built-in modes. * If no custom mode is found, the built-in mode is used with partial merging from promptComponent. * If neither is found, the default mode is used. + * + * When a personality is provided and the mode has an instructions_template, + * the baseInstructions will be rendered with personality-specific content. + * + * @param mode - The mode slug to get selection for + * @param promptComponent - Optional prompt component overrides + * @param customModes - Optional array of custom mode configurations + * @param options - Optional settings including personality + * @returns Object containing roleDefinition, baseInstructions, and description */ -export function getModeSelection(mode: string, promptComponent?: PromptComponent, customModes?: ModeConfig[]) { +export function getModeSelection( + mode: string, + promptComponent?: PromptComponent, + customModes?: ModeConfig[], + options?: GetModeSelectionOptions, +) { const customMode = findModeBySlug(mode, customModes) const builtInMode = findModeBySlug(mode, modes) + const personality = options?.personality // If we have a custom mode, use it entirely if (customMode) { + const baseInstructions = customMode.customInstructions || "" + + // Apply personality template if available + const renderedInstructions = getAgentInstructions( + { + base_instructions: baseInstructions, + instructions_template: customMode.instructionsTemplate, + }, + personality, + ) + return { roleDefinition: customMode.roleDefinition || "", - baseInstructions: customMode.customInstructions || "", + baseInstructions: renderedInstructions, description: customMode.description || "", } } // Otherwise, use built-in mode as base and merge with promptComponent const baseMode = builtInMode || modes[0] // fallback to default mode + const baseInstructions = promptComponent?.customInstructions || baseMode.customInstructions || "" + + // Apply personality template if available + const renderedInstructions = getAgentInstructions( + { + base_instructions: baseInstructions, + instructions_template: baseMode.instructionsTemplate, + }, + personality, + ) return { roleDefinition: promptComponent?.roleDefinition || baseMode.roleDefinition || "", - baseInstructions: promptComponent?.customInstructions || baseMode.customInstructions || "", + baseInstructions: renderedInstructions, description: baseMode.description || "", } }