diff --git a/src/features/background-agent/manager.ts b/src/features/background-agent/manager.ts index ab77ab46..aa492278 100644 --- a/src/features/background-agent/manager.ts +++ b/src/features/background-agent/manager.ts @@ -138,6 +138,7 @@ export class BackgroundManager { parentModel: input.parentModel, parentAgent: input.parentAgent, model: input.model, + category: input.category, } this.tasks.set(task.id, task) @@ -231,7 +232,7 @@ export class BackgroundManager { const createResult = await this.client.session.create({ body: { parentID: input.parentSessionID, - title: `Background: ${input.description}`, + title: `${input.description} (@${input.agent} subagent)`, permission: [ { permission: "question", action: "deny" as const, pattern: "*" }, ], diff --git a/src/features/background-agent/types.ts b/src/features/background-agent/types.ts index e9b29938..553f5bb5 100644 --- a/src/features/background-agent/types.ts +++ b/src/features/background-agent/types.ts @@ -38,6 +38,8 @@ export interface BackgroundTask { parentAgent?: string /** Marks if the task was launched from an unstable agent/category */ isUnstableAgent?: boolean + /** Category used for this task (e.g., 'quick', 'visual-engineering') */ + category?: string /** Last message count for stability detection */ lastMsgCount?: number @@ -57,6 +59,7 @@ export interface LaunchInput { isUnstableAgent?: boolean skills?: string[] skillContent?: string + category?: string } export interface ResumeInput { diff --git a/src/tools/delegate-task/executor.ts b/src/tools/delegate-task/executor.ts index c9a7712d..8d2a75fe 100644 --- a/src/tools/delegate-task/executor.ts +++ b/src/tools/delegate-task/executor.ts @@ -127,13 +127,16 @@ export async function executeBackgroundContinuation( return `Background task continued. Task ID: ${task.id} -Session ID: ${task.sessionID} Description: ${task.description} Agent: ${task.agent} Status: ${task.status} Agent continues with full previous context preserved. -Use \`background_output\` with task_id="${task.id}" to check progress.` +Use \`background_output\` with task_id="${task.id}" to check progress. + + +session_id: ${task.sessionID} +` } catch (error) { return formatDetailedError(error, { operation: "Continue background task", @@ -277,14 +280,13 @@ export async function executeSyncContinuation( return `Task continued and completed in ${duration}. -Session ID: ${args.session_id} - --- ${textContent || "(No text output)"} ---- -To continue this session: session_id="${args.session_id}"` + +session_id: ${args.session_id} +` } export async function executeUnstableAgentTask( @@ -311,6 +313,7 @@ export async function executeUnstableAgentTask( model: categoryModel, skills: args.load_skills.length > 0 ? args.load_skills : undefined, skillContent: systemContent, + category: args.category, }) const WAIT_FOR_SESSION_INTERVAL_MS = 100 @@ -408,7 +411,6 @@ Your run_in_background=false was automatically converted to background mode for Duration: ${duration} Agent: ${agentToUse}${args.category ? ` (category: ${args.category})` : ""} -Session ID: ${sessionID} MONITORING INSTRUCTIONS: - The task was monitored and completed successfully @@ -422,8 +424,9 @@ RESULT: ${textContent || "(No text output)"} ---- -To continue this session: session_id="${sessionID}"` + +session_id: ${sessionID} +` } catch (error) { return formatDetailedError(error, { operation: "Launch monitored background task", @@ -457,6 +460,7 @@ export async function executeBackgroundTask( model: categoryModel, skills: args.load_skills.length > 0 ? args.load_skills : undefined, skillContent: systemContent, + category: args.category, }) ctx.metadata?.({ @@ -476,13 +480,15 @@ export async function executeBackgroundTask( return `Background task launched. Task ID: ${task.id} -Session ID: ${task.sessionID} Description: ${task.description} Agent: ${task.agent}${args.category ? ` (category: ${args.category})` : ""} Status: ${task.status} System notifies on completion. Use \`background_output\` with task_id="${task.id}" to check. -To continue this session: session_id="${task.sessionID}"` + + +session_id: ${task.sessionID} +` } catch (error) { return formatDetailedError(error, { operation: "Launch background task", @@ -517,7 +523,7 @@ export async function executeSyncTask( const createResult = await client.session.create({ body: { parentID: parentContext.sessionID, - title: `Task: ${args.description}`, + title: `${args.description} (@${agentToUse} subagent)`, permission: [ { permission: "question", action: "deny" as const, pattern: "*" }, ], @@ -715,14 +721,14 @@ export async function executeSyncTask( return `Task completed in ${duration}. Agent: ${agentToUse}${args.category ? ` (category: ${args.category})` : ""} -Session ID: ${sessionID} --- ${textContent || "(No text output)"} ---- -To continue this session: session_id="${sessionID}"` + +session_id: ${sessionID} +` } catch (error) { if (toastManager && taskId !== undefined) { toastManager.removeTask(taskId) diff --git a/src/tools/delegate-task/tools.test.ts b/src/tools/delegate-task/tools.test.ts index 64465ba9..d88d362b 100644 --- a/src/tools/delegate-task/tools.test.ts +++ b/src/tools/delegate-task/tools.test.ts @@ -2563,4 +2563,162 @@ describe("sisyphus-task", () => { expect(promptBody.tools.delegate_task).toBe(false) }, { timeout: 20000 }) }) + + describe("session title and metadata format (OpenCode compatibility)", () => { + test("sync session title follows OpenCode format: '{description} (@{agent} subagent)'", async () => { + // given + const { createDelegateTask } = require("./tools") + let createBody: any + + const mockManager = { launch: async () => ({}) } + const mockClient = { + app: { agents: async () => ({ data: [] }) }, + config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, + model: { list: async () => [{ id: SYSTEM_DEFAULT_MODEL }] }, + session: { + get: async () => ({ data: { directory: "/project" } }), + create: async (input: any) => { + createBody = input.body + return { data: { id: "ses_title_test" } } + }, + prompt: async () => ({ data: {} }), + messages: async () => ({ + data: [{ info: { role: "assistant" }, parts: [{ type: "text", text: "done" }] }] + }), + status: async () => ({ data: { "ses_title_test": { type: "idle" } } }), + }, + } + + const tool = createDelegateTask({ + manager: mockManager, + client: mockClient, + }) + + const toolContext = { + sessionID: "parent-session", + messageID: "parent-message", + agent: "sisyphus", + abort: new AbortController().signal, + } + + // when - sync task with category + await tool.execute( + { + description: "Implement feature X", + prompt: "Build the feature", + category: "quick", + run_in_background: false, + load_skills: [], + }, + toolContext + ) + + // then - title should follow OpenCode format + expect(createBody.title).toBe("Implement feature X (@sisyphus-junior subagent)") + }, { timeout: 10000 }) + + test("sync task output includes block with session_id", async () => { + // given + const { createDelegateTask } = require("./tools") + + const mockManager = { launch: async () => ({}) } + const mockClient = { + app: { agents: async () => ({ data: [] }) }, + config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, + model: { list: async () => [{ id: SYSTEM_DEFAULT_MODEL }] }, + session: { + get: async () => ({ data: { directory: "/project" } }), + create: async () => ({ data: { id: "ses_metadata_test" } }), + prompt: async () => ({ data: {} }), + messages: async () => ({ + data: [{ info: { role: "assistant" }, parts: [{ type: "text", text: "Task completed" }] }] + }), + status: async () => ({ data: { "ses_metadata_test": { type: "idle" } } }), + }, + } + + const tool = createDelegateTask({ + manager: mockManager, + client: mockClient, + }) + + const toolContext = { + sessionID: "parent-session", + messageID: "parent-message", + agent: "sisyphus", + abort: new AbortController().signal, + } + + // when + const result = await tool.execute( + { + description: "Test metadata format", + prompt: "Do something", + category: "quick", + run_in_background: false, + load_skills: [], + }, + toolContext + ) + + // then - output should contain block + expect(result).toContain("") + expect(result).toContain("session_id: ses_metadata_test") + expect(result).toContain("") + }, { timeout: 10000 }) + + test("background task output includes block with session_id", async () => { + // given + const { createDelegateTask } = require("./tools") + + const mockManager = { + launch: async () => ({ + id: "bg_meta_test", + sessionID: "ses_bg_metadata", + description: "Background metadata test", + agent: "sisyphus-junior", + status: "running", + }), + } + const mockClient = { + app: { agents: async () => ({ data: [] }) }, + config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, + model: { list: async () => [{ id: SYSTEM_DEFAULT_MODEL }] }, + session: { + create: async () => ({ data: { id: "ses_bg_metadata" } }), + prompt: async () => ({ data: {} }), + messages: async () => ({ data: [] }), + }, + } + + const tool = createDelegateTask({ + manager: mockManager, + client: mockClient, + }) + + const toolContext = { + sessionID: "parent-session", + messageID: "parent-message", + agent: "sisyphus", + abort: new AbortController().signal, + } + + // when + const result = await tool.execute( + { + description: "Background metadata test", + prompt: "Do something", + category: "quick", + run_in_background: true, + load_skills: [], + }, + toolContext + ) + + // then - output should contain block + expect(result).toContain("") + expect(result).toContain("session_id: ses_bg_metadata") + expect(result).toContain("") + }, { timeout: 10000 }) + }) })