feat(delegate-task): prepend system prompt for plan agent invocations

When plan agent (plan/prometheus/planner) is invoked via delegate_task,
automatically prepend a <system> prompt instructing the agent to:
- Launch explore/librarian agents in background to gather context
- Summarize user request and list uncertainties
- Ask clarifying questions until requirements are 100% clear
This commit is contained in:
justsisyphus
2026-01-26 17:00:06 +09:00
parent 8429da02b8
commit 7e065dfe12
3 changed files with 231 additions and 9 deletions

View File

@@ -185,4 +185,49 @@ export const CATEGORY_DESCRIPTIONS: Record<string, string> = {
writing: "Documentation, prose, technical writing",
}
/**
* System prompt prepended to plan agent invocations.
* Instructs the plan agent to first gather context via explore/librarian agents,
* then summarize user requirements and clarify uncertainties before proceeding.
*/
export const PLAN_AGENT_SYSTEM_PREPEND = `<system>
BEFORE you begin planning, you MUST first understand the user's request deeply.
MANDATORY CONTEXT GATHERING PROTOCOL:
1. Launch background agents to gather context:
- call_omo_agent(agent="explore", background=true, prompt="<search for relevant patterns, files, and implementations in the codebase related to user's request>")
- call_omo_agent(agent="librarian", background=true, prompt="<search for external documentation, examples, and best practices related to user's request>")
2. After gathering context, ALWAYS present:
- **User Request Summary**: Concise restatement of what the user is asking for
- **Uncertainties**: List of unclear points, ambiguities, or assumptions you're making
- **Clarifying Questions**: Specific questions to resolve the uncertainties
3. ITERATE until ALL requirements are crystal clear:
- Do NOT proceed to planning until you have 100% clarity
- Ask the user to confirm your understanding
- Resolve every ambiguity before generating the work plan
REMEMBER: Vague requirements lead to failed implementations. Take the time to understand thoroughly.
</system>
`
/**
* List of agent names that should be treated as plan agents.
* Case-insensitive matching is used.
*/
export const PLAN_AGENT_NAMES = ["plan", "prometheus", "planner"]
/**
* Check if the given agent name is a plan agent.
* @param agentName - The agent name to check
* @returns true if the agent is a plan agent
*/
export function isPlanAgent(agentName: string | undefined): boolean {
if (!agentName) return false
const lowerName = agentName.toLowerCase().trim()
return PLAN_AGENT_NAMES.some(name => lowerName === name || lowerName.includes(name))
}

View File

@@ -1,5 +1,5 @@
import { describe, test, expect, beforeEach } from "bun:test"
import { DEFAULT_CATEGORIES, CATEGORY_PROMPT_APPENDS, CATEGORY_DESCRIPTIONS } from "./constants"
import { DEFAULT_CATEGORIES, CATEGORY_PROMPT_APPENDS, CATEGORY_DESCRIPTIONS, isPlanAgent, PLAN_AGENT_NAMES } from "./constants"
import { resolveCategoryConfig } from "./tools"
import type { CategoryConfig } from "../../config/schema"
import { __resetModelCache } from "../../shared/model-availability"
@@ -77,6 +77,87 @@ describe("sisyphus-task", () => {
})
})
describe("isPlanAgent", () => {
test("returns true for 'plan'", () => {
// #given / #when
const result = isPlanAgent("plan")
// #then
expect(result).toBe(true)
})
test("returns true for 'prometheus'", () => {
// #given / #when
const result = isPlanAgent("prometheus")
// #then
expect(result).toBe(true)
})
test("returns true for 'planner'", () => {
// #given / #when
const result = isPlanAgent("planner")
// #then
expect(result).toBe(true)
})
test("returns true for case-insensitive match 'PLAN'", () => {
// #given / #when
const result = isPlanAgent("PLAN")
// #then
expect(result).toBe(true)
})
test("returns true for case-insensitive match 'Prometheus'", () => {
// #given / #when
const result = isPlanAgent("Prometheus")
// #then
expect(result).toBe(true)
})
test("returns false for 'oracle'", () => {
// #given / #when
const result = isPlanAgent("oracle")
// #then
expect(result).toBe(false)
})
test("returns false for 'explore'", () => {
// #given / #when
const result = isPlanAgent("explore")
// #then
expect(result).toBe(false)
})
test("returns false for undefined", () => {
// #given / #when
const result = isPlanAgent(undefined)
// #then
expect(result).toBe(false)
})
test("returns false for empty string", () => {
// #given / #when
const result = isPlanAgent("")
// #then
expect(result).toBe(false)
})
test("PLAN_AGENT_NAMES contains expected values", () => {
// #given / #when / #then
expect(PLAN_AGENT_NAMES).toContain("plan")
expect(PLAN_AGENT_NAMES).toContain("prometheus")
expect(PLAN_AGENT_NAMES).toContain("planner")
})
})
describe("category delegation config validation", () => {
test("proceeds without error when systemDefaultModel is undefined", async () => {
// #given a mock client with no model in config
@@ -1481,6 +1562,87 @@ describe("sisyphus-task", () => {
expect(result).toContain(categoryPromptAppend)
expect(result).toContain("\n\n")
})
test("prepends plan agent system prompt when agentName is 'plan'", () => {
// #given
const { buildSystemContent } = require("./tools")
const { PLAN_AGENT_SYSTEM_PREPEND } = require("./constants")
// #when
const result = buildSystemContent({ agentName: "plan" })
// #then
expect(result).toContain("<system>")
expect(result).toContain("MANDATORY CONTEXT GATHERING PROTOCOL")
expect(result).toBe(PLAN_AGENT_SYSTEM_PREPEND)
})
test("prepends plan agent system prompt when agentName is 'prometheus'", () => {
// #given
const { buildSystemContent } = require("./tools")
const { PLAN_AGENT_SYSTEM_PREPEND } = require("./constants")
// #when
const result = buildSystemContent({ agentName: "prometheus" })
// #then
expect(result).toContain("<system>")
expect(result).toBe(PLAN_AGENT_SYSTEM_PREPEND)
})
test("prepends plan agent system prompt when agentName is 'Prometheus' (case insensitive)", () => {
// #given
const { buildSystemContent } = require("./tools")
const { PLAN_AGENT_SYSTEM_PREPEND } = require("./constants")
// #when
const result = buildSystemContent({ agentName: "Prometheus" })
// #then
expect(result).toContain("<system>")
expect(result).toBe(PLAN_AGENT_SYSTEM_PREPEND)
})
test("combines plan agent prepend with skill content", () => {
// #given
const { buildSystemContent } = require("./tools")
const { PLAN_AGENT_SYSTEM_PREPEND } = require("./constants")
const skillContent = "You are a planning expert"
// #when
const result = buildSystemContent({ skillContent, agentName: "plan" })
// #then
expect(result).toContain(PLAN_AGENT_SYSTEM_PREPEND)
expect(result).toContain(skillContent)
expect(result!.indexOf(PLAN_AGENT_SYSTEM_PREPEND)).toBeLessThan(result!.indexOf(skillContent))
})
test("does not prepend plan agent prompt for non-plan agents", () => {
// #given
const { buildSystemContent } = require("./tools")
const skillContent = "You are an expert"
// #when
const result = buildSystemContent({ skillContent, agentName: "oracle" })
// #then
expect(result).toBe(skillContent)
expect(result).not.toContain("<system>")
})
test("does not prepend plan agent prompt when agentName is undefined", () => {
// #given
const { buildSystemContent } = require("./tools")
const skillContent = "You are an expert"
// #when
const result = buildSystemContent({ skillContent, agentName: undefined })
// #then
expect(result).toBe(skillContent)
expect(result).not.toContain("<system>")
})
})
describe("modelInfo detection via resolveCategoryConfig", () => {

View File

@@ -4,7 +4,7 @@ import { join } from "node:path"
import type { BackgroundManager } from "../../features/background-agent"
import type { DelegateTaskArgs } from "./types"
import type { CategoryConfig, CategoriesConfig, GitMasterConfig, BrowserAutomationProvider } from "../../config/schema"
import { DEFAULT_CATEGORIES, CATEGORY_PROMPT_APPENDS, CATEGORY_DESCRIPTIONS } from "./constants"
import { DEFAULT_CATEGORIES, CATEGORY_PROMPT_APPENDS, CATEGORY_DESCRIPTIONS, PLAN_AGENT_SYSTEM_PREPEND, isPlanAgent } from "./constants"
import { findNearestMessageWithFields, findFirstMessageWithAgent, MESSAGE_STORAGE } from "../../features/hook-message-injector"
import { resolveMultipleSkillsAsync } from "../../features/opencode-skill-loader/skill-content"
import { discoverSkills } from "../../features/opencode-skill-loader"
@@ -171,20 +171,33 @@ export interface DelegateTaskToolOptions {
export interface BuildSystemContentInput {
skillContent?: string
categoryPromptAppend?: string
agentName?: string
}
export function buildSystemContent(input: BuildSystemContentInput): string | undefined {
const { skillContent, categoryPromptAppend } = input
const { skillContent, categoryPromptAppend, agentName } = input
if (!skillContent && !categoryPromptAppend) {
const planAgentPrepend = isPlanAgent(agentName) ? PLAN_AGENT_SYSTEM_PREPEND : ""
if (!skillContent && !categoryPromptAppend && !planAgentPrepend) {
return undefined
}
if (skillContent && categoryPromptAppend) {
return `${skillContent}\n\n${categoryPromptAppend}`
const parts: string[] = []
if (planAgentPrepend) {
parts.push(planAgentPrepend)
}
return skillContent || categoryPromptAppend
if (skillContent) {
parts.push(skillContent)
}
if (categoryPromptAppend) {
parts.push(categoryPromptAppend)
}
return parts.join("\n\n") || undefined
}
export function createDelegateTask(options: DelegateTaskToolOptions): ToolDefinition {
@@ -382,6 +395,7 @@ Use \`background_output\` with task_id="${task.id}" to check progress.`
task: false,
delegate_task: false,
call_omo_agent: true,
question: false,
},
parts: [{ type: "text", text: args.prompt }],
},
@@ -580,7 +594,7 @@ To continue this session: session_id="${args.session_id}"`
})
if (isUnstableAgent && isRunInBackgroundExplicitlyFalse) {
const systemContent = buildSystemContent({ skillContent, categoryPromptAppend })
const systemContent = buildSystemContent({ skillContent, categoryPromptAppend, agentName: agentToUse })
try {
const task = await manager.launch({
@@ -772,7 +786,7 @@ Sisyphus-Junior is spawned automatically when you specify a category. Pick the a
}
}
const systemContent = buildSystemContent({ skillContent, categoryPromptAppend })
const systemContent = buildSystemContent({ skillContent, categoryPromptAppend, agentName: agentToUse })
if (runInBackground) {
try {
@@ -903,6 +917,7 @@ To continue this session: session_id="${task.sessionID}"`
task: false,
delegate_task: false,
call_omo_agent: true,
question: false,
},
parts: [{ type: "text", text: args.prompt }],
...(categoryModel ? { model: categoryModel } : {}),