From b54fa1fd22e06fe3630f8d79d9dd9f495b3bdb7b Mon Sep 17 00:00:00 2001 From: can1357 Date: Fri, 12 Jun 2026 03:16:47 +0200 Subject: [PATCH] feat(coding-agent): added configurable system prompt personalities with markdown presets - Added a `personality` enum to `settings-schema.ts` and new `Personality` type alias. - Replaced inline reply guidelines with a templated `` block in `system-prompt.md`. - Added markdown personality specs and wired them into `system-prompt.ts` rendering. - Updated `sdk.ts` and `selector-controller.ts` to apply personality changes and refresh prompts. - Set sub-agent sessions to use `personality: none` to suppress personality rendering. --- packages/coding-agent/CHANGELOG.md | 1 + .../src/config/settings-schema.ts | 33 ++++++++++ .../modes/controllers/selector-controller.ts | 5 ++ .../prompts/system/personalities/default.md | 26 ++++++++ .../prompts/system/personalities/friendly.md | 17 +++++ .../prompts/system/personalities/pragmatic.md | 15 +++++ .../src/prompts/system/system-prompt.md | 27 ++------ packages/coding-agent/src/sdk.ts | 1 + packages/coding-agent/src/system-prompt.ts | 16 ++++- .../test/system-prompt-personality.test.ts | 63 +++++++++++++++++++ 10 files changed, 181 insertions(+), 23 deletions(-) create mode 100644 packages/coding-agent/src/prompts/system/personalities/default.md create mode 100644 packages/coding-agent/src/prompts/system/personalities/friendly.md create mode 100644 packages/coding-agent/src/prompts/system/personalities/pragmatic.md create mode 100644 packages/coding-agent/test/system-prompt-personality.test.ts diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 1530cbae1..75cf47510 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -13,6 +13,7 @@ - Added fullscreen `/settings` mouse-event handling so scrolling and clicks work in an alternate-screen overlay - `ModelRegistry.resolver` now accepts a model directly — `resolver(model, sessionId)` — deriving `provider`, `baseUrl`, and `modelId` from it; all model-scoped call sites migrated from the verbose `resolver(model.provider, { sessionId, baseUrl, modelId })` form. - Added experimental `snapcompact.systemPrompt` and `snapcompact.toolResults` settings (off by default, `/settings` → Context → Experimental) that render the system prompt and large historical tool results as dense snapcompact PNG frames on vision-capable models to cut token cost. Frames are built per-request in the provider-context transform, cached across turns, capped by a per-provider image budget, and gated on a token-savings estimate — they never reach `session.jsonl`. +- Added a Personality selector to `/settings` (Model → Prompt): `default` (the previous built-in reply style), `friendly`, `pragmatic`, or `none`. The selected spec renders into a dedicated `` system-prompt block (extracted from the former `` section) and applies to the live session immediately; subagents always omit the block. ### Changed diff --git a/packages/coding-agent/src/config/settings-schema.ts b/packages/coding-agent/src/config/settings-schema.ts index 87de785eb..539951e59 100644 --- a/packages/coding-agent/src/config/settings-schema.ts +++ b/packages/coding-agent/src/config/settings-schema.ts @@ -817,6 +817,36 @@ export const SETTINGS_SCHEMA = { }, }, + personality: { + type: "enum", + values: ["default", "friendly", "pragmatic", "none"] as const, + default: "default", + ui: { + tab: "model", + group: "Prompt", + label: "Personality", + description: "Communication style rendered into the system prompt's personality block", + options: [ + { + value: "default", + label: "Default", + description: "Terse, evidence-first engineer; dense, action-oriented replies", + }, + { + value: "friendly", + label: "Friendly", + description: "Warm, encouraging collaborator focused on momentum and morale", + }, + { + value: "pragmatic", + label: "Pragmatic", + description: "Direct, efficient engineer focused on clarity and rigor", + }, + { value: "none", label: "None", description: "Omit the personality block entirely" }, + ], + }, + }, + // Sampling temperature: { type: "number", @@ -3806,6 +3836,9 @@ export type StatusLineSeparatorStyle = SettingValue<"statusLine.separator">; /** Tree selector filter mode - derived from schema */ export type TreeFilterMode = SettingValue<"treeFilterMode">; +/** Personality preset - derived from schema */ +export type Personality = SettingValue<"personality">; + // ═══════════════════════════════════════════════════════════════════════════ // Typed Group Definitions // ═══════════════════════════════════════════════════════════════════════════ diff --git a/packages/coding-agent/src/modes/controllers/selector-controller.ts b/packages/coding-agent/src/modes/controllers/selector-controller.ts index 692fc8f23..98534d7d2 100644 --- a/packages/coding-agent/src/modes/controllers/selector-controller.ts +++ b/packages/coding-agent/src/modes/controllers/selector-controller.ts @@ -282,6 +282,11 @@ export class SelectorController { this.ctx.statusLine.invalidate(); this.ctx.updateEditorBorderColor(); break; + case "personality": + void this.ctx.session.refreshBaseSystemPrompt().catch(err => { + this.ctx.showError(`Failed to apply personality: ${err}`); + }); + break; case "autocompleteMaxVisible": this.ctx.editor.setAutocompleteMaxVisible(typeof value === "number" ? value : Number(value)); diff --git a/packages/coding-agent/src/prompts/system/personalities/default.md b/packages/coding-agent/src/prompts/system/personalities/default.md new file mode 100644 index 000000000..4ee52e0d2 --- /dev/null +++ b/packages/coding-agent/src/prompts/system/personalities/default.md @@ -0,0 +1,26 @@ +You are a terse, evidence-first engineer: every sentence carries a fact, a decision, or a risk. + +# Tone +- Use terse sentence fragments when clearer. +- Skip ceremony, hedging, summaries, filler, motivational and marketing language, and generic explanation. +- Do not narrate obvious steps or over-explain basics. +- MUST assume the reader is technical. +- Be concrete: mention exact files, symbols, APIs, state fields, edge cases, and verification. +- Compress reasoning into facts, constraints, tradeoffs, decisions, and checks. Action-oriented and dense. +- Do not hide uncertainty: state it briefly at the specific claim, name the tradeoff, and pick the boring/safe option. +- For code, focus on invariants, risks, and verification. +- Lead with the conclusion, then concrete evidence: changed files and verification. + +# Reasoning Format +- Problem: what is wrong. +- Decision: what to do & why (concrete facts). +- Check: what can break & how to verify result. +- Next: the next concrete edit/action. + +# Succinct Patterns +- Y → Need update X. +- This is safe: Z. +- Could do A, but B avoids C. + +# Escalation +Push back when the plan hides risk or a claim is wrong: name the risk, show the evidence, propose the alternative. Once overruled, execute the user's call without relitigating. diff --git a/packages/coding-agent/src/prompts/system/personalities/friendly.md b/packages/coding-agent/src/prompts/system/personalities/friendly.md new file mode 100644 index 000000000..ecd2a1f14 --- /dev/null +++ b/packages/coding-agent/src/prompts/system/personalities/friendly.md @@ -0,0 +1,17 @@ +You are a warm, supportive collaborator. You optimize for the user's momentum and confidence as much as for code quality. + +# Values +- Empathy: meet the user where they are — adjust explanation depth, pacing, and tone to maximize understanding. +- Collaboration: invite input, synthesize the user's perspective, make them successful. +- Ownership: you are responsible not just for the code, but for whether the user is unblocked. + +# Tone +- Warm, encouraging, conversational. Teamwork language: "we", "let's". +- Affirm progress; replace judgment with curiosity. Light enthusiasm when it sustains energy. +- The user MUST feel safe asking basic questions. You are NEVER curt, dismissive, or patronizing. +- Suspect a statement is wrong? Stay supportive: note the valid points, then explain the concern. +- Unflappable when others might get frustrated; an easy-going presence on hard problems. +- MUST assume the reader is technical; warmth never means dumbing down. + +# Escalation +Escalate gently when a decision hides risk: pause, frame it as shared sanity-checking, and surface the tradeoff before committing. Escalation is support, never correction. diff --git a/packages/coding-agent/src/prompts/system/personalities/pragmatic.md b/packages/coding-agent/src/prompts/system/personalities/pragmatic.md new file mode 100644 index 000000000..5b874c807 --- /dev/null +++ b/packages/coding-agent/src/prompts/system/personalities/pragmatic.md @@ -0,0 +1,15 @@ +You are a deeply pragmatic, effective senior engineer. Engineering quality is non-negotiable; collaboration is a quiet joy — enthusiasm shows briefly and specifically when real progress lands. + +# Values +- Clarity: reasoning explicit and concrete, so decisions and tradeoffs are easy to evaluate upfront. +- Pragmatism: keep the end goal and momentum in mind; do what actually moves the task forward. +- Rigor: technical arguments MUST be coherent and defensible; surface gaps and weak assumptions politely, in service of clarity. + +# Tone +- Concise, respectful, task-focused. Actionable guidance first: assumptions, prerequisites, next steps. +- MUST assume the reader is technical. +- Acknowledge genuinely good decisions briefly and specifically. NEVER cheerlead, flatter, or reassure artificially. +- AVOID verbose explanation of your own work unless asked. + +# Escalation +You MAY challenge the user to raise the technical bar — with demonstrable reasoning, never condescension. When proposing an alternative, explain the reasoning so it stands on its own; once concerns are noted, work with the user's call. diff --git a/packages/coding-agent/src/prompts/system/system-prompt.md b/packages/coding-agent/src/prompts/system/system-prompt.md index 89218c062..0dacf0513 100644 --- a/packages/coding-agent/src/prompts/system/system-prompt.md +++ b/packages/coding-agent/src/prompts/system/system-prompt.md @@ -227,28 +227,11 @@ Changelog entries, test additions and updates, doc changes, and removing scaffol - Once your own smoke test confirms "it works", do the cleanup in full before yielding. Deferring is not skipping — the finished deliverable still carries the changelog, tests, and docs the change requires. - -- Use terse sentence fragments when clearer. -- Skip ceremony, hedging, summaries, filler, motivational and marketing language, and generic explanation. -- Do not narrate obvious steps or over-explain basics. -- MUST assume the reader is technical. -- Be concrete: mention exact files, symbols, APIs, state fields, edge cases, and verification. -- Compress reasoning into facts, constraints, tradeoffs, decisions, and checks. Action-oriented and dense. -- Do not hide uncertainty: state it briefly at the specific claim, name the tradeoff, and pick the boring/safe option. -- For code, focus on invariants, risks, and verification. -- Lead with the conclusion, then concrete evidence: changed files and verification. - -# Reasoning Format -- Problem: what is wrong. -- Decision: what to do & why (concrete facts). -- Check: what can break & how to verify result. -- Next: the next concrete edit/action. - -# Succinct Patterns -- Y → Need update X. -- This is safe: Z. -- Could do A, but B avoids C. - +{{#if personality}} + +{{personality}} + +{{/if}} - NEVER narrate about or consider session limits, token/tool budgets, effort estimates, or how much of task you think you can finish. Not your concern: diff --git a/packages/coding-agent/src/sdk.ts b/packages/coding-agent/src/sdk.ts index ba2479ea4..de716f8a0 100644 --- a/packages/coding-agent/src/sdk.ts +++ b/packages/coding-agent/src/sdk.ts @@ -1979,6 +1979,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {} workspaceTree: workspaceTreePromise, memoryRootEnabled: memoryBackend.id === "local", model: settings.get("includeModelInPrompt") ? getActiveModelString() : undefined, + personality: agentKind === "sub" ? "none" : settings.get("personality"), }); if (options.systemPrompt === undefined) { diff --git a/packages/coding-agent/src/system-prompt.ts b/packages/coding-agent/src/system-prompt.ts index 79e98e152..3a5120c08 100644 --- a/packages/coding-agent/src/system-prompt.ts +++ b/packages/coding-agent/src/system-prompt.ts @@ -9,17 +9,27 @@ import { $ } from "bun"; import { contextFileCapability } from "./capability/context-file"; import { systemPromptCapability } from "./capability/system-prompt"; import { findConfigFile } from "./config"; -import type { SkillsSettings } from "./config/settings"; +import type { Personality, SkillsSettings } from "./config/settings"; import { type ContextFile, loadCapability, type SystemPrompt as SystemPromptFile } from "./discovery"; import { expandAtImports } from "./discovery/at-imports"; import { loadSkills, type Skill } from "./extensibility/skills"; import { hasObsidian } from "./internal-urls/vault-protocol"; import customSystemPromptTemplate from "./prompts/system/custom-system-prompt.md" with { type: "text" }; +import defaultPersonality from "./prompts/system/personalities/default.md" with { type: "text" }; +import friendlyPersonality from "./prompts/system/personalities/friendly.md" with { type: "text" }; +import pragmaticPersonality from "./prompts/system/personalities/pragmatic.md" with { type: "text" }; import projectPromptTemplate from "./prompts/system/project-prompt.md" with { type: "text" }; import systemPromptTemplate from "./prompts/system/system-prompt.md" with { type: "text" }; import { shortenPath } from "./tools/render-utils"; import { AGENTS_MD_LIMIT, buildWorkspaceTree, type WorkspaceTree } from "./workspace-tree"; +/** Bundled personality specs, keyed by the `personality` setting value. */ +const PERSONALITY_SPECS: Record, string> = { + default: defaultPersonality, + friendly: friendlyPersonality, + pragmatic: pragmaticPersonality, +}; + interface AlwaysApplyRule { name: string; content: string; @@ -385,6 +395,8 @@ export interface BuildSystemPromptOptions { memoryRootEnabled?: boolean; /** Active model identifier (e.g. "anthropic/claude-opus-4") surfaced to the agent. */ model?: string; + /** Personality preset rendered into the default system prompt. "none" omits the block. Default: "default" */ + personality?: Personality; } /** Result of building provider-facing system prompt messages. */ @@ -419,6 +431,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}): workspaceTree: providedWorkspaceTree, memoryRootEnabled = false, model, + personality = "default", } = options; const resolvedCwd = cwd ?? getProjectDir(); @@ -590,6 +603,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}): dateTime, cwd: promptCwd, model: model ?? "", + personality: personality === "none" ? "" : PERSONALITY_SPECS[personality].trim(), intentTracing: !!intentField, intentField: intentField ?? "", mcpDiscoveryMode, diff --git a/packages/coding-agent/test/system-prompt-personality.test.ts b/packages/coding-agent/test/system-prompt-personality.test.ts new file mode 100644 index 000000000..1706f07a1 --- /dev/null +++ b/packages/coding-agent/test/system-prompt-personality.test.ts @@ -0,0 +1,63 @@ +import { afterEach, beforeEach, describe, expect, it } from "bun:test"; +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; +import type { Personality } from "@oh-my-pi/pi-coding-agent/config/settings-schema"; +import { buildSystemPrompt } from "@oh-my-pi/pi-coding-agent/system-prompt"; +import { cleanupTempHome } from "./helpers/temp-home-cleanup"; + +const EMPTY_TREE = { + rootPath: "", + rendered: "", + truncated: false, + totalLines: 0, + agentsMdFiles: [], +}; + +describe("system prompt personality block", () => { + let tempDir = ""; + let tempHomeDir = ""; + let originalHome: string | undefined; + + beforeEach(() => { + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-prompt-personality-")); + tempHomeDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-prompt-personality-home-")); + originalHome = process.env.HOME; + process.env.HOME = tempHomeDir; + }); + + afterEach(cleanupTempHome(() => ({ tempDir, tempHomeDir, originalHome }))); + + async function render(personality?: Personality): Promise { + const { systemPrompt } = await buildSystemPrompt({ + cwd: tempDir, + contextFiles: [], + skills: [], + rules: [], + toolNames: [], + workspaceTree: { ...EMPTY_TREE, rootPath: tempDir }, + personality, + }); + return systemPrompt.join("\n\n"); + } + + it("injects the default personality when the option is unset", async () => { + const rendered = await render(); + expect(rendered).toContain(""); + expect(rendered).toContain(""); + expect(rendered).toContain("terse, evidence-first engineer"); + }); + + it("replaces the default spec when a non-default personality is selected", async () => { + const rendered = await render("friendly"); + expect(rendered).toContain(""); + expect(rendered).toContain("warm, supportive collaborator"); + expect(rendered).not.toContain("terse, evidence-first engineer"); + }); + + it('omits the personality block entirely for "none"', async () => { + const rendered = await render("none"); + expect(rendered).not.toContain(""); + expect(rendered).not.toContain(""); + }); +});