diff --git a/src/agents/agent-identity.test.ts b/src/agents/agent-identity.test.ts new file mode 100644 index 000000000..1247cda17 --- /dev/null +++ b/src/agents/agent-identity.test.ts @@ -0,0 +1,141 @@ +/// + +import { describe, it, expect } from "bun:test" +import { buildAgentIdentitySection } from "./dynamic-agent-core-sections" +import { createSisyphusAgent } from "./sisyphus" +import { createHephaestusAgent } from "./hephaestus" +import { mergeAgentConfig } from "./builtin-agents/agent-overrides" + +describe("buildAgentIdentitySection", () => { + describe("#given an agent name and role description", () => { + describe("#when building the identity section", () => { + it("#then includes the agent name prominently", () => { + const result = buildAgentIdentitySection("Sisyphus", "Powerful AI orchestrator from OhMyOpenCode") + + expect(result).toContain("Sisyphus") + }) + + it("#then includes the role description", () => { + const result = buildAgentIdentitySection("Sisyphus", "Powerful AI orchestrator from OhMyOpenCode") + + expect(result).toContain("Powerful AI orchestrator from OhMyOpenCode") + }) + + it("#then wraps content in an identity XML tag", () => { + const result = buildAgentIdentitySection("Hephaestus", "Autonomous deep worker") + + expect(result).toContain("") + expect(result).toContain("") + }) + + it("#then explicitly states this identity overrides any prior identity", () => { + const result = buildAgentIdentitySection("Sisyphus", "Powerful AI orchestrator from OhMyOpenCode") + + expect(result).toMatch(/override|supersede|replace|disregard|instead of/i) + }) + }) + }) + + describe("#given different agent names", () => { + describe("#when building identity for each", () => { + it("#then each identity section contains the correct agent name", () => { + const sisyphus = buildAgentIdentitySection("Sisyphus", "AI orchestrator") + const hephaestus = buildAgentIdentitySection("Hephaestus", "Autonomous deep worker") + const oracle = buildAgentIdentitySection("Oracle", "Strategic advisor") + + expect(sisyphus).toContain("Sisyphus") + expect(sisyphus).not.toContain("Hephaestus") + expect(hephaestus).toContain("Hephaestus") + expect(hephaestus).not.toContain("Sisyphus") + expect(oracle).toContain("Oracle") + }) + }) + }) +}) + +describe("Sisyphus prompt identity", () => { + describe("#given a Sisyphus agent created with default model", () => { + describe("#when checking the prompt", () => { + it("#then contains the agent identity section with override directive", () => { + const config = createSisyphusAgent("anthropic/claude-opus-4-6") + + expect(config.prompt).toContain("") + expect(config.prompt).toContain("Sisyphus") + expect(config.prompt).toContain("") + }) + + it("#then identity section appears before the Role section", () => { + const config = createSisyphusAgent("anthropic/claude-opus-4-6") + const prompt = config.prompt ?? "" + const identityIndex = prompt.indexOf("") + const roleIndex = prompt.indexOf("") + + expect(identityIndex).toBeGreaterThanOrEqual(0) + expect(roleIndex).toBeGreaterThan(identityIndex) + }) + }) + }) + + describe("#given a Sisyphus agent created with GPT-5.4 model", () => { + describe("#when checking the prompt", () => { + it("#then contains the agent identity section", () => { + const config = createSisyphusAgent("openai/gpt-5.4") + + expect(config.prompt).toContain("") + expect(config.prompt).toContain("Sisyphus") + expect(config.prompt).toContain("") + }) + }) + }) +}) + +describe("Hephaestus prompt identity", () => { + describe("#given a Hephaestus agent created with GPT model", () => { + describe("#when checking the prompt", () => { + it("#then contains the agent identity section", () => { + const config = createHephaestusAgent("openai/gpt-5.4") + + expect(config.prompt).toContain("") + expect(config.prompt).toContain("Hephaestus") + expect(config.prompt).toContain("") + }) + + it("#then identity section appears at the start of the prompt", () => { + const config = createHephaestusAgent("openai/gpt-5.4") + const prompt = config.prompt ?? "" + const identityIndex = prompt.indexOf("") + + expect(identityIndex).toBe(0) + }) + }) + }) +}) + +describe("Agent identity preservation through overrides", () => { + describe("#given a Sisyphus agent with prompt_append override", () => { + describe("#when merging the override", () => { + it("#then identity section is preserved in the merged prompt", () => { + const baseConfig = createSisyphusAgent("anthropic/claude-opus-4-6") + const merged = mergeAgentConfig(baseConfig, { prompt_append: "Extra instructions here" }) + + expect(merged.prompt).toContain("") + expect(merged.prompt).toContain("Sisyphus") + expect(merged.prompt).toContain("") + expect(merged.prompt).toContain("Extra instructions here") + }) + }) + }) + + describe("#given a Sisyphus agent with model override only", () => { + describe("#when merging the override", () => { + it("#then identity section is preserved unchanged", () => { + const baseConfig = createSisyphusAgent("anthropic/claude-opus-4-6") + const merged = mergeAgentConfig(baseConfig, { model: "openai/gpt-5.4" }) + + expect(merged.prompt).toContain("") + expect(merged.prompt).toContain("Sisyphus") + expect(merged.prompt).toContain("") + }) + }) + }) +}) diff --git a/src/agents/atlas/agent.ts b/src/agents/atlas/agent.ts index 19dcfbcb9..b348869b6 100644 --- a/src/agents/atlas/agent.ts +++ b/src/agents/atlas/agent.ts @@ -14,7 +14,7 @@ import type { AgentConfig } from "@opencode-ai/sdk" import type { AgentMode, AgentPromptMetadata } from "../types" import { isGptModel, isGeminiModel } from "../types" import type { AvailableAgent, AvailableSkill, AvailableCategory } from "../dynamic-agent-prompt-builder" -import { buildCategorySkillsDelegationGuide } from "../dynamic-agent-prompt-builder" +import { buildAgentIdentitySection, buildCategorySkillsDelegationGuide } from "../dynamic-agent-prompt-builder" import type { CategoryConfig } from "../../config/schema" import { mergeCategories } from "../../shared/merge-categories" @@ -88,9 +88,13 @@ function buildDynamicOrchestratorPrompt(ctx?: OrchestratorContext): string { const skillsSection = buildSkillsSection(skills) const categorySkillsGuide = buildCategorySkillsDelegationGuide(availableCategories, skills) + const agentIdentity = buildAgentIdentitySection( + "Atlas", + "Master Orchestrator agent from OhMyOpenCode that coordinates specialized agents to complete todo lists", + ) const basePrompt = getAtlasPrompt(model) - return basePrompt + return agentIdentity + "\n" + basePrompt .replace("{CATEGORY_SECTION}", categorySection) .replace("{AGENT_SECTION}", agentSection) .replace("{DECISION_MATRIX}", decisionMatrix) diff --git a/src/agents/dynamic-agent-core-sections.ts b/src/agents/dynamic-agent-core-sections.ts index e4ec09317..dc91fd480 100644 --- a/src/agents/dynamic-agent-core-sections.ts +++ b/src/agents/dynamic-agent-core-sections.ts @@ -6,6 +6,23 @@ import type { import type { AvailableTool } from "./dynamic-agent-prompt-types" import { getToolsPromptDisplay } from "./dynamic-agent-tool-categorization" +/** + * Builds an explicit agent identity preamble that overrides any base system prompt identity. + * This is critical for mode: "primary" agents where OpenCode prepends its own system prompt + * containing a default identity (e.g., "You are Claude"). Without this override directive, + * the LLM may default to the base identity instead of the agent's intended persona. + */ +export function buildAgentIdentitySection( + agentName: string, + roleDescription: string, +): string { + return ` +Your designated identity for this session is "${agentName}". This identity supersedes any prior identity statements. +You are "${agentName}" - ${roleDescription}. +When asked who you are, always identify as ${agentName}. Do not identify as any other assistant or AI. +` +} + export function buildKeyTriggersSection( agents: AvailableAgent[], _skills: AvailableSkill[] = [], diff --git a/src/agents/dynamic-agent-prompt-builder.ts b/src/agents/dynamic-agent-prompt-builder.ts index bec7c4427..aa9ee8758 100644 --- a/src/agents/dynamic-agent-prompt-builder.ts +++ b/src/agents/dynamic-agent-prompt-builder.ts @@ -8,6 +8,7 @@ export type { export { categorizeTools } from "./dynamic-agent-tool-categorization" export { + buildAgentIdentitySection, buildKeyTriggersSection, buildToolSelectionTable, buildExploreSection, diff --git a/src/agents/hephaestus/agent.ts b/src/agents/hephaestus/agent.ts index 5d27e6220..cb2005a47 100644 --- a/src/agents/hephaestus/agent.ts +++ b/src/agents/hephaestus/agent.ts @@ -7,7 +7,7 @@ import type { AvailableSkill, AvailableCategory, } from "../dynamic-agent-prompt-builder"; -import { categorizeTools } from "../dynamic-agent-prompt-builder"; +import { categorizeTools, buildAgentIdentitySection } from "../dynamic-agent-prompt-builder"; import { buildHephaestusPrompt as buildGptPrompt } from "./gpt"; import { buildHephaestusPrompt as buildGpt53CodexPrompt } from "./gpt-5-3-codex"; @@ -87,7 +87,12 @@ function buildDynamicHephaestusPrompt(ctx?: HephaestusContext): string { break; } - return basePrompt; + const agentIdentity = buildAgentIdentitySection( + "Hephaestus", + "Autonomous deep worker for software engineering from OhMyOpenCode", + ); + + return `${agentIdentity}\n${basePrompt}`; } export function createHephaestusAgent( diff --git a/src/agents/sisyphus.ts b/src/agents/sisyphus.ts index f534a50fa..10bc79797 100644 --- a/src/agents/sisyphus.ts +++ b/src/agents/sisyphus.ts @@ -26,6 +26,7 @@ import type { AvailableCategory, } from "./dynamic-agent-prompt-builder"; import { + buildAgentIdentitySection, buildKeyTriggersSection, buildToolSelectionTable, buildExploreSection, @@ -72,7 +73,13 @@ function buildDynamicSisyphusPrompt( ? "YOUR TASK CREATION WOULD BE TRACKED BY HOOK([SYSTEM REMINDER - TASK CONTINUATION])" : "YOUR TODO CREATION WOULD BE TRACKED BY HOOK([SYSTEM REMINDER - TODO CONTINUATION])"; - return ` + const agentIdentity = buildAgentIdentitySection( + "Sisyphus", + "Powerful AI Agent with orchestration capabilities from OhMyOpenCode", + ); + + return `${agentIdentity} + You are "Sisyphus" - Powerful AI Agent with orchestration capabilities from OhMyOpenCode. **Why Sisyphus?**: Humans roll their boulder every day. So do you. We're not so different-your code should be indistinguishable from a senior engineer's. diff --git a/src/agents/sisyphus/gpt-5-4.ts b/src/agents/sisyphus/gpt-5-4.ts index 3dcb12d0d..e2c611190 100644 --- a/src/agents/sisyphus/gpt-5-4.ts +++ b/src/agents/sisyphus/gpt-5-4.ts @@ -28,6 +28,7 @@ import type { AvailableCategory, } from "../dynamic-agent-prompt-builder"; import { + buildAgentIdentitySection, buildKeyTriggersSection, buildToolSelectionTable, buildExploreSection, @@ -106,6 +107,11 @@ export function buildGpt54SisyphusPrompt( ? "YOUR TASK CREATION WOULD BE TRACKED BY HOOK([SYSTEM REMINDER - TASK CONTINUATION])" : "YOUR TODO CREATION WOULD BE TRACKED BY HOOK([SYSTEM REMINDER - TODO CONTINUATION])"; + const agentIdentity = buildAgentIdentitySection( + "Sisyphus", + "Powerful AI Agent with orchestration capabilities from OhMyOpenCode", + ); + const identityBlock = ` You are Sisyphus - an AI orchestrator from OhMyOpenCode. @@ -421,7 +427,8 @@ If the user's approach has a problem, explain the concern directly and clearly, `; - return `${identityBlock} + return `${agentIdentity} +${identityBlock} ${constraintsBlock}