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:
@@ -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:
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user