-
Notifications
You must be signed in to change notification settings - Fork 2.9k
feat: add personality traits system with per-mode configuration #11979
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1 @@ | ||||||||||||||||||||
| {} | ||||||||||||||||||||
|
||||||||||||||||||||
| {} | |
| { | |
| "_meta": { | |
| "description": "Aliases configuration file. This file is intentionally versioned; add alias mappings under the 'aliases' key.", | |
| "version": 1 | |
| }, | |
| "aliases": { | |
| } | |
| } |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1 @@ | ||||||
| {"state":{"hard_state":{"term":0,"vote":0,"commit":0},"conf_state":{"voters":[4166060179281456],"learners":[],"voters_outgoing":[],"learners_next":[],"auto_leave":false}},"latest_snapshot_meta":{"term":0,"index":0},"apply_progress_queue":null,"first_voter":4166060179281456,"peer_address_by_id":{},"peer_metadata_by_id":{},"this_peer_id":4166060179281456} | ||||||
|
||||||
| {"state":{"hard_state":{"term":0,"vote":0,"commit":0},"conf_state":{"voters":[4166060179281456],"learners":[],"voters_outgoing":[],"learners_next":[],"auto_leave":false}},"latest_snapshot_meta":{"term":0,"index":0},"apply_progress_queue":null,"first_voter":4166060179281456,"peer_address_by_id":{},"peer_metadata_by_id":{},"this_peer_id":4166060179281456} | |
| {} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,205 @@ | ||
| import { PersonalityTrait, PersonalityConfig } from "@roo-code/types" | ||
|
|
||
| import { | ||
| BUILT_IN_PERSONALITY_TRAITS, | ||
| resolveActiveTraits, | ||
| getAllTraitsForConfig, | ||
| buildPersonalityPrompt, | ||
| } from "../../../../shared/personality-traits" | ||
|
|
||
| describe("buildPersonalityPrompt", () => { | ||
| it("should return empty string when no config is provided", () => { | ||
| expect(buildPersonalityPrompt(undefined)).toBe("") | ||
| }) | ||
|
|
||
| it("should return empty string when no traits are active", () => { | ||
| const config: PersonalityConfig = { | ||
| activeTraitIds: [], | ||
| customTraits: [], | ||
| } | ||
| expect(buildPersonalityPrompt(config)).toBe("") | ||
| }) | ||
|
|
||
| it("should return formatted section for a single active built-in trait", () => { | ||
| const config: PersonalityConfig = { | ||
| activeTraitIds: ["roo"], | ||
| customTraits: [], | ||
| } | ||
|
|
||
| const result = buildPersonalityPrompt(config) | ||
|
|
||
| expect(result).toContain("Personality & Communication Style:") | ||
| expect(result).toContain("non-negotiable") | ||
| expect(result).toContain("You are Roo") | ||
| expect(result).toContain("IMPORTANT: Maintaining this personality is critical") | ||
| }) | ||
|
Comment on lines
+23
to
+35
|
||
|
|
||
| it("should concatenate multiple active traits", () => { | ||
| const config: PersonalityConfig = { | ||
| activeTraitIds: ["dry-wit", "straight-shooter"], | ||
| customTraits: [], | ||
| } | ||
|
|
||
| const result = buildPersonalityPrompt(config) | ||
|
|
||
| expect(result).toContain("bone-dry, deadpan") | ||
| expect(result).toContain("extremely direct and concise") | ||
| }) | ||
|
|
||
| it("should include custom traits", () => { | ||
| const customTrait: PersonalityTrait = { | ||
| id: "pirate", | ||
| emoji: "🏴☠️", | ||
| label: "Pirate", | ||
| prompt: "You are a pirate. Use pirate language like 'Ahoy matey!' and 'Arrr!'", | ||
| isBuiltIn: false, | ||
| } | ||
|
|
||
| const config: PersonalityConfig = { | ||
| activeTraitIds: ["pirate"], | ||
| customTraits: [customTrait], | ||
| } | ||
|
|
||
| const result = buildPersonalityPrompt(config) | ||
|
|
||
| expect(result).toContain("You are a pirate") | ||
| expect(result).toContain("Ahoy matey!") | ||
| }) | ||
|
|
||
| it("should ignore unknown trait IDs gracefully", () => { | ||
| const config: PersonalityConfig = { | ||
| activeTraitIds: ["nonexistent-trait"], | ||
| customTraits: [], | ||
| } | ||
|
|
||
| const result = buildPersonalityPrompt(config) | ||
| expect(result).toBe("") | ||
| }) | ||
|
|
||
| it("should include the behavioral anchor at the end", () => { | ||
| const config: PersonalityConfig = { | ||
| activeTraitIds: ["roo"], | ||
| customTraits: [], | ||
| } | ||
|
|
||
| const result = buildPersonalityPrompt(config) | ||
|
|
||
| // The behavioral anchor should be at the end | ||
| expect(result).toContain("IMPORTANT: Maintaining this personality is critical") | ||
| expect(result).toContain("generic, neutral AI assistant tone") | ||
| // Verify it ends with the anchor | ||
| expect(result.trim().endsWith("not a default chatbot.")).toBe(true) | ||
| }) | ||
| }) | ||
|
|
||
| describe("Built-in traits", () => { | ||
| it("should have 12 built-in traits", () => { | ||
| expect(BUILT_IN_PERSONALITY_TRAITS).toHaveLength(12) | ||
| }) | ||
|
|
||
| it("should have unique IDs", () => { | ||
| const ids = BUILT_IN_PERSONALITY_TRAITS.map((t) => t.id) | ||
| expect(new Set(ids).size).toBe(ids.length) | ||
| }) | ||
|
|
||
| it("should all be marked as isBuiltIn", () => { | ||
| BUILT_IN_PERSONALITY_TRAITS.forEach((trait) => { | ||
| expect(trait.isBuiltIn).toBe(true) | ||
| }) | ||
| }) | ||
|
|
||
| it("should all use direct natural-language format (no section markers)", () => { | ||
| BUILT_IN_PERSONALITY_TRAITS.forEach((trait) => { | ||
| // No [SECTION_KEY] markers should be present | ||
| expect(trait.prompt).not.toMatch(/\[COMMUNICATION_STYLE\]/) | ||
| expect(trait.prompt).not.toMatch(/\[TASK_COMPLETION\]/) | ||
| expect(trait.prompt).not.toMatch(/\[ERROR_HANDLING\]/) | ||
| expect(trait.prompt).not.toMatch(/\[SUGGESTIONS\]/) | ||
| }) | ||
| }) | ||
|
|
||
| it("should all start with identity-first framing (You are/You have/You speak/You prioritize/You question)", () => { | ||
| BUILT_IN_PERSONALITY_TRAITS.forEach((trait) => { | ||
| const startsWithIdentity = /^You (are|have|speak|prioritize|question|see)\b/.test(trait.prompt.trim()) | ||
| expect(startsWithIdentity).toBe(true) | ||
| }) | ||
|
Comment on lines
+95
to
+125
|
||
| }) | ||
|
|
||
| it("should all contain negative constraints (Never)", () => { | ||
| BUILT_IN_PERSONALITY_TRAITS.forEach((trait) => { | ||
| expect(trait.prompt).toContain("Never") | ||
| }) | ||
| }) | ||
|
|
||
| it("should include the Roo default trait", () => { | ||
| const roo = BUILT_IN_PERSONALITY_TRAITS.find((t) => t.id === "roo") | ||
| expect(roo).toBeDefined() | ||
| expect(roo!.emoji).toBe("🦘") | ||
| expect(roo!.label).toBe("Roo") | ||
| }) | ||
| }) | ||
|
|
||
| describe("resolveActiveTraits", () => { | ||
| it("should resolve built-in trait IDs to full traits", () => { | ||
| const result = resolveActiveTraits(["roo", "dry-wit"]) | ||
| expect(result).toHaveLength(2) | ||
| expect(result[0].id).toBe("roo") | ||
| expect(result[1].id).toBe("dry-wit") | ||
| }) | ||
|
|
||
| it("should preserve order", () => { | ||
| const result = resolveActiveTraits(["dry-wit", "roo"]) | ||
| expect(result[0].id).toBe("dry-wit") | ||
| expect(result[1].id).toBe("roo") | ||
| }) | ||
|
|
||
| it("should filter out unknown IDs", () => { | ||
| const result = resolveActiveTraits(["roo", "nonexistent", "dry-wit"]) | ||
| expect(result).toHaveLength(2) | ||
| }) | ||
|
|
||
| it("should resolve custom traits", () => { | ||
| const custom: PersonalityTrait = { | ||
| id: "my-custom", | ||
| emoji: "🧪", | ||
| label: "Custom", | ||
| prompt: "You are custom.", | ||
| isBuiltIn: false, | ||
| } | ||
| const result = resolveActiveTraits(["my-custom"], [custom]) | ||
| expect(result).toHaveLength(1) | ||
| expect(result[0].label).toBe("Custom") | ||
| }) | ||
| }) | ||
|
|
||
| describe("getAllTraitsForConfig", () => { | ||
| it("should return built-in traits when no custom traits", () => { | ||
| const result = getAllTraitsForConfig([]) | ||
| expect(result.length).toBe(BUILT_IN_PERSONALITY_TRAITS.length) | ||
| }) | ||
|
|
||
| it("should append custom traits", () => { | ||
| const custom: PersonalityTrait = { | ||
| id: "new-trait", | ||
| emoji: "🆕", | ||
| label: "New", | ||
| prompt: "You are new.", | ||
| isBuiltIn: false, | ||
| } | ||
| const result = getAllTraitsForConfig([custom]) | ||
| expect(result.length).toBe(BUILT_IN_PERSONALITY_TRAITS.length + 1) | ||
| }) | ||
|
|
||
| it("should allow custom traits to override built-in ones by ID", () => { | ||
| const override: PersonalityTrait = { | ||
| id: "roo", | ||
| emoji: "🦘", | ||
| label: "Custom Roo", | ||
| prompt: "You are a custom Roo.", | ||
| isBuiltIn: false, | ||
| } | ||
| const result = getAllTraitsForConfig([override]) | ||
| const roo = result.find((t) => t.id === "roo") | ||
| expect(roo!.label).toBe("Custom Roo") | ||
| }) | ||
| }) | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -388,6 +388,7 @@ export async function addCustomInstructions( | |||||||||||||
| language?: string | ||||||||||||||
| rooIgnoreInstructions?: string | ||||||||||||||
| settings?: SystemPromptSettings | ||||||||||||||
| personalityPrompt?: string | ||||||||||||||
| } = {}, | ||||||||||||||
| ): Promise<string> { | ||||||||||||||
| const sections = [] | ||||||||||||||
|
|
@@ -491,6 +492,13 @@ export async function addCustomInstructions( | |||||||||||||
| sections.push(`Rules:\n\n${rules.join("\n\n")}`) | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| // Inject personality prompt LAST for maximum recency effect. | ||||||||||||||
| // This is the last thing the model reads before generating, | ||||||||||||||
| // which research shows produces the strongest behavioral adherence. | ||||||||||||||
| if (options.personalityPrompt && options.personalityPrompt.trim()) { | ||||||||||||||
| sections.push(options.personalityPrompt.trim()) | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
|
Comment on lines
+495
to
+501
|
||||||||||||||
| // Inject personality prompt LAST for maximum recency effect. | |
| // This is the last thing the model reads before generating, | |
| // which research shows produces the strongest behavioral adherence. | |
| if (options.personalityPrompt && options.personalityPrompt.trim()) { | |
| sections.push(options.personalityPrompt.trim()) | |
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,3 +8,4 @@ export { getCapabilitiesSection } from "./capabilities" | |
| export { getModesSection } from "./modes" | ||
| export { markdownFormattingSection } from "./markdown-formatting" | ||
| export { getSkillsSection } from "./skills" | ||
| export { getPersonalitySection, buildPersonalityPromptParts } from "./personality" | ||
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,9 @@ | ||||||
| /** | ||||||
| * Personality section for system prompt. | ||||||
| * Uses the sandwich technique: personality at the TOP and reinforced at the BOTTOM. | ||||||
| */ | ||||||
| import { buildPersonalityPrompt, buildPersonalityPromptParts } from "../../../shared/personality-traits" | ||||||
|
||||||
| import { buildPersonalityPrompt, buildPersonalityPromptParts } from "../../../shared/personality-traits" | |
| import { buildPersonalityPrompt } from "../../../shared/personality-traits" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
ndjson.portsetting looks like a developer-local VS Code extension preference rather than a project-wide requirement. Consider removing it from the repo-level.vscode/settings.json(or documenting why it must be shared) to avoid forcing local preferences on contributors.