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