import { TextBlock } from '@anthropic-ai/sdk/resources/index.mjs' import chalk from 'chalk' import { last, memoize } from 'lodash-es' import { EOL } from 'os' import React, { useState, useEffect } from 'react' import { Box, Text } from 'ink' import { z } from 'zod' import { Tool, ValidationResult } from '../../Tool' import { FallbackToolUseRejectedMessage } from '../../components/FallbackToolUseRejectedMessage' import { getAgentPrompt } from '../../constants/prompts' import { getContext } from '../../context' import { hasPermissionsToUseTool } from '../../permissions' import { AssistantMessage, Message as MessageType, query } from '../../query' import { formatDuration, formatNumber } from '../../utils/format' import { getMessagesPath, getNextAvailableLogSidechainNumber, overwriteLog, } from '../../utils/log.js' import { applyMarkdown } from '../../utils/markdown' import { createAssistantMessage, createUserMessage, getLastAssistantMessageId, INTERRUPT_MESSAGE, normalizeMessages, } from '../../utils/messages.js' import { getModelManager } from '../../utils/model' import { getMaxThinkingTokens } from '../../utils/thinking' import { getTheme } from '../../utils/theme' import { generateAgentId } from '../../utils/agentStorage' import { debug as debugLogger } from '../../utils/debugLogger' import { getTaskTools, getPrompt } from './prompt' import { TOOL_NAME } from './constants' import { getActiveAgents, getAgentByType, getAvailableAgentTypes } from '../../utils/agentLoader' const inputSchema = z.object({ description: z .string() .describe('A short (3-5 word) description of the task'), prompt: z.string().describe('The task for the agent to perform'), model_name: z .string() .optional() .describe( 'Optional: Specific model name to use for this task. If not provided, uses the default task model pointer.', ), subagent_type: z .string() .optional() .describe( 'The type of specialized agent to use for this task', ), }) export const TaskTool = { async prompt({ safeMode }) { // Match original Claude Code - prompt returns full agent descriptions return await getPrompt(safeMode) }, name: TOOL_NAME, async description() { // Match original Claude Code exactly - simple description return "Launch a new task" }, inputSchema, async *call( { description, prompt, model_name, subagent_type }, { abortController, options: { safeMode = false, forkNumber, messageLogName, verbose }, readFileTimestamps, }, ) { const startTime = Date.now() // Default to general-purpose if no subagent_type specified const agentType = subagent_type || 'general-purpose' // Apply subagent configuration let effectivePrompt = prompt let effectiveModel = model_name || 'task' let toolFilter = null let temperature = undefined // Load agent configuration dynamically if (agentType) { const agentConfig = await getAgentByType(agentType) if (!agentConfig) { // If agent type not found, return helpful message instead of throwing const availableTypes = await getAvailableAgentTypes() const helpMessage = `Agent type '${agentType}' not found.\n\nAvailable agents:\n${availableTypes.map(t => ` • ${t}`).join('\n')}\n\nUse /agents command to manage agent configurations.` yield { type: 'result', data: { error: helpMessage }, resultForAssistant: helpMessage, } return } // Apply system prompt if configured if (agentConfig.systemPrompt) { effectivePrompt = `${agentConfig.systemPrompt}\n\n${prompt}` } // Apply model if not overridden by model_name parameter if (!model_name && agentConfig.model_name) { // Support inherit: keep pointer-based default if (agentConfig.model_name !== 'inherit') { effectiveModel = agentConfig.model_name as string } } // Store tool filter for later application toolFilter = agentConfig.tools // Note: temperature is not currently in our agent configs // but could be added in the future } const messages: MessageType[] = [createUserMessage(effectivePrompt)] let tools = await getTaskTools(safeMode) // Apply tool filtering if specified by subagent config if (toolFilter) { // Back-compat: ['*'] means all tools const isAllArray = Array.isArray(toolFilter) && toolFilter.length === 1 && toolFilter[0] === '*' if (toolFilter === '*' || isAllArray) { // no-op, keep all tools } else if (Array.isArray(toolFilter)) { tools = tools.filter(tool => toolFilter.includes(tool.name)) } } // We yield an initial message immediately so the UI // doesn't move around when messages start streaming back. yield { type: 'progress', content: createAssistantMessage(chalk.dim(`[${agentType}] ${description}`)), normalizedMessages: normalizeMessages(messages), tools, } const [taskPrompt, context, maxThinkingTokens] = await Promise.all([ getAgentPrompt(), getContext(), getMaxThinkingTokens(messages), ]) // Model already resolved in effectiveModel variable above const modelToUse = effectiveModel // Inject model context to prevent self-referential expert consultations taskPrompt.push(`\nIMPORTANT: You are currently running as ${modelToUse}. You do not need to consult ${modelToUse} via AskExpertModel since you ARE ${modelToUse}. Complete tasks directly using your capabilities.`) let toolUseCount = 0 const getSidechainNumber = memoize(() => getNextAvailableLogSidechainNumber(messageLogName, forkNumber), ) // Generate unique Task ID for this task execution const taskId = generateAgentId() // 🔧 ULTRA SIMPLIFIED: Exact original AgentTool pattern // Build query options, adding temperature if specified const queryOptions = { safeMode, forkNumber, messageLogName, tools, commands: [], verbose, maxThinkingTokens, model: modelToUse, } // Add temperature if specified by subagent config if (temperature !== undefined) { queryOptions['temperature'] = temperature } for await (const message of query( messages, taskPrompt, context, hasPermissionsToUseTool, { abortController, options: queryOptions, messageId: getLastAssistantMessageId(messages), agentId: taskId, readFileTimestamps, }, )) { messages.push(message) overwriteLog( getMessagesPath(messageLogName, forkNumber, getSidechainNumber()), messages.filter(_ => _.type !== 'progress'), ) if (message.type !== 'assistant') { continue } const normalizedMessages = normalizeMessages(messages) // Process tool uses and text content for better visibility for (const content of message.message.content) { if (content.type === 'text' && content.text && content.text !== INTERRUPT_MESSAGE) { // Show agent's reasoning/responses const preview = content.text.length > 200 ? content.text.substring(0, 200) + '...' : content.text yield { type: 'progress', content: createAssistantMessage(`[${agentType}] ${preview}`), normalizedMessages, tools, } } else if (content.type === 'tool_use') { toolUseCount++ // Show which tool is being used with agent context const toolMessage = normalizedMessages.find( _ => _.type === 'assistant' && _.message.content[0]?.type === 'tool_use' && _.message.content[0].id === content.id, ) as AssistantMessage if (toolMessage) { // Clone and modify the message to show agent context const modifiedMessage = { ...toolMessage, message: { ...toolMessage.message, content: toolMessage.message.content.map(c => { if (c.type === 'tool_use' && c.id === content.id) { // Add agent context to tool name display return { ...c, name: c.name // Keep original name, UI will handle display } } return c }) } } yield { type: 'progress', content: modifiedMessage, normalizedMessages, tools, } } } } } const normalizedMessages = normalizeMessages(messages) const lastMessage = last(messages) if (lastMessage?.type !== 'assistant') { throw new Error('Last message was not an assistant message') } // 🔧 CRITICAL FIX: Match original AgentTool interrupt handling pattern exactly if ( lastMessage.message.content.some( _ => _.type === 'text' && _.text === INTERRUPT_MESSAGE, ) ) { yield { type: 'progress', content: lastMessage, normalizedMessages, tools, } } else { const result = [ toolUseCount === 1 ? '1 tool use' : `${toolUseCount} tool uses`, formatNumber( (lastMessage.message.usage.cache_creation_input_tokens ?? 0) + (lastMessage.message.usage.cache_read_input_tokens ?? 0) + lastMessage.message.usage.input_tokens + lastMessage.message.usage.output_tokens, ) + ' tokens', formatDuration(Date.now() - startTime), ] yield { type: 'progress', content: createAssistantMessage(`[${agentType}] Completed (${result.join(' · ')})`), normalizedMessages, tools, } } // Output is an AssistantMessage, but since TaskTool is a tool, it needs // to serialize its response to UserMessage-compatible content. const data = lastMessage.message.content.filter(_ => _.type === 'text') yield { type: 'result', data, normalizedMessages, resultForAssistant: this.renderResultForAssistant(data), tools, } }, isReadOnly() { return true // for now... }, isConcurrencySafe() { return true // Task tool supports concurrent execution in official implementation }, async validateInput(input, context) { if (!input.description || typeof input.description !== 'string') { return { result: false, message: 'Description is required and must be a string', } } if (!input.prompt || typeof input.prompt !== 'string') { return { result: false, message: 'Prompt is required and must be a string', } } // Model validation - similar to Edit tool error handling if (input.model_name) { const modelManager = getModelManager() const availableModels = modelManager.getAllAvailableModelNames() if (!availableModels.includes(input.model_name)) { return { result: false, message: `Model '${input.model_name}' does not exist. Available models: ${availableModels.join(', ')}`, meta: { model_name: input.model_name, availableModels, }, } } } // Validate subagent_type if provided if (input.subagent_type) { const availableTypes = await getAvailableAgentTypes() if (!availableTypes.includes(input.subagent_type)) { return { result: false, message: `Agent type '${input.subagent_type}' does not exist. Available types: ${availableTypes.join(', ')}`, meta: { subagent_type: input.subagent_type, availableTypes, }, } } } return { result: true } }, async isEnabled() { return true }, userFacingName(input?: any) { // Return agent name with proper prefix const agentType = input?.subagent_type || 'general-purpose' return `agent-${agentType}` }, needsPermissions() { return false }, renderResultForAssistant(data: TextBlock[]) { return data.map(block => block.type === 'text' ? block.text : '').join('\n') }, renderToolUseMessage({ description, prompt, model_name, subagent_type }, { verbose }) { if (!description || !prompt) return null const modelManager = getModelManager() const defaultTaskModel = modelManager.getModelName('task') const actualModel = model_name || defaultTaskModel const agentType = subagent_type || 'general-purpose' const promptPreview = prompt.length > 80 ? prompt.substring(0, 80) + '...' : prompt const theme = getTheme() if (verbose) { return ( [{agentType}] {actualModel}: {description} {promptPreview} ) } // Simple display: agent type, model and description return `[${agentType}] ${actualModel}: ${description}` }, renderToolUseRejectedMessage() { return }, renderToolResultMessage(content) { const theme = getTheme() if (Array.isArray(content)) { const textBlocks = content.filter(block => block.type === 'text') const totalLength = textBlocks.reduce( (sum, block) => sum + block.text.length, 0, ) // 🔧 CRITICAL FIX: Use exact match for interrupt detection, not .includes() const isInterrupted = content.some( block => block.type === 'text' && block.text === INTERRUPT_MESSAGE, ) if (isInterrupted) { // 🔧 CRITICAL FIX: Match original system interrupt rendering exactly return (   ⎿   Interrupted by user ) } return (   ⎿   Task completed {textBlocks.length > 0 && ( {' '} ({totalLength} characters) )} ) } return (   ⎿   Task completed ) }, } satisfies Tool