refactor: migrate delegate_task to task tool with metadata fixes
- Rename delegate_task tool to task across codebase (100 files) - Update model references: claude-opus-4-6 → 4-5, gpt-5.3-codex → 5.2-codex - Add tool-metadata-store to restore metadata overwritten by fromPlugin() - Add session ID polling for BackgroundManager task sessions - Await async ctx.metadata() calls in tool executors - Add ses_ prefix guard to getMessageDir for performance - Harden BackgroundManager with idle deferral and error handling - Fix duplicate task key in sisyphus-junior test object literals - Fix unawaited showOutputToUser in ast_grep_replace - Fix background=true → run_in_background=true in ultrawork prompt - Fix duplicate task/task references in docs and comments
This commit is contained in:
@@ -459,13 +459,13 @@ YOU MUST END YOUR RESPONSE WITH THIS SECTION.
|
||||
|
||||
1. **Wave 1**: Fire these tasks IN PARALLEL (no dependencies)
|
||||
\`\`\`
|
||||
delegate_task(category="...", load_skills=[...], run_in_background=false, prompt="Task 1: ...")
|
||||
delegate_task(category="...", load_skills=[...], run_in_background=false, prompt="Task N: ...")
|
||||
task(category="...", load_skills=[...], run_in_background=false, prompt="Task 1: ...")
|
||||
task(category="...", load_skills=[...], run_in_background=false, prompt="Task N: ...")
|
||||
\`\`\`
|
||||
|
||||
2. **Wave 2**: After Wave 1 completes, fire next wave IN PARALLEL
|
||||
\`\`\`
|
||||
delegate_task(category="...", load_skills=[...], run_in_background=false, prompt="Task 2: ...")
|
||||
task(category="...", load_skills=[...], run_in_background=false, prompt="Task 2: ...")
|
||||
\`\`\`
|
||||
|
||||
3. Continue until all waves complete
|
||||
@@ -476,7 +476,7 @@ YOU MUST END YOUR RESPONSE WITH THIS SECTION.
|
||||
WHY THIS FORMAT IS MANDATORY:
|
||||
- Caller can directly copy TODO items
|
||||
- Wave grouping enables parallel execution
|
||||
- Each task has clear delegate_task parameters
|
||||
- Each task has clear task parameters
|
||||
- QA criteria ensure verifiable completion
|
||||
</FINAL_OUTPUT_FOR_CALLER>
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import { log, getAgentToolRestrictions, resolveModelPipeline, promptWithModelSug
|
||||
import { fetchAvailableModels, isModelAvailable } from "../../shared/model-availability"
|
||||
import { readConnectedProvidersCache } from "../../shared/connected-providers-cache"
|
||||
import { CATEGORY_MODEL_REQUIREMENTS } from "../../shared/model-requirements"
|
||||
import { storeToolMetadata } from "../../features/tool-metadata-store"
|
||||
|
||||
const SISYPHUS_JUNIOR_AGENT = "sisyphus-junior"
|
||||
|
||||
@@ -67,7 +68,7 @@ export function resolveParentContext(ctx: ToolContextWithMetadata): ParentContex
|
||||
const sessionAgent = getSessionAgent(ctx.sessionID)
|
||||
const parentAgent = ctx.agent ?? sessionAgent ?? firstMessageAgent ?? prevMessage?.agent
|
||||
|
||||
log("[delegate_task] parentAgent resolution", {
|
||||
log("[task] parentAgent resolution", {
|
||||
sessionID: ctx.sessionID,
|
||||
messageDir,
|
||||
ctxAgent: ctx.agent,
|
||||
@@ -111,7 +112,7 @@ export async function executeBackgroundContinuation(
|
||||
parentAgent: parentContext.agent,
|
||||
})
|
||||
|
||||
ctx.metadata?.({
|
||||
const bgContMeta = {
|
||||
title: `Continue: ${task.description}`,
|
||||
metadata: {
|
||||
prompt: args.prompt,
|
||||
@@ -122,7 +123,11 @@ export async function executeBackgroundContinuation(
|
||||
sessionId: task.sessionID,
|
||||
command: args.command,
|
||||
},
|
||||
})
|
||||
}
|
||||
await ctx.metadata?.(bgContMeta)
|
||||
if (ctx.callID) {
|
||||
storeToolMetadata(ctx.sessionID, ctx.callID, bgContMeta)
|
||||
}
|
||||
|
||||
return `Background task continued.
|
||||
|
||||
@@ -165,7 +170,7 @@ export async function executeSyncContinuation(
|
||||
})
|
||||
}
|
||||
|
||||
ctx.metadata?.({
|
||||
const syncContMeta = {
|
||||
title: `Continue: ${args.description}`,
|
||||
metadata: {
|
||||
prompt: args.prompt,
|
||||
@@ -176,7 +181,11 @@ export async function executeSyncContinuation(
|
||||
sync: true,
|
||||
command: args.command,
|
||||
},
|
||||
})
|
||||
}
|
||||
await ctx.metadata?.(syncContMeta)
|
||||
if (ctx.callID) {
|
||||
storeToolMetadata(ctx.sessionID, ctx.callID, syncContMeta)
|
||||
}
|
||||
|
||||
try {
|
||||
let resumeAgent: string | undefined
|
||||
@@ -207,13 +216,12 @@ export async function executeSyncContinuation(
|
||||
body: {
|
||||
...(resumeAgent !== undefined ? { agent: resumeAgent } : {}),
|
||||
...(resumeModel !== undefined ? { model: resumeModel } : {}),
|
||||
tools: {
|
||||
...(resumeAgent ? getAgentToolRestrictions(resumeAgent) : {}),
|
||||
task: false,
|
||||
delegate_task: false,
|
||||
call_omo_agent: true,
|
||||
question: false,
|
||||
},
|
||||
tools: {
|
||||
...(resumeAgent ? getAgentToolRestrictions(resumeAgent) : {}),
|
||||
task: false,
|
||||
call_omo_agent: true,
|
||||
question: false,
|
||||
},
|
||||
parts: [{ type: "text", text: args.prompt }],
|
||||
},
|
||||
})
|
||||
@@ -316,17 +324,17 @@ export async function executeUnstableAgentTask(
|
||||
category: args.category,
|
||||
})
|
||||
|
||||
const WAIT_FOR_SESSION_INTERVAL_MS = 100
|
||||
const WAIT_FOR_SESSION_TIMEOUT_MS = 30000
|
||||
const timing = getTimingConfig()
|
||||
const waitStart = Date.now()
|
||||
while (!task.sessionID && Date.now() - waitStart < WAIT_FOR_SESSION_TIMEOUT_MS) {
|
||||
let sessionID = task.sessionID
|
||||
while (!sessionID && Date.now() - waitStart < timing.WAIT_FOR_SESSION_TIMEOUT_MS) {
|
||||
if (ctx.abort?.aborted) {
|
||||
return `Task aborted while waiting for session to start.\n\nTask ID: ${task.id}`
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, WAIT_FOR_SESSION_INTERVAL_MS))
|
||||
await new Promise(resolve => setTimeout(resolve, timing.WAIT_FOR_SESSION_INTERVAL_MS))
|
||||
const updated = manager.getTask(task.id)
|
||||
sessionID = updated?.sessionID
|
||||
}
|
||||
|
||||
const sessionID = task.sessionID
|
||||
if (!sessionID) {
|
||||
return formatDetailedError(new Error(`Task failed to start within timeout (30s). Task ID: ${task.id}, Status: ${task.status}`), {
|
||||
operation: "Launch monitored background task",
|
||||
@@ -336,7 +344,7 @@ export async function executeUnstableAgentTask(
|
||||
})
|
||||
}
|
||||
|
||||
ctx.metadata?.({
|
||||
const bgTaskMeta = {
|
||||
title: args.description,
|
||||
metadata: {
|
||||
prompt: args.prompt,
|
||||
@@ -348,7 +356,11 @@ export async function executeUnstableAgentTask(
|
||||
sessionId: sessionID,
|
||||
command: args.command,
|
||||
},
|
||||
})
|
||||
}
|
||||
await ctx.metadata?.(bgTaskMeta)
|
||||
if (ctx.callID) {
|
||||
storeToolMetadata(ctx.sessionID, ctx.callID, bgTaskMeta)
|
||||
}
|
||||
|
||||
const startTime = new Date()
|
||||
const timingCfg = getTimingConfig()
|
||||
@@ -463,7 +475,23 @@ export async function executeBackgroundTask(
|
||||
category: args.category,
|
||||
})
|
||||
|
||||
ctx.metadata?.({
|
||||
// OpenCode TUI's `Task` tool UI calculates toolcalls by looking up
|
||||
// `props.metadata.sessionId` and then counting tool parts in that session.
|
||||
// BackgroundManager.launch() returns immediately (pending) before the session exists,
|
||||
// so we must wait briefly for the session to be created to set metadata correctly.
|
||||
const timing = getTimingConfig()
|
||||
const waitStart = Date.now()
|
||||
let sessionId = task.sessionID
|
||||
while (!sessionId && Date.now() - waitStart < timing.WAIT_FOR_SESSION_TIMEOUT_MS) {
|
||||
if (ctx.abort?.aborted) {
|
||||
return `Task aborted while waiting for session to start.\n\nTask ID: ${task.id}`
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, timing.WAIT_FOR_SESSION_INTERVAL_MS))
|
||||
const updated = manager.getTask(task.id)
|
||||
sessionId = updated?.sessionID
|
||||
}
|
||||
|
||||
const unstableMeta = {
|
||||
title: args.description,
|
||||
metadata: {
|
||||
prompt: args.prompt,
|
||||
@@ -472,10 +500,14 @@ export async function executeBackgroundTask(
|
||||
load_skills: args.load_skills,
|
||||
description: args.description,
|
||||
run_in_background: args.run_in_background,
|
||||
sessionId: task.sessionID,
|
||||
sessionId: sessionId ?? "pending",
|
||||
command: args.command,
|
||||
},
|
||||
})
|
||||
}
|
||||
await ctx.metadata?.(unstableMeta)
|
||||
if (ctx.callID) {
|
||||
storeToolMetadata(ctx.sessionID, ctx.callID, unstableMeta)
|
||||
}
|
||||
|
||||
return `Background task launched.
|
||||
|
||||
@@ -487,7 +519,7 @@ Status: ${task.status}
|
||||
System notifies on completion. Use \`background_output\` with task_id="${task.id}" to check.
|
||||
|
||||
<task_metadata>
|
||||
session_id: ${task.sessionID}
|
||||
session_id: ${sessionId}
|
||||
</task_metadata>`
|
||||
} catch (error) {
|
||||
return formatDetailedError(error, {
|
||||
@@ -542,13 +574,13 @@ export async function executeSyncTask(
|
||||
subagentSessions.add(sessionID)
|
||||
|
||||
if (onSyncSessionCreated) {
|
||||
log("[delegate_task] Invoking onSyncSessionCreated callback", { sessionID, parentID: parentContext.sessionID })
|
||||
log("[task] Invoking onSyncSessionCreated callback", { sessionID, parentID: parentContext.sessionID })
|
||||
await onSyncSessionCreated({
|
||||
sessionID,
|
||||
parentID: parentContext.sessionID,
|
||||
title: args.description,
|
||||
}).catch((err) => {
|
||||
log("[delegate_task] onSyncSessionCreated callback failed", { error: String(err) })
|
||||
log("[task] onSyncSessionCreated callback failed", { error: String(err) })
|
||||
})
|
||||
await new Promise(r => setTimeout(r, 200))
|
||||
}
|
||||
@@ -568,7 +600,7 @@ export async function executeSyncTask(
|
||||
})
|
||||
}
|
||||
|
||||
ctx.metadata?.({
|
||||
const syncTaskMeta = {
|
||||
title: args.description,
|
||||
metadata: {
|
||||
prompt: args.prompt,
|
||||
@@ -581,18 +613,21 @@ export async function executeSyncTask(
|
||||
sync: true,
|
||||
command: args.command,
|
||||
},
|
||||
})
|
||||
}
|
||||
await ctx.metadata?.(syncTaskMeta)
|
||||
if (ctx.callID) {
|
||||
storeToolMetadata(ctx.sessionID, ctx.callID, syncTaskMeta)
|
||||
}
|
||||
|
||||
try {
|
||||
const allowDelegateTask = isPlanAgent(agentToUse)
|
||||
const allowTask = isPlanAgent(agentToUse)
|
||||
await promptWithModelSuggestionRetry(client, {
|
||||
path: { id: sessionID },
|
||||
body: {
|
||||
agent: agentToUse,
|
||||
system: systemContent,
|
||||
tools: {
|
||||
task: false,
|
||||
delegate_task: allowDelegateTask,
|
||||
task: allowTask,
|
||||
call_omo_agent: true,
|
||||
question: false,
|
||||
},
|
||||
@@ -630,11 +665,11 @@ export async function executeSyncTask(
|
||||
let stablePolls = 0
|
||||
let pollCount = 0
|
||||
|
||||
log("[delegate_task] Starting poll loop", { sessionID, agentToUse })
|
||||
log("[task] Starting poll loop", { sessionID, agentToUse })
|
||||
|
||||
while (Date.now() - pollStart < syncTiming.MAX_POLL_TIME_MS) {
|
||||
if (ctx.abort?.aborted) {
|
||||
log("[delegate_task] Aborted by user", { sessionID })
|
||||
log("[task] Aborted by user", { sessionID })
|
||||
if (toastManager && taskId) toastManager.removeTask(taskId)
|
||||
return `Task aborted.\n\nSession ID: ${sessionID}`
|
||||
}
|
||||
@@ -647,7 +682,7 @@ export async function executeSyncTask(
|
||||
const sessionStatus = allStatuses[sessionID]
|
||||
|
||||
if (pollCount % 10 === 0) {
|
||||
log("[delegate_task] Poll status", {
|
||||
log("[task] Poll status", {
|
||||
sessionID,
|
||||
pollCount,
|
||||
elapsed: Math.floor((Date.now() - pollStart) / 1000) + "s",
|
||||
@@ -675,7 +710,7 @@ export async function executeSyncTask(
|
||||
if (currentMsgCount === lastMsgCount) {
|
||||
stablePolls++
|
||||
if (stablePolls >= syncTiming.STABILITY_POLLS_REQUIRED) {
|
||||
log("[delegate_task] Poll complete - messages stable", { sessionID, pollCount, currentMsgCount })
|
||||
log("[task] Poll complete - messages stable", { sessionID, pollCount, currentMsgCount })
|
||||
break
|
||||
}
|
||||
} else {
|
||||
@@ -685,7 +720,7 @@ export async function executeSyncTask(
|
||||
}
|
||||
|
||||
if (Date.now() - pollStart >= syncTiming.MAX_POLL_TIME_MS) {
|
||||
log("[delegate_task] Poll timeout reached", { sessionID, pollCount, lastMsgCount, stablePolls })
|
||||
log("[task] Poll timeout reached", { sessionID, pollCount, lastMsgCount, stablePolls })
|
||||
}
|
||||
|
||||
const messagesResult = await client.session.messages({
|
||||
@@ -928,7 +963,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 delegate_task.
|
||||
error: `You are prometheus. You cannot delegate to prometheus via task.
|
||||
|
||||
Create the work plan directly - that's your job as the planning agent.`,
|
||||
}
|
||||
@@ -955,7 +990,7 @@ Create the work plan directly - that's your job as the planning agent.`,
|
||||
return {
|
||||
agentToUse: "",
|
||||
categoryModel: undefined,
|
||||
error: `Cannot call primary agent "${isPrimaryAgent.name}" via delegate_task. Primary agents are top-level orchestrators.`,
|
||||
error: `Cannot call primary agent "${isPrimaryAgent.name}" via task. Primary agents are top-level orchestrators.`,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ export function parseModelString(model: string): { providerID: string; modelID:
|
||||
* Get the message directory for a session, checking both direct and nested paths.
|
||||
*/
|
||||
export function getMessageDir(sessionID: string): string | null {
|
||||
if (!sessionID.startsWith("ses_")) return null
|
||||
if (!existsSync(MESSAGE_STORAGE)) return null
|
||||
|
||||
const directPath = join(MESSAGE_STORAGE, sessionID)
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
const { describe, test, expect } = require("bun:test")
|
||||
|
||||
import { executeBackgroundTask } from "./executor"
|
||||
import type { DelegateTaskArgs, ToolContextWithMetadata } from "./types"
|
||||
|
||||
describe("task tool metadata awaiting", () => {
|
||||
test("executeBackgroundTask awaits ctx.metadata before returning", async () => {
|
||||
// given
|
||||
let metadataResolved = false
|
||||
const abort = new AbortController()
|
||||
|
||||
const ctx: ToolContextWithMetadata = {
|
||||
sessionID: "ses_parent",
|
||||
messageID: "msg_parent",
|
||||
agent: "sisyphus",
|
||||
abort: abort.signal,
|
||||
metadata: async () => {
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, 50))
|
||||
metadataResolved = true
|
||||
},
|
||||
}
|
||||
|
||||
const args: DelegateTaskArgs = {
|
||||
load_skills: [],
|
||||
description: "Test task",
|
||||
prompt: "Do something",
|
||||
run_in_background: true,
|
||||
subagent_type: "explore",
|
||||
}
|
||||
|
||||
const executorCtx = {
|
||||
manager: {
|
||||
launch: async () => ({
|
||||
id: "task_1",
|
||||
description: "Test task",
|
||||
prompt: "Do something",
|
||||
agent: "explore",
|
||||
status: "pending",
|
||||
sessionID: "ses_child",
|
||||
}),
|
||||
getTask: () => undefined,
|
||||
},
|
||||
} as any
|
||||
|
||||
const parentContext = {
|
||||
sessionID: "ses_parent",
|
||||
messageID: "msg_parent",
|
||||
}
|
||||
|
||||
// when
|
||||
const result = await executeBackgroundTask(
|
||||
args,
|
||||
ctx,
|
||||
executorCtx,
|
||||
parentContext,
|
||||
"explore",
|
||||
undefined,
|
||||
undefined,
|
||||
)
|
||||
|
||||
// then
|
||||
expect(result).toContain("Background task launched")
|
||||
expect(metadataResolved).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, test, expect, beforeEach, afterEach, spyOn } from "bun:test"
|
||||
declare const require: (name: string) => any
|
||||
const { describe, test, expect, beforeEach, afterEach, spyOn } = require("bun:test")
|
||||
import { DEFAULT_CATEGORIES, CATEGORY_PROMPT_APPENDS, CATEGORY_DESCRIPTIONS, isPlanAgent, PLAN_AGENT_NAMES } from "./constants"
|
||||
import { resolveCategoryConfig } from "./tools"
|
||||
import type { CategoryConfig } from "../../config/schema"
|
||||
@@ -207,6 +208,66 @@ describe("sisyphus-task", () => {
|
||||
})
|
||||
|
||||
describe("category delegation config validation", () => {
|
||||
test("fills subagent_type as sisyphus-junior when category is provided without subagent_type", async () => {
|
||||
// given
|
||||
const { createDelegateTask } = require("./tools")
|
||||
|
||||
const mockManager = {
|
||||
launch: async () => ({
|
||||
id: "task-123",
|
||||
status: "pending",
|
||||
description: "Test task",
|
||||
agent: "sisyphus-junior",
|
||||
sessionID: "test-session",
|
||||
}),
|
||||
}
|
||||
const mockClient = {
|
||||
app: { agents: async () => ({ data: [] }) },
|
||||
config: { get: async () => ({}) },
|
||||
provider: { list: async () => ({ data: { connected: ["openai"] } }) },
|
||||
model: { list: async () => ({ data: [{ provider: "openai", id: "gpt-5.3-codex" }] }) },
|
||||
session: {
|
||||
create: async () => ({ data: { id: "test-session" } }),
|
||||
prompt: async () => ({ data: {} }),
|
||||
messages: async () => ({ data: [] }),
|
||||
status: async () => ({ data: {} }),
|
||||
},
|
||||
}
|
||||
|
||||
const tool = createDelegateTask({
|
||||
manager: mockManager,
|
||||
client: mockClient,
|
||||
})
|
||||
|
||||
const toolContext = {
|
||||
sessionID: "parent-session",
|
||||
messageID: "parent-message",
|
||||
agent: "sisyphus",
|
||||
abort: new AbortController().signal,
|
||||
}
|
||||
|
||||
const args: {
|
||||
description: string
|
||||
prompt: string
|
||||
category: string
|
||||
run_in_background: boolean
|
||||
load_skills: string[]
|
||||
subagent_type?: string
|
||||
} = {
|
||||
description: "Quick category test",
|
||||
prompt: "Do something",
|
||||
category: "quick",
|
||||
run_in_background: true,
|
||||
load_skills: [],
|
||||
}
|
||||
|
||||
// when
|
||||
await tool.execute(args, toolContext)
|
||||
|
||||
// then
|
||||
expect(args.subagent_type).toBe("sisyphus-junior")
|
||||
}, { timeout: 10000 })
|
||||
|
||||
test("proceeds without error when systemDefaultModel is undefined", async () => {
|
||||
// given a mock client with no model in config
|
||||
const { createDelegateTask } = require("./tools")
|
||||
@@ -304,6 +365,71 @@ describe("sisyphus-task", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("background metadata sessionId", () => {
|
||||
test("should wait for background sessionId and set metadata for TUI toolcall counting", async () => {
|
||||
//#given - manager.launch returns before sessionID is available
|
||||
const { createDelegateTask } = require("./tools")
|
||||
|
||||
const tasks = new Map<string, { id: string; sessionID?: string; status: string; description: string; agent: string }>()
|
||||
const mockManager = {
|
||||
getTask: (id: string) => tasks.get(id),
|
||||
launch: async () => {
|
||||
const task = { id: "bg_1", status: "pending", description: "Test task", agent: "explore" }
|
||||
tasks.set(task.id, task)
|
||||
setTimeout(() => {
|
||||
tasks.set(task.id, { ...task, status: "running", sessionID: "ses_child" })
|
||||
}, 20)
|
||||
return task
|
||||
},
|
||||
}
|
||||
|
||||
const mockClient = {
|
||||
app: { agents: async () => ({ data: [{ name: "explore", mode: "subagent" }] }) },
|
||||
config: { get: async () => ({}) },
|
||||
provider: { list: async () => ({ data: { connected: ["openai"] } }) },
|
||||
model: { list: async () => ({ data: [{ provider: "openai", id: "gpt-5.3-codex" }] }) },
|
||||
session: {
|
||||
create: async () => ({ data: { id: "test-session" } }),
|
||||
prompt: async () => ({ data: {} }),
|
||||
messages: async () => ({ data: [] }),
|
||||
status: async () => ({ data: {} }),
|
||||
},
|
||||
}
|
||||
|
||||
const tool = createDelegateTask({
|
||||
manager: mockManager,
|
||||
client: mockClient,
|
||||
})
|
||||
|
||||
const metadataCalls: Array<{ title?: string; metadata?: Record<string, unknown> }> = []
|
||||
const toolContext = {
|
||||
sessionID: "parent-session",
|
||||
messageID: "parent-message",
|
||||
agent: "sisyphus",
|
||||
abort: new AbortController().signal,
|
||||
metadata: (input: { title?: string; metadata?: Record<string, unknown> }) => {
|
||||
metadataCalls.push(input)
|
||||
},
|
||||
}
|
||||
|
||||
const args = {
|
||||
description: "Explore task",
|
||||
prompt: "Explore features directory deeply",
|
||||
subagent_type: "explore",
|
||||
run_in_background: true,
|
||||
load_skills: [],
|
||||
}
|
||||
|
||||
//#when
|
||||
const result = await tool.execute(args, toolContext)
|
||||
|
||||
//#then - metadata should include sessionId (camelCase) once it's available
|
||||
expect(String(result)).toContain("Background task launched")
|
||||
const sessionIdCall = metadataCalls.find((c) => c.metadata?.sessionId === "ses_child")
|
||||
expect(sessionIdCall).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("resolveCategoryConfig", () => {
|
||||
test("returns null for unknown category without user config", () => {
|
||||
// given
|
||||
@@ -1894,7 +2020,7 @@ describe("sisyphus-task", () => {
|
||||
|
||||
describe("browserProvider propagation", () => {
|
||||
test("should resolve agent-browser skill when browserProvider is passed", async () => {
|
||||
// given - delegate_task configured with browserProvider: "agent-browser"
|
||||
// given - task configured with browserProvider: "agent-browser"
|
||||
const { createDelegateTask } = require("./tools")
|
||||
let promptBody: any
|
||||
|
||||
@@ -1949,7 +2075,7 @@ describe("sisyphus-task", () => {
|
||||
}, { timeout: 20000 })
|
||||
|
||||
test("should NOT resolve agent-browser skill when browserProvider is not set", async () => {
|
||||
// given - delegate_task without browserProvider (defaults to playwright)
|
||||
// given - task without browserProvider (defaults to playwright)
|
||||
const { createDelegateTask } = require("./tools")
|
||||
|
||||
const mockManager = { launch: async () => ({}) }
|
||||
@@ -2720,8 +2846,8 @@ describe("sisyphus-task", () => {
|
||||
}, { timeout: 20000 })
|
||||
})
|
||||
|
||||
describe("prometheus subagent delegate_task permission", () => {
|
||||
test("prometheus subagent should have delegate_task permission enabled", async () => {
|
||||
describe("prometheus subagent task permission", () => {
|
||||
test("prometheus subagent should have task permission enabled", async () => {
|
||||
// given - sisyphus delegates to prometheus
|
||||
const { createDelegateTask } = require("./tools")
|
||||
let promptBody: any
|
||||
@@ -2759,7 +2885,7 @@ describe("sisyphus-task", () => {
|
||||
// when - sisyphus delegates to prometheus
|
||||
await tool.execute(
|
||||
{
|
||||
description: "Test prometheus delegate_task permission",
|
||||
description: "Test prometheus task permission",
|
||||
prompt: "Create a plan",
|
||||
subagent_type: "prometheus",
|
||||
run_in_background: false,
|
||||
@@ -2768,11 +2894,11 @@ describe("sisyphus-task", () => {
|
||||
toolContext
|
||||
)
|
||||
|
||||
// then - prometheus should have delegate_task permission
|
||||
expect(promptBody.tools.delegate_task).toBe(true)
|
||||
// then - prometheus should have task permission
|
||||
expect(promptBody.tools.task).toBe(true)
|
||||
}, { timeout: 20000 })
|
||||
|
||||
test("non-prometheus subagent should NOT have delegate_task permission", async () => {
|
||||
test("non-prometheus subagent should NOT have task permission", async () => {
|
||||
// given - sisyphus delegates to oracle (non-prometheus)
|
||||
const { createDelegateTask } = require("./tools")
|
||||
let promptBody: any
|
||||
@@ -2810,7 +2936,7 @@ describe("sisyphus-task", () => {
|
||||
// when - sisyphus delegates to oracle
|
||||
await tool.execute(
|
||||
{
|
||||
description: "Test oracle no delegate_task permission",
|
||||
description: "Test oracle no task permission",
|
||||
prompt: "Consult on architecture",
|
||||
subagent_type: "oracle",
|
||||
run_in_background: false,
|
||||
@@ -2819,8 +2945,8 @@ describe("sisyphus-task", () => {
|
||||
toolContext
|
||||
)
|
||||
|
||||
// then - oracle should NOT have delegate_task permission
|
||||
expect(promptBody.tools.delegate_task).toBe(false)
|
||||
// then - oracle should NOT have task permission
|
||||
expect(promptBody.tools.task).toBe(false)
|
||||
}, { timeout: 20000 })
|
||||
})
|
||||
|
||||
|
||||
@@ -86,6 +86,13 @@ Prompts MUST be in English.`
|
||||
async execute(args: DelegateTaskArgs, toolContext) {
|
||||
const ctx = toolContext as ToolContextWithMetadata
|
||||
|
||||
if (args.category && !args.subagent_type) {
|
||||
args.subagent_type = "sisyphus-junior"
|
||||
}
|
||||
await ctx.metadata?.({
|
||||
title: args.description,
|
||||
})
|
||||
|
||||
if (args.run_in_background === undefined) {
|
||||
throw new Error(`Invalid arguments: 'run_in_background' parameter is REQUIRED. Use run_in_background=false for task delegation, run_in_background=true only for parallel exploration.`)
|
||||
}
|
||||
@@ -116,7 +123,7 @@ Prompts MUST be in English.`
|
||||
return executeSyncContinuation(args, ctx, options)
|
||||
}
|
||||
|
||||
if (args.category && args.subagent_type) {
|
||||
if (args.category && args.subagent_type && args.subagent_type !== "sisyphus-junior") {
|
||||
return `Invalid arguments: Provide EITHER category OR subagent_type, not both.`
|
||||
}
|
||||
|
||||
@@ -157,7 +164,7 @@ Prompts MUST be in English.`
|
||||
|
||||
const isRunInBackgroundExplicitlyFalse = args.run_in_background === false || args.run_in_background === "false" as unknown as boolean
|
||||
|
||||
log("[delegate_task] unstable agent detection", {
|
||||
log("[task] unstable agent detection", {
|
||||
category: args.category,
|
||||
actualModel,
|
||||
isUnstableAgent,
|
||||
|
||||
@@ -28,7 +28,12 @@ export interface ToolContextWithMetadata {
|
||||
messageID: string
|
||||
agent: string
|
||||
abort: AbortSignal
|
||||
metadata?: (input: { title?: string; metadata?: Record<string, unknown> }) => void
|
||||
metadata?: (input: { title?: string; metadata?: Record<string, unknown> }) => void | Promise<void>
|
||||
/**
|
||||
* Tool call ID injected by OpenCode's internal context (not in plugin ToolContext type,
|
||||
* but present at runtime via spread in fromPlugin()). Used for metadata store keying.
|
||||
*/
|
||||
callID?: string
|
||||
}
|
||||
|
||||
export interface SyncSessionCreatedEvent {
|
||||
|
||||
Reference in New Issue
Block a user