diff --git a/src/tools/sisyphus-task/constants.ts b/src/tools/sisyphus-task/constants.ts new file mode 100644 index 00000000..c468cf7d --- /dev/null +++ b/src/tools/sisyphus-task/constants.ts @@ -0,0 +1,249 @@ +import type { CategoryConfig } from "../../config/schema" + +export const VISUAL_CATEGORY_PROMPT_APPEND = ` +You are working on VISUAL/UI tasks. + +Design-first mindset: +- Bold aesthetic choices over safe defaults +- Unexpected layouts, asymmetry, grid-breaking elements +- Distinctive typography (avoid: Arial, Inter, Roboto, Space Grotesk) +- Cohesive color palettes with sharp accents +- High-impact animations with staggered reveals +- Atmosphere: gradient meshes, noise textures, layered transparencies + +AVOID: Generic fonts, purple gradients on white, predictable layouts, cookie-cutter patterns. +` + +export const STRATEGIC_CATEGORY_PROMPT_APPEND = ` +You are working on BUSINESS LOGIC / ARCHITECTURE tasks. + +Strategic advisor mindset: +- Bias toward simplicity: least complex solution that fulfills requirements +- Leverage existing code/patterns over new components +- Prioritize developer experience and maintainability +- One clear recommendation with effort estimate (Quick/Short/Medium/Large) +- Signal when advanced approach warranted + +Response format: +- Bottom line (2-3 sentences) +- Action plan (numbered steps) +- Risks and mitigations (if relevant) +` + +export const ARTISTRY_CATEGORY_PROMPT_APPEND = ` +You are working on HIGHLY CREATIVE / ARTISTIC tasks. + +Artistic genius mindset: +- Push far beyond conventional boundaries +- Explore radical, unconventional directions +- Surprise and delight: unexpected twists, novel combinations +- Rich detail and vivid expression +- Break patterns deliberately when it serves the creative vision + +Approach: +- Generate diverse, bold options first +- Embrace ambiguity and wild experimentation +- Balance novelty with coherence +- This is for tasks requiring exceptional creativity +` + +export const QUICK_CATEGORY_PROMPT_APPEND = ` +You are working on SMALL / QUICK tasks. + +Efficient execution mindset: +- Fast, focused, minimal overhead +- Get to the point immediately +- No over-engineering +- Simple solutions for simple problems + +Approach: +- Minimal viable implementation +- Skip unnecessary abstractions +- Direct and concise + + + +⚠️ THIS CATEGORY USES A LESS CAPABLE MODEL (claude-haiku-4-5). + +The model executing this task has LIMITED reasoning capacity. Your prompt MUST be: + +**EXHAUSTIVELY EXPLICIT** - Leave NOTHING to interpretation: +1. MUST DO: List every required action as atomic, numbered steps +2. MUST NOT DO: Explicitly forbid likely mistakes and deviations +3. EXPECTED OUTPUT: Describe exact success criteria with concrete examples + +**WHY THIS MATTERS:** +- Less capable models WILL deviate without explicit guardrails +- Vague instructions → unpredictable results +- Implicit expectations → missed requirements + +**PROMPT STRUCTURE (MANDATORY):** +\`\`\` +TASK: [One-sentence goal] + +MUST DO: +1. [Specific action with exact details] +2. [Another specific action] +... + +MUST NOT DO: +- [Forbidden action + why] +- [Another forbidden action] +... + +EXPECTED OUTPUT: +- [Exact deliverable description] +- [Success criteria / verification method] +\`\`\` + +If your prompt lacks this structure, REWRITE IT before delegating. +` + +export const MOST_CAPABLE_CATEGORY_PROMPT_APPEND = ` +You are working on COMPLEX / MOST-CAPABLE tasks. + +Maximum capability mindset: +- Bring full reasoning power to bear +- Consider all edge cases and implications +- Deep analysis before action +- Quality over speed + +Approach: +- Thorough understanding first +- Comprehensive solution design +- Meticulous execution +- This is for the most challenging problems +` + +export const WRITING_CATEGORY_PROMPT_APPEND = ` +You are working on WRITING / PROSE tasks. + +Wordsmith mindset: +- Clear, flowing prose +- Appropriate tone and voice +- Engaging and readable +- Proper structure and organization + +Approach: +- Understand the audience +- Draft with care +- Polish for clarity and impact +- Documentation, READMEs, articles, technical writing +` + +export const GENERAL_CATEGORY_PROMPT_APPEND = ` +You are working on GENERAL tasks. + +Balanced execution mindset: +- Practical, straightforward approach +- Good enough is good enough +- Focus on getting things done + +Approach: +- Standard best practices +- Reasonable trade-offs +- Efficient completion + + + +⚠️ THIS CATEGORY USES A MID-TIER MODEL (claude-sonnet-4-5). + +While capable, this model benefits significantly from EXPLICIT instructions. + +**PROVIDE CLEAR STRUCTURE:** +1. MUST DO: Enumerate required actions explicitly - don't assume inference +2. MUST NOT DO: State forbidden actions to prevent scope creep or wrong approaches +3. EXPECTED OUTPUT: Define concrete success criteria and deliverables + +**COMMON PITFALLS WITHOUT EXPLICIT INSTRUCTIONS:** +- Model may take shortcuts that miss edge cases +- Implicit requirements get overlooked +- Output format may not match expectations +- Scope may expand beyond intended boundaries + +**RECOMMENDED PROMPT PATTERN:** +\`\`\` +TASK: [Clear, single-purpose goal] + +CONTEXT: [Relevant background the model needs] + +MUST DO: +- [Explicit requirement 1] +- [Explicit requirement 2] + +MUST NOT DO: +- [Boundary/constraint 1] +- [Boundary/constraint 2] + +EXPECTED OUTPUT: +- [What success looks like] +- [How to verify completion] +\`\`\` + +The more explicit your prompt, the better the results. +` + +export const DEFAULT_CATEGORIES: Record = { + "visual-engineering": { + model: "google/gemini-3-pro-preview", + temperature: 0.7, + }, + "high-iq": { + model: "openai/gpt-5.2", + temperature: 0.1, + }, + artistry: { + model: "google/gemini-3-pro-preview", + temperature: 0.9, + }, + quick: { + model: "anthropic/claude-haiku-4-5", + temperature: 0.3, + }, + "most-capable": { + model: "anthropic/claude-opus-4-5", + temperature: 0.1, + }, + writing: { + model: "google/gemini-3-flash-preview", + temperature: 0.5, + }, + general: { + model: "anthropic/claude-sonnet-4-5", + temperature: 0.3, + }, +} + +export const CATEGORY_PROMPT_APPENDS: Record = { + "visual-engineering": VISUAL_CATEGORY_PROMPT_APPEND, + "high-iq": STRATEGIC_CATEGORY_PROMPT_APPEND, + artistry: ARTISTRY_CATEGORY_PROMPT_APPEND, + quick: QUICK_CATEGORY_PROMPT_APPEND, + "most-capable": MOST_CAPABLE_CATEGORY_PROMPT_APPEND, + writing: WRITING_CATEGORY_PROMPT_APPEND, + general: GENERAL_CATEGORY_PROMPT_APPEND, +} + +export const CATEGORY_DESCRIPTIONS: Record = { + "visual-engineering": "Frontend, UI/UX, design, styling, animation", + "high-iq": "Strict architecture design, very complex business logic", + artistry: "Highly creative/artistic tasks, novel ideas", + quick: "Cheap & fast - small tasks with minimal overhead, budget-friendly", + "most-capable": "Complex tasks requiring maximum capability", + writing: "Documentation, prose, technical writing", + general: "General purpose tasks", +} + +const BUILTIN_CATEGORIES = Object.keys(DEFAULT_CATEGORIES).join(", ") + +export const SISYPHUS_TASK_DESCRIPTION = `Spawn agent task with category-based or direct agent selection. + +MUTUALLY EXCLUSIVE: Provide EITHER category OR agent, not both (unless resuming). + +- category: Use predefined category (${BUILTIN_CATEGORIES}) → Spawns Sisyphus-Junior with category config +- agent: Use specific agent directly (e.g., "oracle", "explore") +- background: true=async (returns task_id), false=sync (waits for result). Default: false. Use background=true ONLY for parallel exploration with 5+ independent queries. +- resume: Task ID to resume - continues previous agent session with full context preserved +- skills: Array of skill names to prepend to prompt (e.g., ["playwright", "frontend-ui-ux"]). Skills will be resolved and their content prepended with a separator. Empty array = no prepending. + +Prompts MUST be in English.` diff --git a/src/tools/sisyphus-task/index.ts b/src/tools/sisyphus-task/index.ts new file mode 100644 index 00000000..bbbe3f58 --- /dev/null +++ b/src/tools/sisyphus-task/index.ts @@ -0,0 +1,3 @@ +export { createSisyphusTask, type SisyphusTaskToolOptions } from "./tools" +export type * from "./types" +export * from "./constants" diff --git a/src/tools/sisyphus-task/tools.test.ts b/src/tools/sisyphus-task/tools.test.ts new file mode 100644 index 00000000..05964e57 --- /dev/null +++ b/src/tools/sisyphus-task/tools.test.ts @@ -0,0 +1,217 @@ +import { describe, test, expect } from "bun:test" +import { DEFAULT_CATEGORIES, CATEGORY_PROMPT_APPENDS, CATEGORY_DESCRIPTIONS, SISYPHUS_TASK_DESCRIPTION } from "./constants" +import type { CategoryConfig } from "../../config/schema" + +function resolveCategoryConfig( + categoryName: string, + userCategories?: Record +): { config: CategoryConfig; promptAppend: string } | null { + const defaultConfig = DEFAULT_CATEGORIES[categoryName] + const userConfig = userCategories?.[categoryName] + const defaultPromptAppend = CATEGORY_PROMPT_APPENDS[categoryName] ?? "" + + if (!defaultConfig && !userConfig) { + return null + } + + const config: CategoryConfig = { + ...defaultConfig, + ...userConfig, + model: userConfig?.model ?? defaultConfig?.model ?? "anthropic/claude-sonnet-4-5", + } + + let promptAppend = defaultPromptAppend + if (userConfig?.prompt_append) { + promptAppend = defaultPromptAppend + ? defaultPromptAppend + "\n\n" + userConfig.prompt_append + : userConfig.prompt_append + } + + return { config, promptAppend } +} + +describe("sisyphus-task", () => { + describe("DEFAULT_CATEGORIES", () => { + test("visual-engineering category has gemini model", () => { + // #given + const category = DEFAULT_CATEGORIES["visual-engineering"] + + // #when / #then + expect(category).toBeDefined() + expect(category.model).toBe("google/gemini-3-pro-preview") + expect(category.temperature).toBe(0.7) + }) + + test("high-iq category has gpt model", () => { + // #given + const category = DEFAULT_CATEGORIES["high-iq"] + + // #when / #then + expect(category).toBeDefined() + expect(category.model).toBe("openai/gpt-5.2") + expect(category.temperature).toBe(0.1) + }) + }) + + describe("CATEGORY_PROMPT_APPENDS", () => { + test("visual-engineering category has design-focused prompt", () => { + // #given + const promptAppend = CATEGORY_PROMPT_APPENDS["visual-engineering"] + + // #when / #then + expect(promptAppend).toContain("VISUAL/UI") + expect(promptAppend).toContain("Design-first") + }) + + test("high-iq category has strategic prompt", () => { + // #given + const promptAppend = CATEGORY_PROMPT_APPENDS["high-iq"] + + // #when / #then + expect(promptAppend).toContain("BUSINESS LOGIC") + expect(promptAppend).toContain("Strategic advisor") + }) + }) + + describe("CATEGORY_DESCRIPTIONS", () => { + test("has description for all default categories", () => { + // #given + const defaultCategoryNames = Object.keys(DEFAULT_CATEGORIES) + + // #when / #then + for (const name of defaultCategoryNames) { + expect(CATEGORY_DESCRIPTIONS[name]).toBeDefined() + expect(CATEGORY_DESCRIPTIONS[name].length).toBeGreaterThan(0) + } + }) + + test("most-capable category exists and has description", () => { + // #given / #when + const description = CATEGORY_DESCRIPTIONS["most-capable"] + + // #then + expect(description).toBeDefined() + expect(description).toContain("Complex") + }) + }) + + describe("SISYPHUS_TASK_DESCRIPTION", () => { + test("documents background parameter as required with default false", () => { + // #given / #when / #then + expect(SISYPHUS_TASK_DESCRIPTION).toContain("background") + expect(SISYPHUS_TASK_DESCRIPTION).toContain("Default: false") + }) + + test("warns about parallel exploration usage", () => { + // #given / #when / #then + expect(SISYPHUS_TASK_DESCRIPTION).toContain("5+") + }) + }) + + describe("resolveCategoryConfig", () => { + test("returns null for unknown category without user config", () => { + // #given + const categoryName = "unknown-category" + + // #when + const result = resolveCategoryConfig(categoryName) + + // #then + expect(result).toBeNull() + }) + + test("returns default config for builtin category", () => { + // #given + const categoryName = "visual-engineering" + + // #when + const result = resolveCategoryConfig(categoryName) + + // #then + expect(result).not.toBeNull() + expect(result!.config.model).toBe("google/gemini-3-pro-preview") + expect(result!.promptAppend).toContain("VISUAL/UI") + }) + + test("user config overrides default model", () => { + // #given + const categoryName = "visual-engineering" + const userCategories = { + "visual-engineering": { model: "anthropic/claude-opus-4-5" }, + } + + // #when + const result = resolveCategoryConfig(categoryName, userCategories) + + // #then + expect(result).not.toBeNull() + expect(result!.config.model).toBe("anthropic/claude-opus-4-5") + }) + + test("user prompt_append is appended to default", () => { + // #given + const categoryName = "visual-engineering" + const userCategories = { + "visual-engineering": { + model: "google/gemini-3-pro-preview", + prompt_append: "Custom instructions here", + }, + } + + // #when + const result = resolveCategoryConfig(categoryName, userCategories) + + // #then + expect(result).not.toBeNull() + expect(result!.promptAppend).toContain("VISUAL/UI") + expect(result!.promptAppend).toContain("Custom instructions here") + }) + + test("user can define custom category", () => { + // #given + const categoryName = "my-custom" + const userCategories = { + "my-custom": { + model: "openai/gpt-5.2", + temperature: 0.5, + prompt_append: "You are a custom agent", + }, + } + + // #when + const result = resolveCategoryConfig(categoryName, userCategories) + + // #then + expect(result).not.toBeNull() + expect(result!.config.model).toBe("openai/gpt-5.2") + expect(result!.config.temperature).toBe(0.5) + expect(result!.promptAppend).toBe("You are a custom agent") + }) + + test("user category overrides temperature", () => { + // #given + const categoryName = "visual-engineering" + const userCategories = { + "visual-engineering": { + model: "google/gemini-3-pro-preview", + temperature: 0.3, + }, + } + + // #when + const result = resolveCategoryConfig(categoryName, userCategories) + + // #then + expect(result).not.toBeNull() + expect(result!.config.temperature).toBe(0.3) + }) + }) + + describe("skills parameter", () => { + test("SISYPHUS_TASK_DESCRIPTION documents skills parameter", () => { + // #given / #when / #then + expect(SISYPHUS_TASK_DESCRIPTION).toContain("skills") + expect(SISYPHUS_TASK_DESCRIPTION).toContain("Array of skill names") + }) + }) +}) diff --git a/src/tools/sisyphus-task/tools.ts b/src/tools/sisyphus-task/tools.ts new file mode 100644 index 00000000..7481e503 --- /dev/null +++ b/src/tools/sisyphus-task/tools.ts @@ -0,0 +1,312 @@ +import { tool, type PluginInput, type ToolDefinition } from "@opencode-ai/plugin" +import { existsSync, readdirSync } from "node:fs" +import { join } from "node:path" +import type { BackgroundManager } from "../../features/background-agent" +import type { SisyphusTaskArgs } from "./types" +import type { CategoryConfig, CategoriesConfig } from "../../config/schema" +import { SISYPHUS_TASK_DESCRIPTION, DEFAULT_CATEGORIES, CATEGORY_PROMPT_APPENDS } from "./constants" +import { findNearestMessageWithFields, MESSAGE_STORAGE } from "../../features/hook-message-injector" +import { resolveMultipleSkills } from "../../features/opencode-skill-loader/skill-content" +import { createBuiltinSkills } from "../../features/builtin-skills/skills" + +type OpencodeClient = PluginInput["client"] + +const SISYPHUS_JUNIOR_AGENT = "Sisyphus-Junior" +const CATEGORY_EXAMPLES = Object.keys(DEFAULT_CATEGORIES).map(k => `'${k}'`).join(", ") + +function parseModelString(model: string): { providerID: string; modelID: string } | undefined { + const parts = model.split("/") + if (parts.length >= 2) { + return { providerID: parts[0], modelID: parts.slice(1).join("/") } + } + return undefined +} + +function getMessageDir(sessionID: string): string | null { + if (!existsSync(MESSAGE_STORAGE)) return null + + const directPath = join(MESSAGE_STORAGE, sessionID) + if (existsSync(directPath)) return directPath + + for (const dir of readdirSync(MESSAGE_STORAGE)) { + const sessionPath = join(MESSAGE_STORAGE, dir, sessionID) + if (existsSync(sessionPath)) return sessionPath + } + + return null +} + +function formatDuration(start: Date, end?: Date): string { + const duration = (end ?? new Date()).getTime() - start.getTime() + const seconds = Math.floor(duration / 1000) + const minutes = Math.floor(seconds / 60) + const hours = Math.floor(minutes / 60) + + if (hours > 0) return `${hours}h ${minutes % 60}m ${seconds % 60}s` + if (minutes > 0) return `${minutes}m ${seconds % 60}s` + return `${seconds}s` +} + +type ToolContextWithMetadata = { + sessionID: string + messageID: string + agent: string + abort: AbortSignal + metadata?: (input: { title?: string; metadata?: Record }) => void +} + +function resolveCategoryConfig( + categoryName: string, + userCategories?: CategoriesConfig +): { config: CategoryConfig; promptAppend: string } | null { + const defaultConfig = DEFAULT_CATEGORIES[categoryName] + const userConfig = userCategories?.[categoryName] + const defaultPromptAppend = CATEGORY_PROMPT_APPENDS[categoryName] ?? "" + + if (!defaultConfig && !userConfig) { + return null + } + + const config: CategoryConfig = { + ...defaultConfig, + ...userConfig, + model: userConfig?.model ?? defaultConfig?.model ?? "anthropic/claude-sonnet-4-5", + } + + let promptAppend = defaultPromptAppend + if (userConfig?.prompt_append) { + promptAppend = defaultPromptAppend + ? defaultPromptAppend + "\n\n" + userConfig.prompt_append + : userConfig.prompt_append + } + + return { config, promptAppend } +} + +export interface SisyphusTaskToolOptions { + manager: BackgroundManager + client: OpencodeClient + userCategories?: CategoriesConfig +} + +export function createSisyphusTask(options: SisyphusTaskToolOptions): ToolDefinition { + const { manager, client, userCategories } = options + + return tool({ + description: SISYPHUS_TASK_DESCRIPTION, + args: { + description: tool.schema.string().describe("Short task description"), + prompt: tool.schema.string().describe("Full detailed prompt for the agent"), + category: tool.schema.string().optional().describe(`Category name (e.g., ${CATEGORY_EXAMPLES}). Mutually exclusive with agent.`), + agent: tool.schema.string().optional().describe("Agent name directly (e.g., 'oracle', 'explore'). Mutually exclusive with category."), + background: tool.schema.boolean().describe("Run in background. MUST be explicitly set. Use false for task delegation, true only for parallel exploration."), + resume: tool.schema.string().optional().describe("Session ID to resume - continues previous agent session with full context"), + skills: tool.schema.array(tool.schema.string()).optional().describe("Array of skill names to prepend to the prompt. Skills will be resolved and their content prepended with a separator."), + }, + async execute(args: SisyphusTaskArgs, toolContext) { + const ctx = toolContext as ToolContextWithMetadata + if (args.background === undefined) { + return `❌ Invalid arguments: 'background' parameter is REQUIRED. Use background=false for task delegation, background=true only for parallel exploration.` + } + const runInBackground = args.background === true + + // Handle skills - resolve and prepend to prompt + if (args.skills && args.skills.length > 0) { + const { resolved, notFound } = resolveMultipleSkills(args.skills) + if (notFound.length > 0) { + const available = createBuiltinSkills().map(s => s.name).join(", ") + return `❌ Skills not found: ${notFound.join(", ")}. Available: ${available}` + } + const skillContent = Array.from(resolved.values()).join("\n\n") + args.prompt = skillContent + "\n\n---\n\n" + args.prompt + } + + const messageDir = getMessageDir(ctx.sessionID) + const prevMessage = messageDir ? findNearestMessageWithFields(messageDir) : null + const parentModel = prevMessage?.model?.providerID && prevMessage?.model?.modelID + ? { providerID: prevMessage.model.providerID, modelID: prevMessage.model.modelID } + : undefined + + // Handle resume case first + if (args.resume) { + try { + const task = await manager.resume({ + sessionId: args.resume, + prompt: args.prompt, + parentSessionID: ctx.sessionID, + parentMessageID: ctx.messageID, + parentModel, + }) + + ctx.metadata?.({ + title: `Resume: ${task.description}`, + metadata: { sessionId: task.sessionID }, + }) + + return `Background task resumed. + +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.` + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + return `❌ Failed to resume task: ${message}` + } + } + + if (args.category && args.agent) { + return `❌ Invalid arguments: Provide EITHER category OR agent, not both.` + } + + if (!args.category && !args.agent) { + return `❌ Invalid arguments: Must provide either category or agent.` + } + + let agentToUse: string + + let categoryModel: { providerID: string; modelID: string } | undefined + + if (args.category) { + const resolved = resolveCategoryConfig(args.category, userCategories) + if (!resolved) { + return `❌ Unknown category: "${args.category}". Available: ${Object.keys({ ...DEFAULT_CATEGORIES, ...userCategories }).join(", ")}` + } + + agentToUse = SISYPHUS_JUNIOR_AGENT + categoryModel = parseModelString(resolved.config.model) + } else { + agentToUse = args.agent!.trim() + if (!agentToUse) { + return `❌ Agent name cannot be empty.` + } + + // Validate agent exists and is callable (not a primary agent) + try { + const agentsResult = await client.app.agents() + type AgentInfo = { name: string; mode?: "subagent" | "primary" | "all" } + const agents = (agentsResult as { data?: AgentInfo[] }).data ?? agentsResult as unknown as AgentInfo[] + + const callableAgents = agents.filter((a) => a.mode !== "primary") + const callableNames = callableAgents.map((a) => a.name) + + if (!callableNames.includes(agentToUse)) { + const isPrimaryAgent = agents.some((a) => a.name === agentToUse && a.mode === "primary") + if (isPrimaryAgent) { + return `❌ Cannot call primary agent "${agentToUse}" via sisyphus_task. Primary agents are top-level orchestrators.` + } + + const availableAgents = callableNames + .sort() + .join(", ") + return `❌ Unknown agent: "${agentToUse}". Available agents: ${availableAgents}` + } + } catch { + // If we can't fetch agents, proceed anyway - the session.prompt will fail with a clearer error + } + } + + if (runInBackground) { + try { + const task = await manager.launch({ + description: args.description, + prompt: args.prompt, + agent: agentToUse, + parentSessionID: ctx.sessionID, + parentMessageID: ctx.messageID, + parentModel, + model: categoryModel, + }) + + ctx.metadata?.({ + title: args.description, + metadata: { sessionId: task.sessionID, category: args.category }, + }) + + 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.` + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + return `❌ Failed to launch task: ${message}` + } + } + + try { + const createResult = await client.session.create({ + body: { + parentID: ctx.sessionID, + title: `Task: ${args.description}`, + }, + }) + + if (createResult.error) { + return `❌ Failed to create session: ${createResult.error}` + } + + const sessionID = createResult.data.id + const startTime = new Date() + + ctx.metadata?.({ + title: args.description, + metadata: { sessionId: sessionID, category: args.category, sync: true }, + }) + + await client.session.prompt({ + path: { id: sessionID }, + body: { + agent: agentToUse, + model: categoryModel, + tools: { + task: false, + sisyphus_task: false, + }, + parts: [{ type: "text", text: args.prompt }], + }, + }) + + const messagesResult = await client.session.messages({ + path: { id: sessionID }, + }) + + if (messagesResult.error) { + return `❌ Error fetching result: ${messagesResult.error}` + } + + const messages = ((messagesResult as { data?: unknown }).data ?? messagesResult) as Array<{ + info?: { role?: string } + parts?: Array<{ type?: string; text?: string }> + }> + + const assistantMessages = messages.filter((m) => m.info?.role === "assistant") + const lastMessage = assistantMessages[assistantMessages.length - 1] + const textParts = lastMessage?.parts?.filter((p) => p.type === "text") ?? [] + const textContent = textParts.map((p) => p.text ?? "").filter(Boolean).join("\n") + + const duration = formatDuration(startTime) + + return `Task completed in ${duration}. + +Agent: ${agentToUse}${args.category ? ` (category: ${args.category})` : ""} +Session ID: ${sessionID} + +--- + +${textContent || "(No text output)"}` + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + return `❌ Task failed: ${message}` + } + }, + }) +} diff --git a/src/tools/sisyphus-task/types.ts b/src/tools/sisyphus-task/types.ts new file mode 100644 index 00000000..d6581e31 --- /dev/null +++ b/src/tools/sisyphus-task/types.ts @@ -0,0 +1,9 @@ +export interface SisyphusTaskArgs { + description: string + prompt: string + category?: string + agent?: string + background: boolean + resume?: string + skills?: string[] +}