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:
parent
8429da02b8
commit
7e065dfe12
@ -185,4 +185,49 @@ export const CATEGORY_DESCRIPTIONS: Record<string, string> = {
|
|||||||
writing: "Documentation, prose, technical writing",
|
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 { 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 { resolveCategoryConfig } from "./tools"
|
||||||
import type { CategoryConfig } from "../../config/schema"
|
import type { CategoryConfig } from "../../config/schema"
|
||||||
import { __resetModelCache } from "../../shared/model-availability"
|
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", () => {
|
describe("category delegation config validation", () => {
|
||||||
test("proceeds without error when systemDefaultModel is undefined", async () => {
|
test("proceeds without error when systemDefaultModel is undefined", async () => {
|
||||||
// #given a mock client with no model in config
|
// #given a mock client with no model in config
|
||||||
@ -1481,6 +1562,87 @@ describe("sisyphus-task", () => {
|
|||||||
expect(result).toContain(categoryPromptAppend)
|
expect(result).toContain(categoryPromptAppend)
|
||||||
expect(result).toContain("\n\n")
|
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", () => {
|
describe("modelInfo detection via resolveCategoryConfig", () => {
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { join } from "node:path"
|
|||||||
import type { BackgroundManager } from "../../features/background-agent"
|
import type { BackgroundManager } from "../../features/background-agent"
|
||||||
import type { DelegateTaskArgs } from "./types"
|
import type { DelegateTaskArgs } from "./types"
|
||||||
import type { CategoryConfig, CategoriesConfig, GitMasterConfig, BrowserAutomationProvider } from "../../config/schema"
|
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 { findNearestMessageWithFields, findFirstMessageWithAgent, MESSAGE_STORAGE } from "../../features/hook-message-injector"
|
||||||
import { resolveMultipleSkillsAsync } from "../../features/opencode-skill-loader/skill-content"
|
import { resolveMultipleSkillsAsync } from "../../features/opencode-skill-loader/skill-content"
|
||||||
import { discoverSkills } from "../../features/opencode-skill-loader"
|
import { discoverSkills } from "../../features/opencode-skill-loader"
|
||||||
@ -171,20 +171,33 @@ export interface DelegateTaskToolOptions {
|
|||||||
export interface BuildSystemContentInput {
|
export interface BuildSystemContentInput {
|
||||||
skillContent?: string
|
skillContent?: string
|
||||||
categoryPromptAppend?: string
|
categoryPromptAppend?: string
|
||||||
|
agentName?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildSystemContent(input: BuildSystemContentInput): string | undefined {
|
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
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
if (skillContent && categoryPromptAppend) {
|
const parts: string[] = []
|
||||||
return `${skillContent}\n\n${categoryPromptAppend}`
|
|
||||||
|
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 {
|
export function createDelegateTask(options: DelegateTaskToolOptions): ToolDefinition {
|
||||||
@ -382,6 +395,7 @@ Use \`background_output\` with task_id="${task.id}" to check progress.`
|
|||||||
task: false,
|
task: false,
|
||||||
delegate_task: false,
|
delegate_task: false,
|
||||||
call_omo_agent: true,
|
call_omo_agent: true,
|
||||||
|
question: false,
|
||||||
},
|
},
|
||||||
parts: [{ type: "text", text: args.prompt }],
|
parts: [{ type: "text", text: args.prompt }],
|
||||||
},
|
},
|
||||||
@ -580,7 +594,7 @@ To continue this session: session_id="${args.session_id}"`
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (isUnstableAgent && isRunInBackgroundExplicitlyFalse) {
|
if (isUnstableAgent && isRunInBackgroundExplicitlyFalse) {
|
||||||
const systemContent = buildSystemContent({ skillContent, categoryPromptAppend })
|
const systemContent = buildSystemContent({ skillContent, categoryPromptAppend, agentName: agentToUse })
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const task = await manager.launch({
|
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) {
|
if (runInBackground) {
|
||||||
try {
|
try {
|
||||||
@ -903,6 +917,7 @@ To continue this session: session_id="${task.sessionID}"`
|
|||||||
task: false,
|
task: false,
|
||||||
delegate_task: false,
|
delegate_task: false,
|
||||||
call_omo_agent: true,
|
call_omo_agent: true,
|
||||||
|
question: false,
|
||||||
},
|
},
|
||||||
parts: [{ type: "text", text: args.prompt }],
|
parts: [{ type: "text", text: args.prompt }],
|
||||||
...(categoryModel ? { model: categoryModel } : {}),
|
...(categoryModel ? { model: categoryModel } : {}),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user