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:
@@ -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
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
118
src/plugin-handlers/plan-model-inheritance.test.ts
Normal file
118
src/plugin-handlers/plan-model-inheritance.test.ts
Normal 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"])
|
||||
})
|
||||
})
|
||||
27
src/plugin-handlers/plan-model-inheritance.ts
Normal file
27
src/plugin-handlers/plan-model-inheritance.ts
Normal 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 }
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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.`,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user