Merge pull request #1653 from code-yeongyu/fix/plan-prometheus-decoupling

fix(delegation): decouple plan from prometheus and fix sync task responses
This commit is contained in:
YeonGyu-Kim
2026-02-08 13:46:40 +09:00
committed by GitHub
7 changed files with 458 additions and 194 deletions

View File

@@ -600,6 +600,187 @@ describe("Prometheus direct override priority over category", () => {
})
})
describe("Plan agent model inheritance from prometheus", () => {
test("plan agent inherits all model-related settings from resolved prometheus config", async () => {
//#given - prometheus resolves to claude-opus-4-6 with model settings
spyOn(shared, "resolveModelPipeline" as any).mockReturnValue({
model: "anthropic/claude-opus-4-6",
provenance: "provider-fallback",
variant: "max",
})
const pluginConfig: OhMyOpenCodeConfig = {
sisyphus_agent: {
planner_enabled: true,
replace_plan: true,
},
}
const config: Record<string, unknown> = {
model: "anthropic/claude-opus-4-6",
agent: {
plan: {
name: "plan",
mode: "primary",
prompt: "original plan prompt",
},
},
}
const handler = createConfigHandler({
ctx: { directory: "/tmp" },
pluginConfig,
modelCacheState: {
anthropicContext1MEnabled: false,
modelContextLimitsCache: new Map(),
},
})
//#when
await handler(config)
//#then - plan inherits model and variant from prometheus, but NOT prompt
const agents = config.agent as Record<string, { mode?: string; model?: string; variant?: string; prompt?: string }>
expect(agents.plan).toBeDefined()
expect(agents.plan.mode).toBe("subagent")
expect(agents.plan.model).toBe("anthropic/claude-opus-4-6")
expect(agents.plan.variant).toBe("max")
expect(agents.plan.prompt).toBeUndefined()
})
test("plan agent inherits temperature, reasoningEffort, and other model settings from prometheus", async () => {
//#given - prometheus configured with category that has temperature and reasoningEffort
spyOn(shared, "resolveModelPipeline" as any).mockReturnValue({
model: "openai/gpt-5.2",
provenance: "override",
variant: "high",
})
const pluginConfig: OhMyOpenCodeConfig = {
sisyphus_agent: {
planner_enabled: true,
replace_plan: true,
},
agents: {
prometheus: {
model: "openai/gpt-5.2",
variant: "high",
temperature: 0.3,
top_p: 0.9,
maxTokens: 16000,
reasoningEffort: "high",
textVerbosity: "medium",
thinking: { type: "enabled", budgetTokens: 8000 },
},
},
}
const config: Record<string, unknown> = {
model: "anthropic/claude-opus-4-6",
agent: {},
}
const handler = createConfigHandler({
ctx: { directory: "/tmp" },
pluginConfig,
modelCacheState: {
anthropicContext1MEnabled: false,
modelContextLimitsCache: new Map(),
},
})
//#when
await handler(config)
//#then - plan inherits ALL model-related settings from resolved prometheus
const agents = config.agent as Record<string, Record<string, unknown>>
expect(agents.plan).toBeDefined()
expect(agents.plan.mode).toBe("subagent")
expect(agents.plan.model).toBe("openai/gpt-5.2")
expect(agents.plan.variant).toBe("high")
expect(agents.plan.temperature).toBe(0.3)
expect(agents.plan.top_p).toBe(0.9)
expect(agents.plan.maxTokens).toBe(16000)
expect(agents.plan.reasoningEffort).toBe("high")
expect(agents.plan.textVerbosity).toBe("medium")
expect(agents.plan.thinking).toEqual({ type: "enabled", budgetTokens: 8000 })
})
test("plan agent user override takes priority over prometheus inherited settings", async () => {
//#given - prometheus resolves to opus, but user has plan override for gpt-5.2
spyOn(shared, "resolveModelPipeline" as any).mockReturnValue({
model: "anthropic/claude-opus-4-6",
provenance: "provider-fallback",
variant: "max",
})
const pluginConfig: OhMyOpenCodeConfig = {
sisyphus_agent: {
planner_enabled: true,
replace_plan: true,
},
agents: {
plan: {
model: "openai/gpt-5.2",
variant: "high",
temperature: 0.5,
},
},
}
const config: Record<string, unknown> = {
model: "anthropic/claude-opus-4-6",
agent: {},
}
const handler = createConfigHandler({
ctx: { directory: "/tmp" },
pluginConfig,
modelCacheState: {
anthropicContext1MEnabled: false,
modelContextLimitsCache: new Map(),
},
})
//#when
await handler(config)
//#then - plan uses its own override, not prometheus settings
const agents = config.agent as Record<string, Record<string, unknown>>
expect(agents.plan.model).toBe("openai/gpt-5.2")
expect(agents.plan.variant).toBe("high")
expect(agents.plan.temperature).toBe(0.5)
})
test("plan agent does NOT inherit prompt, description, or color from prometheus", async () => {
//#given
spyOn(shared, "resolveModelPipeline" as any).mockReturnValue({
model: "anthropic/claude-opus-4-6",
provenance: "provider-fallback",
variant: "max",
})
const pluginConfig: OhMyOpenCodeConfig = {
sisyphus_agent: {
planner_enabled: true,
replace_plan: true,
},
}
const config: Record<string, unknown> = {
model: "anthropic/claude-opus-4-6",
agent: {},
}
const handler = createConfigHandler({
ctx: { directory: "/tmp" },
pluginConfig,
modelCacheState: {
anthropicContext1MEnabled: false,
modelContextLimitsCache: new Map(),
},
})
//#when
await handler(config)
//#then - plan has model settings but NOT prompt/description/color
const agents = config.agent as Record<string, Record<string, unknown>>
expect(agents.plan.model).toBe("anthropic/claude-opus-4-6")
expect(agents.plan.prompt).toBeUndefined()
expect(agents.plan.description).toBeUndefined()
expect(agents.plan.color).toBeUndefined()
})
})
describe("Deadlock prevention - fetchAvailableModels must not receive client", () => {
test("fetchAvailableModels should be called with undefined client to prevent deadlock during plugin init", async () => {
// given - This test ensures we don't regress on issue #1301

View File

@@ -32,6 +32,7 @@ import { AGENT_NAME_MAP } from "../shared/migration";
import { AGENT_MODEL_REQUIREMENTS } from "../shared/model-requirements";
import { PROMETHEUS_SYSTEM_PROMPT, PROMETHEUS_PERMISSION } from "../agents/prometheus";
import { DEFAULT_CATEGORIES } from "../tools/delegate-task/constants";
import { buildPlanDemoteConfig } from "./plan-model-inheritance";
import type { ModelCacheState } from "../plugin-state";
import type { CategoryConfig } from "../config/schema";
@@ -385,8 +386,10 @@ export function createConfigHandler(deps: ConfigHandlerDeps) {
: {};
const planDemoteConfig = shouldDemotePlan
? { mode: "subagent" as const
}
? buildPlanDemoteConfig(
agentConfig["prometheus"] as Record<string, unknown> | undefined,
pluginConfig.agents?.plan as Record<string, unknown> | undefined,
)
: undefined;
config.agent = {

View File

@@ -0,0 +1,118 @@
import { describe, test, expect } from "bun:test"
import { buildPlanDemoteConfig } from "./plan-model-inheritance"
describe("buildPlanDemoteConfig", () => {
test("returns only mode when prometheus and plan override are both undefined", () => {
//#given
const prometheusConfig = undefined
const planOverride = undefined
//#when
const result = buildPlanDemoteConfig(prometheusConfig, planOverride)
//#then
expect(result).toEqual({ mode: "subagent" })
})
test("extracts all model settings from prometheus config", () => {
//#given
const prometheusConfig = {
name: "prometheus",
model: "anthropic/claude-opus-4-6",
variant: "max",
mode: "all",
prompt: "You are Prometheus...",
permission: { edit: "allow" },
description: "Plan agent (Prometheus)",
color: "#FF5722",
temperature: 0.1,
top_p: 0.95,
maxTokens: 32000,
thinking: { type: "enabled", budgetTokens: 10000 },
reasoningEffort: "high",
textVerbosity: "medium",
providerOptions: { key: "value" },
}
//#when
const result = buildPlanDemoteConfig(prometheusConfig, undefined)
//#then - picks model settings, NOT prompt/permission/description/color/name/mode
expect(result.mode).toBe("subagent")
expect(result.model).toBe("anthropic/claude-opus-4-6")
expect(result.variant).toBe("max")
expect(result.temperature).toBe(0.1)
expect(result.top_p).toBe(0.95)
expect(result.maxTokens).toBe(32000)
expect(result.thinking).toEqual({ type: "enabled", budgetTokens: 10000 })
expect(result.reasoningEffort).toBe("high")
expect(result.textVerbosity).toBe("medium")
expect(result.providerOptions).toEqual({ key: "value" })
expect(result.prompt).toBeUndefined()
expect(result.permission).toBeUndefined()
expect(result.description).toBeUndefined()
expect(result.color).toBeUndefined()
expect(result.name).toBeUndefined()
})
test("plan override takes priority over prometheus for all model settings", () => {
//#given
const prometheusConfig = {
model: "anthropic/claude-opus-4-6",
variant: "max",
temperature: 0.1,
reasoningEffort: "high",
}
const planOverride = {
model: "openai/gpt-5.2",
variant: "high",
temperature: 0.5,
reasoningEffort: "low",
}
//#when
const result = buildPlanDemoteConfig(prometheusConfig, planOverride)
//#then
expect(result.model).toBe("openai/gpt-5.2")
expect(result.variant).toBe("high")
expect(result.temperature).toBe(0.5)
expect(result.reasoningEffort).toBe("low")
})
test("falls back to prometheus when plan override has partial settings", () => {
//#given
const prometheusConfig = {
model: "anthropic/claude-opus-4-6",
variant: "max",
temperature: 0.1,
reasoningEffort: "high",
}
const planOverride = {
model: "openai/gpt-5.2",
}
//#when
const result = buildPlanDemoteConfig(prometheusConfig, planOverride)
//#then - plan model wins, rest inherits from prometheus
expect(result.model).toBe("openai/gpt-5.2")
expect(result.variant).toBe("max")
expect(result.temperature).toBe(0.1)
expect(result.reasoningEffort).toBe("high")
})
test("skips undefined values from both sources", () => {
//#given
const prometheusConfig = {
model: "anthropic/claude-opus-4-6",
}
//#when
const result = buildPlanDemoteConfig(prometheusConfig, undefined)
//#then
expect(result).toEqual({ mode: "subagent", model: "anthropic/claude-opus-4-6" })
expect(Object.keys(result)).toEqual(["mode", "model"])
})
})

View File

@@ -0,0 +1,27 @@
const MODEL_SETTINGS_KEYS = [
"model",
"variant",
"temperature",
"top_p",
"maxTokens",
"thinking",
"reasoningEffort",
"textVerbosity",
"providerOptions",
] as const
export function buildPlanDemoteConfig(
prometheusConfig: Record<string, unknown> | undefined,
planOverride: Record<string, unknown> | undefined,
): Record<string, unknown> {
const modelSettings: Record<string, unknown> = {}
for (const key of MODEL_SETTINGS_KEYS) {
const value = planOverride?.[key] ?? prometheusConfig?.[key]
if (value !== undefined) {
modelSettings[key] = value
}
}
return { mode: "subagent" as const, ...modelSettings }
}

View File

@@ -538,7 +538,7 @@ export function buildPlanAgentSystemPrepend(
* List of agent names that should be treated as plan agents.
* Case-insensitive matching is used.
*/
export const PLAN_AGENT_NAMES = ["plan", "prometheus", "planner"]
export const PLAN_AGENT_NAMES = ["plan", "planner"]
/**
* Check if the given agent name is a plan agent.

View File

@@ -12,7 +12,7 @@ import { resolveMultipleSkillsAsync } from "../../features/opencode-skill-loader
import { discoverSkills } from "../../features/opencode-skill-loader"
import { getTaskToastManager } from "../../features/task-toast-manager"
import { subagentSessions, getSessionAgent } from "../../features/claude-code-session-state"
import { log, getAgentToolRestrictions, resolveModelPipeline, promptWithModelSuggestionRetry } from "../../shared"
import { log, getAgentToolRestrictions, resolveModelPipeline, promptWithModelSuggestionRetry, promptSyncWithModelSuggestionRetry } from "../../shared"
import { fetchAvailableModels, isModelAvailable } from "../../shared/model-availability"
import { readConnectedProvidersCache } from "../../shared/connected-providers-cache"
import { CATEGORY_MODEL_REQUIREMENTS } from "../../shared/model-requirements"
@@ -211,7 +211,7 @@ export async function executeSyncContinuation(
: undefined
}
await (client.session as any).promptAsync({
await promptSyncWithModelSuggestionRetry(client, {
path: { id: args.session_id! },
body: {
...(resumeAgent !== undefined ? { agent: resumeAgent } : {}),
@@ -233,30 +233,6 @@ export async function executeSyncContinuation(
return `Failed to send continuation prompt: ${errorMessage}\n\nSession ID: ${args.session_id}`
}
const timing = getTimingConfig()
const pollStart = Date.now()
let lastMsgCount = 0
let stablePolls = 0
while (Date.now() - pollStart < 60000) {
await new Promise(resolve => setTimeout(resolve, timing.POLL_INTERVAL_MS))
const elapsed = Date.now() - pollStart
if (elapsed < timing.SESSION_CONTINUATION_STABILITY_MS) continue
const messagesCheck = await client.session.messages({ path: { id: args.session_id! } })
const msgs = ((messagesCheck as { data?: unknown }).data ?? messagesCheck) as Array<unknown>
const currentMsgCount = msgs.length
if (currentMsgCount > 0 && currentMsgCount === lastMsgCount) {
stablePolls++
if (stablePolls >= timing.STABILITY_POLLS_REQUIRED) break
} else {
stablePolls = 0
lastMsgCount = currentMsgCount
}
}
const messagesResult = await client.session.messages({
path: { id: args.session_id! },
})
@@ -621,7 +597,7 @@ export async function executeSyncTask(
try {
const allowTask = isPlanAgent(agentToUse)
await promptWithModelSuggestionRetry(client, {
await promptSyncWithModelSuggestionRetry(client, {
path: { id: sessionID },
body: {
agent: agentToUse,
@@ -659,70 +635,6 @@ export async function executeSyncTask(
})
}
const syncTiming = getTimingConfig()
const pollStart = Date.now()
let lastMsgCount = 0
let stablePolls = 0
let pollCount = 0
log("[task] Starting poll loop", { sessionID, agentToUse })
while (Date.now() - pollStart < syncTiming.MAX_POLL_TIME_MS) {
if (ctx.abort?.aborted) {
log("[task] Aborted by user", { sessionID })
if (toastManager && taskId) toastManager.removeTask(taskId)
return `Task aborted.\n\nSession ID: ${sessionID}`
}
await new Promise(resolve => setTimeout(resolve, syncTiming.POLL_INTERVAL_MS))
pollCount++
const statusResult = await client.session.status()
const allStatuses = (statusResult.data ?? {}) as Record<string, { type: string }>
const sessionStatus = allStatuses[sessionID]
if (pollCount % 10 === 0) {
log("[task] Poll status", {
sessionID,
pollCount,
elapsed: Math.floor((Date.now() - pollStart) / 1000) + "s",
sessionStatus: sessionStatus?.type ?? "not_in_status",
stablePolls,
lastMsgCount,
})
}
if (sessionStatus && sessionStatus.type !== "idle") {
stablePolls = 0
lastMsgCount = 0
continue
}
const elapsed = Date.now() - pollStart
if (elapsed < syncTiming.MIN_STABILITY_TIME_MS) {
continue
}
const messagesCheck = await client.session.messages({ path: { id: sessionID } })
const msgs = ((messagesCheck as { data?: unknown }).data ?? messagesCheck) as Array<unknown>
const currentMsgCount = msgs.length
if (currentMsgCount === lastMsgCount) {
stablePolls++
if (stablePolls >= syncTiming.STABILITY_POLLS_REQUIRED) {
log("[task] Poll complete - messages stable", { sessionID, pollCount, currentMsgCount })
break
}
} else {
stablePolls = 0
lastMsgCount = currentMsgCount
}
}
if (Date.now() - pollStart >= syncTiming.MAX_POLL_TIME_MS) {
log("[task] Poll timeout reached", { sessionID, pollCount, lastMsgCount, stablePolls })
}
const messagesResult = await client.session.messages({
path: { id: sessionID },
})
@@ -963,7 +875,7 @@ Sisyphus-Junior is spawned automatically when you specify a category. Pick the a
return {
agentToUse: "",
categoryModel: undefined,
error: `You are prometheus. You cannot delegate to prometheus via task.
error: `You are the plan agent. You cannot delegate to plan via task.
Create the work plan directly - that's your job as the planning agent.`,
}

View File

@@ -135,12 +135,12 @@ describe("sisyphus-task", () => {
expect(result).toBe(true)
})
test("returns true for 'prometheus'", () => {
// given / #when
test("returns false for 'prometheus' (decoupled from plan)", () => {
//#given / #when
const result = isPlanAgent("prometheus")
// then
expect(result).toBe(true)
//#then - prometheus is NOT a plan agent
expect(result).toBe(false)
})
test("returns true for 'planner'", () => {
@@ -159,12 +159,12 @@ describe("sisyphus-task", () => {
expect(result).toBe(true)
})
test("returns true for case-insensitive match 'Prometheus'", () => {
// given / #when
test("returns false for case-insensitive match 'Prometheus' (decoupled from plan)", () => {
//#given / #when
const result = isPlanAgent("Prometheus")
// then
expect(result).toBe(true)
//#then - Prometheus is NOT a plan agent
expect(result).toBe(false)
})
test("returns false for 'oracle'", () => {
@@ -199,11 +199,11 @@ describe("sisyphus-task", () => {
expect(result).toBe(false)
})
test("PLAN_AGENT_NAMES contains expected values", () => {
// given / #when / #then
test("PLAN_AGENT_NAMES contains only plan and planner (not prometheus)", () => {
//#given / #when / #then
expect(PLAN_AGENT_NAMES).toContain("plan")
expect(PLAN_AGENT_NAMES).toContain("prometheus")
expect(PLAN_AGENT_NAMES).toContain("planner")
expect(PLAN_AGENT_NAMES).not.toContain("prometheus")
})
})
@@ -2258,68 +2258,36 @@ describe("sisyphus-task", () => {
expect(result).toBe(buildPlanAgentSystemPrepend(availableCategories, availableSkills))
})
test("prepends plan agent system prompt when agentName is 'prometheus'", () => {
// given
test("does not prepend plan agent prompt for prometheus agent", () => {
//#given - prometheus is NOT a plan agent (decoupled)
const { buildSystemContent } = require("./tools")
const { buildPlanAgentSystemPrepend } = require("./constants")
const skillContent = "You are a strategic planner"
const availableCategories = [
{
name: "ultrabrain",
description: "Complex architecture, deep logical reasoning",
model: "openai/gpt-5.3-codex",
},
]
const availableSkills = [
{
name: "git-master",
description: "Atomic commits, git operations.",
location: "plugin",
},
]
// when
//#when
const result = buildSystemContent({
skillContent,
agentName: "prometheus",
availableCategories,
availableSkills,
})
// then
expect(result).toContain("<system>")
expect(result).toBe(buildPlanAgentSystemPrepend(availableCategories, availableSkills))
//#then - prometheus should NOT get plan agent system prepend
expect(result).toBe(skillContent)
expect(result).not.toContain("MANDATORY CONTEXT GATHERING PROTOCOL")
})
test("prepends plan agent system prompt when agentName is 'Prometheus' (case insensitive)", () => {
// given
test("does not prepend plan agent prompt for Prometheus (case insensitive)", () => {
//#given - Prometheus (capitalized) is NOT a plan agent
const { buildSystemContent } = require("./tools")
const { buildPlanAgentSystemPrepend } = require("./constants")
const skillContent = "You are a strategic planner"
const availableCategories = [
{
name: "quick",
description: "Trivial tasks",
model: "anthropic/claude-haiku-4-5",
},
]
const availableSkills = [
{
name: "dev-browser",
description: "Persistent browser state automation.",
location: "plugin",
},
]
// when
//#when
const result = buildSystemContent({
skillContent,
agentName: "Prometheus",
availableCategories,
availableSkills,
})
// then
expect(result).toContain("<system>")
expect(result).toBe(buildPlanAgentSystemPrepend(availableCategories, availableSkills))
//#then
expect(result).toBe(skillContent)
expect(result).not.toContain("MANDATORY CONTEXT GATHERING PROTOCOL")
})
test("combines plan agent prepend with skill content", () => {
@@ -2565,14 +2533,14 @@ describe("sisyphus-task", () => {
})
})
describe("prometheus self-delegation block", () => {
test("prometheus cannot delegate to prometheus - returns error with guidance", async () => {
// given - current agent is prometheus
describe("plan agent self-delegation block", () => {
test("plan agent cannot delegate to plan - returns error with guidance", async () => {
//#given - current agent is plan
const { createDelegateTask } = require("./tools")
const mockManager = { launch: async () => ({}) }
const mockClient = {
app: { agents: async () => ({ data: [{ name: "prometheus", mode: "subagent" }] }) },
app: { agents: async () => ({ data: [{ name: "plan", mode: "subagent" }] }) },
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
session: {
get: async () => ({ data: { directory: "/project" } }),
@@ -2592,44 +2560,44 @@ describe("sisyphus-task", () => {
const toolContext = {
sessionID: "parent-session",
messageID: "parent-message",
agent: "prometheus",
agent: "plan",
abort: new AbortController().signal,
}
// when - prometheus tries to delegate to prometheus
//#when - plan agent tries to delegate to plan
const result = await tool.execute(
{
description: "Test self-delegation block",
prompt: "Create a plan",
subagent_type: "prometheus",
subagent_type: "plan",
run_in_background: false,
load_skills: [],
},
toolContext
)
// then - should return error telling prometheus to create plan directly
expect(result).toContain("prometheus")
//#then - should return error telling plan agent to create plan directly
expect(result).toContain("plan agent")
expect(result).toContain("directly")
})
test("non-prometheus agent CAN delegate to prometheus - proceeds normally", async () => {
// given - current agent is sisyphus
test("prometheus is NOT a plan agent - can delegate to plan normally", async () => {
//#given - current agent is prometheus (no longer treated as plan agent)
const { createDelegateTask } = require("./tools")
const mockManager = { launch: async () => ({}) }
const mockClient = {
app: { agents: async () => ({ data: [{ name: "prometheus", mode: "subagent" }] }) },
app: { agents: async () => ({ data: [{ name: "plan", mode: "subagent" }] }) },
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
session: {
get: async () => ({ data: { directory: "/project" } }),
create: async () => ({ data: { id: "ses_prometheus_allowed" } }),
create: async () => ({ data: { id: "ses_plan_from_prometheus" } }),
prompt: async () => ({ data: {} }),
promptAsync: async () => ({ data: {} }),
messages: async () => ({
data: [{ info: { role: "assistant" }, parts: [{ type: "text", text: "Plan created successfully" }] }]
}),
status: async () => ({ data: { "ses_prometheus_allowed": { type: "idle" } } }),
status: async () => ({ data: { "ses_plan_from_prometheus": { type: "idle" } } }),
},
}
@@ -2641,34 +2609,34 @@ describe("sisyphus-task", () => {
const toolContext = {
sessionID: "parent-session",
messageID: "parent-message",
agent: "sisyphus",
agent: "prometheus",
abort: new AbortController().signal,
}
// when - sisyphus delegates to prometheus
//#when - prometheus delegates to plan (should work now)
const result = await tool.execute(
{
description: "Test prometheus delegation from non-prometheus agent",
description: "Test plan delegation from prometheus",
prompt: "Create a plan",
subagent_type: "prometheus",
subagent_type: "plan",
run_in_background: false,
load_skills: [],
},
toolContext
)
// then - should proceed normally
//#then - should proceed normally (prometheus is not plan agent)
expect(result).not.toContain("Cannot delegate")
expect(result).toContain("Plan created successfully")
}, { timeout: 20000 })
test("case-insensitive: Prometheus (capitalized) cannot delegate to prometheus", async () => {
// given - current agent is Prometheus (capitalized)
test("planner agent self-delegation is also blocked", async () => {
//#given - current agent is planner
const { createDelegateTask } = require("./tools")
const mockManager = { launch: async () => ({}) }
const mockClient = {
app: { agents: async () => ({ data: [{ name: "prometheus", mode: "subagent" }] }) },
app: { agents: async () => ({ data: [{ name: "planner", mode: "subagent" }] }) },
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
session: {
get: async () => ({ data: { directory: "/project" } }),
@@ -2688,24 +2656,24 @@ describe("sisyphus-task", () => {
const toolContext = {
sessionID: "parent-session",
messageID: "parent-message",
agent: "Prometheus",
agent: "planner",
abort: new AbortController().signal,
}
// when - Prometheus tries to delegate to prometheus
//#when - planner tries to delegate to plan
const result = await tool.execute(
{
description: "Test case-insensitive block",
description: "Test planner self-delegation block",
prompt: "Create a plan",
subagent_type: "prometheus",
subagent_type: "plan",
run_in_background: false,
load_skills: [],
},
toolContext
)
// then - should still return error
expect(result).toContain("prometheus")
//#then - should return error (planner is a plan agent alias)
expect(result).toContain("plan agent")
expect(result).toContain("directly")
})
})
@@ -2903,9 +2871,9 @@ describe("sisyphus-task", () => {
}, { timeout: 20000 })
})
describe("prometheus subagent task permission", () => {
test("prometheus subagent should have task permission enabled", async () => {
// given - sisyphus delegates to prometheus
describe("subagent task permission", () => {
test("plan subagent should have task permission enabled", async () => {
//#given - sisyphus delegates to plan agent
const { createDelegateTask } = require("./tools")
let promptBody: any
@@ -2917,17 +2885,17 @@ describe("sisyphus-task", () => {
}
const mockClient = {
app: { agents: async () => ({ data: [{ name: "prometheus", mode: "subagent" }] }) },
app: { agents: async () => ({ data: [{ name: "plan", mode: "subagent" }] }) },
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
session: {
get: async () => ({ data: { directory: "/project" } }),
create: async () => ({ data: { id: "ses_prometheus_delegate" } }),
create: async () => ({ data: { id: "ses_plan_delegate" } }),
prompt: promptMock,
promptAsync: promptMock,
messages: async () => ({
data: [{ info: { role: "assistant" }, parts: [{ type: "text", text: "Plan created" }] }]
}),
status: async () => ({ data: { "ses_prometheus_delegate": { type: "idle" } } }),
status: async () => ({ data: { "ses_plan_delegate": { type: "idle" } } }),
},
}
@@ -2943,10 +2911,65 @@ describe("sisyphus-task", () => {
abort: new AbortController().signal,
}
// when - sisyphus delegates to prometheus
//#when - sisyphus delegates to plan
await tool.execute(
{
description: "Test prometheus task permission",
description: "Test plan task permission",
prompt: "Create a plan",
subagent_type: "plan",
run_in_background: false,
load_skills: [],
},
toolContext
)
//#then - plan agent should have task permission
expect(promptBody.tools.task).toBe(true)
}, { timeout: 20000 })
test("prometheus subagent should NOT have task permission (decoupled from plan)", async () => {
//#given - sisyphus delegates to prometheus (no longer a plan agent)
const { createDelegateTask } = require("./tools")
let promptBody: any
const mockManager = { launch: async () => ({}) }
const promptMock = async (input: any) => {
promptBody = input.body
return { data: {} }
}
const mockClient = {
app: { agents: async () => ({ data: [{ name: "prometheus", mode: "subagent" }] }) },
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
session: {
get: async () => ({ data: { directory: "/project" } }),
create: async () => ({ data: { id: "ses_prometheus_no_task" } }),
prompt: promptMock,
promptAsync: promptMock,
messages: async () => ({
data: [{ info: { role: "assistant" }, parts: [{ type: "text", text: "Plan created" }] }]
}),
status: async () => ({ data: { "ses_prometheus_no_task": { type: "idle" } } }),
},
}
const tool = createDelegateTask({
manager: mockManager,
client: mockClient,
})
const toolContext = {
sessionID: "parent-session",
messageID: "parent-message",
agent: "sisyphus",
abort: new AbortController().signal,
}
//#when - sisyphus delegates to prometheus
await tool.execute(
{
description: "Test prometheus no task permission",
prompt: "Create a plan",
subagent_type: "prometheus",
run_in_background: false,
@@ -2955,12 +2978,12 @@ describe("sisyphus-task", () => {
toolContext
)
// then - prometheus should have task permission
expect(promptBody.tools.task).toBe(true)
//#then - prometheus should NOT have task permission (it's not a plan agent)
expect(promptBody.tools.task).toBe(false)
}, { timeout: 20000 })
test("non-prometheus subagent should NOT have task permission", async () => {
// given - sisyphus delegates to oracle (non-prometheus)
test("non-plan subagent should NOT have task permission", async () => {
//#given - sisyphus delegates to oracle (non-plan)
const { createDelegateTask } = require("./tools")
let promptBody: any