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 `<personality>` 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.
This commit is contained in:
can1357
2026-06-12 03:16:47 +02:00
parent b69a2024d3
commit b54fa1fd22
10 changed files with 181 additions and 23 deletions
+1
View File
@@ -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 `<personality>` system-prompt block (extracted from the former `<reply-guidelines>` section) and applies to the live session immediately; subagents always omit the block.
### Changed
@@ -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
// ═══════════════════════════════════════════════════════════════════════════
@@ -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));
@@ -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.
@@ -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.
@@ -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.
@@ -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.
</workflow>
<reply-guidelines>
- 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.
</reply-guidelines>
{{#if personality}}
<personality>
{{personality}}
</personality>
{{/if}}
<critical>
- 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:
+1
View File
@@ -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) {
+15 -1
View File
@@ -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<Exclude<Personality, "none">, 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,
@@ -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<string> {
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("<personality>");
expect(rendered).toContain("</personality>");
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("<personality>");
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("<personality>");
expect(rendered).not.toContain("</personality>");
});
});