Addresses user reports where Sisyphus/Hephaestus would delegate tasks to explore/librarian subagents but then immediately perform the same search/work themselves, wasting context and defeating the purpose of delegation. Changes: - Add Anti-Duplication section to dynamic-agent-prompt-builder with clear rules: once you delegate, do NOT manually re-do the same search - Update all agent prompts (sisyphus, hephaestus, sis-junior gemini/gpt) to use 'non-overlapping work' instead of 'keep working' after delegation - Add buildAntiDuplicationSection() with explicit examples of forbidden vs allowed behavior after delegation - Add 'Delegation Trust Rule' to explore section - Add 'Delegation Duplication' to anti-patterns list Atlas fixes: - Add AUTO-CONTINUE POLICY to all Atlas variants (default, gemini, gpt) preventing the 'should I continue?' confirmation loop between plan steps - Fix plan file path in default atlas (.sisyphus/tasks/ -> .sisyphus/plans/) - Update GPT atlas uncertainty section to only ask questions during initial plan analysis, not during execution Fixes: subagent delegation duplication, Atlas continuation prompting
464 lines
16 KiB
TypeScript
464 lines
16 KiB
TypeScript
import type { AgentPromptMetadata } from "./types"
|
|
|
|
export interface AvailableAgent {
|
|
name: string
|
|
description: string
|
|
metadata: AgentPromptMetadata
|
|
}
|
|
|
|
export interface AvailableTool {
|
|
name: string
|
|
category: "lsp" | "ast" | "search" | "session" | "command" | "other"
|
|
}
|
|
|
|
export interface AvailableSkill {
|
|
name: string
|
|
description: string
|
|
location: "user" | "project" | "plugin"
|
|
}
|
|
|
|
export interface AvailableCategory {
|
|
name: string
|
|
description: string
|
|
model?: string
|
|
}
|
|
|
|
export function categorizeTools(toolNames: string[]): AvailableTool[] {
|
|
return toolNames.map((name) => {
|
|
let category: AvailableTool["category"] = "other"
|
|
if (name.startsWith("lsp_")) {
|
|
category = "lsp"
|
|
} else if (name.startsWith("ast_grep")) {
|
|
category = "ast"
|
|
} else if (name === "grep" || name === "glob") {
|
|
category = "search"
|
|
} else if (name.startsWith("session_")) {
|
|
category = "session"
|
|
} else if (name === "skill") {
|
|
category = "command"
|
|
}
|
|
return { name, category }
|
|
})
|
|
}
|
|
|
|
function formatToolsForPrompt(tools: AvailableTool[]): string {
|
|
const lspTools = tools.filter((t) => t.category === "lsp")
|
|
const astTools = tools.filter((t) => t.category === "ast")
|
|
const searchTools = tools.filter((t) => t.category === "search")
|
|
|
|
const parts: string[] = []
|
|
|
|
if (searchTools.length > 0) {
|
|
parts.push(...searchTools.map((t) => `\`${t.name}\``))
|
|
}
|
|
|
|
if (lspTools.length > 0) {
|
|
parts.push("`lsp_*`")
|
|
}
|
|
|
|
if (astTools.length > 0) {
|
|
parts.push("`ast_grep`")
|
|
}
|
|
|
|
return parts.join(", ")
|
|
}
|
|
|
|
export function buildKeyTriggersSection(agents: AvailableAgent[], _skills: AvailableSkill[] = []): string {
|
|
const keyTriggers = agents
|
|
.filter((a) => a.metadata.keyTrigger)
|
|
.map((a) => `- ${a.metadata.keyTrigger}`)
|
|
|
|
if (keyTriggers.length === 0) return ""
|
|
|
|
return `### Key Triggers (check BEFORE classification):
|
|
|
|
${keyTriggers.join("\n")}
|
|
- **"Look into" + "create PR"** → Not just research. Full implementation cycle expected.`
|
|
}
|
|
|
|
export function buildToolSelectionTable(
|
|
agents: AvailableAgent[],
|
|
tools: AvailableTool[] = [],
|
|
_skills: AvailableSkill[] = []
|
|
): string {
|
|
const rows: string[] = [
|
|
"### Tool & Agent Selection:",
|
|
"",
|
|
]
|
|
|
|
if (tools.length > 0) {
|
|
const toolsDisplay = formatToolsForPrompt(tools)
|
|
rows.push(`- ${toolsDisplay} — **FREE** — Not Complex, Scope Clear, No Implicit Assumptions`)
|
|
}
|
|
|
|
const costOrder = { FREE: 0, CHEAP: 1, EXPENSIVE: 2 }
|
|
const sortedAgents = [...agents]
|
|
.filter((a) => a.metadata.category !== "utility")
|
|
.sort((a, b) => costOrder[a.metadata.cost] - costOrder[b.metadata.cost])
|
|
|
|
for (const agent of sortedAgents) {
|
|
const shortDesc = agent.description.split(".")[0] || agent.description
|
|
rows.push(`- \`${agent.name}\` agent — **${agent.metadata.cost}** — ${shortDesc}`)
|
|
}
|
|
|
|
rows.push("")
|
|
rows.push("**Default flow**: explore/librarian (background) + tools → oracle (if required)")
|
|
|
|
return rows.join("\n")
|
|
}
|
|
|
|
export function buildExploreSection(agents: AvailableAgent[]): string {
|
|
const exploreAgent = agents.find((a) => a.name === "explore")
|
|
if (!exploreAgent) return ""
|
|
|
|
const useWhen = exploreAgent.metadata.useWhen || []
|
|
const avoidWhen = exploreAgent.metadata.avoidWhen || []
|
|
|
|
return `### Explore Agent = Contextual Grep
|
|
|
|
Use it as a **peer tool**, not a fallback. Fire liberally.
|
|
|
|
**Delegation Trust Rule:** Once you fire an explore agent for a search, do **not** manually perform that same search yourself. Use direct tools only for non-overlapping work or when you intentionally skipped delegation.
|
|
|
|
**Use Direct Tools when:**
|
|
${avoidWhen.map((w) => `- ${w}`).join("\n")}
|
|
|
|
**Use Explore Agent when:**
|
|
${useWhen.map((w) => `- ${w}`).join("\n")}`
|
|
}
|
|
|
|
export function buildLibrarianSection(agents: AvailableAgent[]): string {
|
|
const librarianAgent = agents.find((a) => a.name === "librarian")
|
|
if (!librarianAgent) return ""
|
|
|
|
const useWhen = librarianAgent.metadata.useWhen || []
|
|
|
|
return `### Librarian Agent = Reference Grep
|
|
|
|
Search **external references** (docs, OSS, web). Fire proactively when unfamiliar libraries are involved.
|
|
|
|
**Contextual Grep (Internal)** — search OUR codebase, find patterns in THIS repo, project-specific logic.
|
|
**Reference Grep (External)** — search EXTERNAL resources, official API docs, library best practices, OSS implementation examples.
|
|
|
|
**Trigger phrases** (fire librarian immediately):
|
|
${useWhen.map((w) => `- "${w}"`).join("\n")}`
|
|
}
|
|
|
|
export function buildDelegationTable(agents: AvailableAgent[]): string {
|
|
const rows: string[] = [
|
|
"### Delegation Table:",
|
|
"",
|
|
]
|
|
|
|
for (const agent of agents) {
|
|
for (const trigger of agent.metadata.triggers) {
|
|
rows.push(`- **${trigger.domain}** → \`${agent.name}\` — ${trigger.trigger}`)
|
|
}
|
|
}
|
|
|
|
return rows.join("\n")
|
|
}
|
|
|
|
|
|
export function buildCategorySkillsDelegationGuide(categories: AvailableCategory[], skills: AvailableSkill[]): string {
|
|
if (categories.length === 0 && skills.length === 0) return ""
|
|
|
|
const categoryRows = categories.map((c) => {
|
|
const desc = c.description || c.name
|
|
return `- \`${c.name}\` — ${desc}`
|
|
})
|
|
|
|
const builtinSkills = skills.filter((s) => s.location === "plugin")
|
|
const customSkills = skills.filter((s) => s.location !== "plugin")
|
|
|
|
const builtinNames = builtinSkills.map((s) => s.name).join(", ")
|
|
const customNames = customSkills.map((s) => {
|
|
const source = s.location === "project" ? "project" : "user"
|
|
return `${s.name} (${source})`
|
|
}).join(", ")
|
|
|
|
let skillsSection: string
|
|
|
|
if (customSkills.length > 0 && builtinSkills.length > 0) {
|
|
skillsSection = `#### Available Skills (via \`skill\` tool)
|
|
|
|
**Built-in**: ${builtinNames}
|
|
**⚡ YOUR SKILLS (PRIORITY)**: ${customNames}
|
|
|
|
> User-installed skills OVERRIDE built-in defaults. ALWAYS prefer YOUR SKILLS when domain matches.
|
|
> Full skill descriptions → use the \`skill\` tool to check before EVERY delegation.`
|
|
} else if (customSkills.length > 0) {
|
|
skillsSection = `#### Available Skills (via \`skill\` tool)
|
|
|
|
**⚡ YOUR SKILLS (PRIORITY)**: ${customNames}
|
|
|
|
> User-installed skills OVERRIDE built-in defaults. ALWAYS prefer YOUR SKILLS when domain matches.
|
|
> Full skill descriptions → use the \`skill\` tool to check before EVERY delegation.`
|
|
} else if (builtinSkills.length > 0) {
|
|
skillsSection = `#### Available Skills (via \`skill\` tool)
|
|
|
|
**Built-in**: ${builtinNames}
|
|
|
|
> Full skill descriptions → use the \`skill\` tool to check before EVERY delegation.`
|
|
} else {
|
|
skillsSection = ""
|
|
}
|
|
|
|
return `### Category + Skills Delegation System
|
|
|
|
**task() combines categories and skills for optimal task execution.**
|
|
|
|
#### Available Categories (Domain-Optimized Models)
|
|
|
|
Each category is configured with a model optimized for that domain. Read the description to understand when to use it.
|
|
|
|
${categoryRows.join("\n")}
|
|
|
|
${skillsSection}
|
|
|
|
---
|
|
|
|
### MANDATORY: Category + Skill Selection Protocol
|
|
|
|
**STEP 1: Select Category**
|
|
- Read each category's description
|
|
- Match task requirements to category domain
|
|
- Select the category whose domain BEST fits the task
|
|
|
|
**STEP 2: Evaluate ALL Skills**
|
|
Check the \`skill\` tool for available skills and their descriptions. For EVERY skill, ask:
|
|
> "Does this skill's expertise domain overlap with my task?"
|
|
|
|
- If YES → INCLUDE in \`load_skills=[...]\`
|
|
- If NO → OMIT (no justification needed)
|
|
${customSkills.length > 0 ? `
|
|
> **User-installed skills get PRIORITY.** When in doubt, INCLUDE rather than omit.` : ""}
|
|
|
|
---
|
|
|
|
### Delegation Pattern
|
|
|
|
\`\`\`typescript
|
|
task(
|
|
category="[selected-category]",
|
|
load_skills=["skill-1", "skill-2"], // Include ALL relevant skills — ESPECIALLY user-installed ones
|
|
prompt="..."
|
|
)
|
|
\`\`\`
|
|
|
|
**ANTI-PATTERN (will produce poor results):**
|
|
\`\`\`typescript
|
|
task(category="...", load_skills=[], run_in_background=false, prompt="...") // Empty load_skills without justification
|
|
\`\`\``
|
|
}
|
|
|
|
export function buildOracleSection(agents: AvailableAgent[]): string {
|
|
const oracleAgent = agents.find((a) => a.name === "oracle")
|
|
if (!oracleAgent) return ""
|
|
|
|
const useWhen = oracleAgent.metadata.useWhen || []
|
|
const avoidWhen = oracleAgent.metadata.avoidWhen || []
|
|
|
|
return `<Oracle_Usage>
|
|
## Oracle — Read-Only High-IQ Consultant
|
|
|
|
Oracle is a read-only, expensive, high-quality reasoning model for debugging and architecture. Consultation only.
|
|
|
|
### WHEN to Consult (Oracle FIRST, then implement):
|
|
|
|
${useWhen.map((w) => `- ${w}`).join("\n")}
|
|
|
|
### WHEN NOT to Consult:
|
|
|
|
${avoidWhen.map((w) => `- ${w}`).join("\n")}
|
|
|
|
### Usage Pattern:
|
|
Briefly announce "Consulting Oracle for [reason]" before invocation.
|
|
|
|
**Exception**: This is the ONLY case where you announce before acting. For all other work, start immediately without status updates.
|
|
|
|
### Oracle Background Task Policy:
|
|
|
|
**Collect Oracle results before your final answer. No exceptions.**
|
|
|
|
- Oracle takes minutes. When done with your own work: **end your response** — wait for the \`<system-reminder>\`.
|
|
- Do NOT poll \`background_output\` on a running Oracle. The notification will come.
|
|
- Never cancel Oracle.
|
|
</Oracle_Usage>`
|
|
}
|
|
|
|
export function buildHardBlocksSection(): string {
|
|
const blocks = [
|
|
"- Type error suppression (`as any`, `@ts-ignore`) — **Never**",
|
|
"- Commit without explicit request — **Never**",
|
|
"- Speculate about unread code — **Never**",
|
|
"- Leave code in broken state after failures — **Never**",
|
|
"- `background_cancel(all=true)` — **Never.** Always cancel individually by taskId.",
|
|
"- Delivering final answer before collecting Oracle result — **Never.**",
|
|
]
|
|
|
|
return `## Hard Blocks (NEVER violate)
|
|
|
|
${blocks.join("\n")}`
|
|
}
|
|
|
|
export function buildAntiPatternsSection(): string {
|
|
const patterns = [
|
|
"- **Type Safety**: `as any`, `@ts-ignore`, `@ts-expect-error`",
|
|
"- **Error Handling**: Empty catch blocks `catch(e) {}`",
|
|
"- **Testing**: Deleting failing tests to \"pass\"",
|
|
"- **Search**: Firing agents for single-line typos or obvious syntax errors",
|
|
"- **Debugging**: Shotgun debugging, random changes",
|
|
"- **Background Tasks**: Polling `background_output` on running tasks — end response and wait for notification",
|
|
"- **Delegation Duplication**: Delegating exploration to explore/librarian and then manually doing the same search yourself",
|
|
"- **Oracle**: Delivering answer without collecting Oracle results",
|
|
]
|
|
|
|
return `## Anti-Patterns (BLOCKING violations)
|
|
|
|
${patterns.join("\n")}`
|
|
}
|
|
|
|
export function buildNonClaudePlannerSection(model: string): string {
|
|
const isNonClaude = !model.toLowerCase().includes('claude')
|
|
if (!isNonClaude) return ""
|
|
|
|
return `### Plan Agent Dependency (Non-Claude)
|
|
|
|
Multi-step task? **ALWAYS consult Plan Agent first.** Do NOT start implementation without a plan.
|
|
|
|
- Single-file fix or trivial change → proceed directly
|
|
- Anything else (2+ steps, unclear scope, architecture) → \`task(subagent_type="plan", ...)\` FIRST
|
|
- Use \`session_id\` to resume the same Plan Agent — ask follow-up questions aggressively
|
|
- If ANY part of the task is ambiguous, ask Plan Agent before guessing
|
|
|
|
Plan Agent returns a structured work breakdown with parallel execution opportunities. Follow it.`
|
|
}
|
|
|
|
export function buildDeepParallelSection(model: string, categories: AvailableCategory[]): string {
|
|
const isNonClaude = !model.toLowerCase().includes('claude')
|
|
const hasDeepCategory = categories.some(c => c.name === 'deep')
|
|
|
|
if (!isNonClaude || !hasDeepCategory) return ""
|
|
|
|
return `### Deep Parallel Delegation
|
|
|
|
Delegate EVERY independent unit to a \`deep\` agent in parallel (\`run_in_background=true\`).
|
|
If a task decomposes into 4 independent units, spawn 4 agents simultaneously — not 1 at a time.
|
|
|
|
1. Decompose the implementation into independent work units
|
|
2. Assign one \`deep\` agent per unit — all via \`run_in_background=true\`
|
|
3. Give each agent a clear GOAL with success criteria, not step-by-step instructions
|
|
4. Collect all results, integrate, verify coherence across units`
|
|
}
|
|
|
|
export function buildUltraworkSection(
|
|
agents: AvailableAgent[],
|
|
categories: AvailableCategory[],
|
|
skills: AvailableSkill[]
|
|
): string {
|
|
const lines: string[] = []
|
|
|
|
if (categories.length > 0) {
|
|
lines.push("**Categories** (for implementation tasks):")
|
|
for (const cat of categories) {
|
|
const shortDesc = cat.description || cat.name
|
|
lines.push(`- \`${cat.name}\`: ${shortDesc}`)
|
|
}
|
|
lines.push("")
|
|
}
|
|
|
|
if (skills.length > 0) {
|
|
const builtinSkills = skills.filter((s) => s.location === "plugin")
|
|
const customSkills = skills.filter((s) => s.location !== "plugin")
|
|
|
|
if (builtinSkills.length > 0) {
|
|
lines.push("**Built-in Skills** (combine with categories):")
|
|
for (const skill of builtinSkills) {
|
|
const shortDesc = skill.description.split(".")[0] || skill.description
|
|
lines.push(`- \`${skill.name}\`: ${shortDesc}`)
|
|
}
|
|
lines.push("")
|
|
}
|
|
|
|
if (customSkills.length > 0) {
|
|
lines.push("**User-Installed Skills** (HIGH PRIORITY - user installed these for their workflow):")
|
|
for (const skill of customSkills) {
|
|
const shortDesc = skill.description.split(".")[0] || skill.description
|
|
lines.push(`- \`${skill.name}\`: ${shortDesc}`)
|
|
}
|
|
lines.push("")
|
|
}
|
|
}
|
|
|
|
if (agents.length > 0) {
|
|
const ultraworkAgentPriority = ["explore", "librarian", "plan", "oracle"]
|
|
const sortedAgents = [...agents].sort((a, b) => {
|
|
const aIdx = ultraworkAgentPriority.indexOf(a.name)
|
|
const bIdx = ultraworkAgentPriority.indexOf(b.name)
|
|
if (aIdx === -1 && bIdx === -1) return 0
|
|
if (aIdx === -1) return 1
|
|
if (bIdx === -1) return -1
|
|
return aIdx - bIdx
|
|
})
|
|
|
|
lines.push("**Agents** (for specialized consultation/exploration):")
|
|
for (const agent of sortedAgents) {
|
|
const shortDesc = agent.description.length > 120 ? agent.description.slice(0, 120) + "..." : agent.description
|
|
const suffix = agent.name === "explore" || agent.name === "librarian" ? " (multiple)" : ""
|
|
lines.push(`- \`${agent.name}${suffix}\`: ${shortDesc}`)
|
|
}
|
|
}
|
|
|
|
return lines.join("\n")
|
|
}
|
|
|
|
// Anti-duplication section for agent prompts
|
|
export function buildAntiDuplicationSection(): string {
|
|
return `<Anti_Duplication>
|
|
## Anti-Duplication Rule (CRITICAL)
|
|
|
|
Once you delegate exploration to explore/librarian agents, **DO NOT perform the same search yourself**.
|
|
|
|
### What this means:
|
|
|
|
**FORBIDDEN:**
|
|
- After firing explore/librarian, manually grep/search for the same information
|
|
- Re-doing the research the agents were just tasked with
|
|
- "Just quickly checking" the same files the background agents are checking
|
|
|
|
**ALLOWED:**
|
|
- Continue with **non-overlapping work** — work that doesn't depend on the delegated research
|
|
- Work on unrelated parts of the codebase
|
|
- Preparation work (e.g., setting up files, configs) that can proceed independently
|
|
|
|
### Wait for Results Properly:
|
|
|
|
When you need the delegated results but they're not ready:
|
|
|
|
1. **End your response** — do NOT continue with work that depends on those results
|
|
2. **Wait for the completion notification** — the system will trigger your next turn
|
|
3. **Then** collect results via \`background_output(task_id="...")\`
|
|
4. **Do NOT** impatiently re-search the same topics while waiting
|
|
|
|
### Why This Matters:
|
|
|
|
- **Wasted tokens**: Duplicate exploration wastes your context budget
|
|
- **Confusion**: You might contradict the agent's findings
|
|
- **Efficiency**: The whole point of delegation is parallel throughput
|
|
|
|
### Example:
|
|
|
|
\`\`\`typescript
|
|
// WRONG: After delegating, re-doing the search
|
|
task(subagent_type="explore", run_in_background=true, ...)
|
|
// Then immediately grep for the same thing yourself — FORBIDDEN
|
|
|
|
// CORRECT: Continue non-overlapping work
|
|
task(subagent_type="explore", run_in_background=true, ...)
|
|
// Work on a different, unrelated file while they search
|
|
// End your response and wait for the notification
|
|
\`\`\`
|
|
</Anti_Duplication>`
|
|
}
|