diff --git a/src/tools/delegate-task/constants.ts b/src/tools/delegate-task/constants.ts index 43961ddf7..e0e5499c1 100644 --- a/src/tools/delegate-task/constants.ts +++ b/src/tools/delegate-task/constants.ts @@ -185,4 +185,49 @@ export const CATEGORY_DESCRIPTIONS: Record = { 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 = ` +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="") + - call_omo_agent(agent="librarian", background=true, prompt="") + +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. + + +` + +/** + * 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)) +} + diff --git a/src/tools/delegate-task/tools.test.ts b/src/tools/delegate-task/tools.test.ts index 0c455fe9b..76425ce6e 100644 --- a/src/tools/delegate-task/tools.test.ts +++ b/src/tools/delegate-task/tools.test.ts @@ -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("") + 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("") + 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("") + 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("") + }) + + 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("") + }) }) describe("modelInfo detection via resolveCategoryConfig", () => { diff --git a/src/tools/delegate-task/tools.ts b/src/tools/delegate-task/tools.ts index f2007d204..64389808b 100644 --- a/src/tools/delegate-task/tools.ts +++ b/src/tools/delegate-task/tools.ts @@ -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 } : {}),