From 549f296eb59ab57363ae90df5a4f97f4e442ed1f Mon Sep 17 00:00:00 2001 From: tanzhenxin Date: Tue, 9 Sep 2025 20:53:53 +0800 Subject: [PATCH] feat: add built-in agent(general-purpose) --- docs/subagents.md | 59 ++-- .../subagents/ActionSelectionStep.tsx | 13 +- .../subagents/AgentSelectionStep.tsx | 120 ++++++- .../components/subagents/AgentViewerStep.tsx | 9 - .../subagents/AgentsManagerDialog.tsx | 17 +- .../subagents/SubagentExecutionDisplay.tsx | 4 +- .../ui/components/subagents/ToolSelector.tsx | 6 +- .../core/src/subagents/builtin-agents.test.ts | 95 ++++++ packages/core/src/subagents/builtin-agents.ts | 95 ++++++ packages/core/src/subagents/index.ts | 4 +- .../src/subagents/subagent-manager.test.ts | 15 +- .../core/src/subagents/subagent-manager.ts | 60 +++- .../src/subagents/subagent-result-format.ts | 156 --------- .../src/subagents/subagent-statistics.test.ts | 309 ++++++++++++++++++ .../core/src/subagents/subagent-statistics.ts | 145 ++++++++ packages/core/src/subagents/subagent.ts | 4 +- packages/core/src/subagents/types.ts | 9 +- packages/core/src/tools/task.test.ts | 2 +- packages/core/src/tools/task.ts | 2 +- 19 files changed, 896 insertions(+), 228 deletions(-) create mode 100644 packages/core/src/subagents/builtin-agents.test.ts create mode 100644 packages/core/src/subagents/builtin-agents.ts delete mode 100644 packages/core/src/subagents/subagent-result-format.ts create mode 100644 packages/core/src/subagents/subagent-statistics.test.ts diff --git a/docs/subagents.md b/docs/subagents.md index 601ef888..34a9fa93 100644 --- a/docs/subagents.md +++ b/docs/subagents.md @@ -32,15 +32,19 @@ Subagents are independent AI assistants that: ### Quick Start 1. **Create your first subagent**: + ``` /agents create ``` + Follow the guided wizard to create a specialized agent. 2. **List existing agents**: + ``` /agents list ``` + View and manage your configured subagents. 3. **Use subagents automatically**: @@ -57,7 +61,6 @@ AI: I'll delegate this to your testing specialist subagent. [Returns with completed test files and execution summary] ``` - ## Management ### CLI Commands @@ -69,6 +72,7 @@ Subagents are managed through the `/agents` slash command and its subcommands: Creates a new subagent through a guided step wizard. **Usage:** + ``` /agents create ``` @@ -78,6 +82,7 @@ Creates a new subagent through a guided step wizard. Opens an interactive management dialog for viewing and managing existing subagents. **Usage:** + ``` /agents list ``` @@ -101,7 +106,7 @@ Subagents are configured using Markdown files with YAML frontmatter. This format --- name: agent-name description: Brief description of when and how to use this agent -tools: tool1, tool2, tool3 # Optional +tools: tool1, tool2, tool3 # Optional --- System prompt content goes here. @@ -124,7 +129,7 @@ Your task: ${task_description} Working directory: ${current_directory} Generated on: ${timestamp} -Focus on creating clear, comprehensive documentation that helps both +Focus on creating clear, comprehensive documentation that helps both new contributors and end users understand the project. ``` @@ -141,22 +146,20 @@ Perfect for comprehensive test creation and test-driven development. name: testing-expert description: Writes comprehensive unit tests, integration tests, and handles test automation with best practices tools: read_file, write_file, read_many_files, run_shell_command -modelConfig: - temp: 0.2 -runConfig: - max_time_minutes: 20 --- You are a testing specialist focused on creating high-quality, maintainable tests. Your expertise includes: + - Unit testing with appropriate mocking and isolation -- Integration testing for component interactions +- Integration testing for component interactions - Test-driven development practices - Edge case identification and comprehensive coverage - Performance and load testing when appropriate For each testing task: + 1. Analyze the code structure and dependencies 2. Identify key functionality, edge cases, and error conditions 3. Create comprehensive test suites with descriptive names @@ -169,6 +172,7 @@ Focus on both positive and negative test cases. ``` **Use Cases:** + - "Write unit tests for the authentication service" - "Create integration tests for the payment processing workflow" - "Add test coverage for edge cases in the data validation module" @@ -182,16 +186,15 @@ Specialized in creating clear, comprehensive documentation. name: documentation-writer description: Creates comprehensive documentation, README files, API docs, and user guides tools: read_file, write_file, read_many_files, web_search -modelConfig: - temp: 0.4 --- You are a technical documentation specialist for ${project_name}. -Your role is to create clear, comprehensive documentation that serves both +Your role is to create clear, comprehensive documentation that serves both developers and end users. Focus on: **For API Documentation:** + - Clear endpoint descriptions with examples - Parameter details with types and constraints - Response format documentation @@ -199,6 +202,7 @@ developers and end users. Focus on: - Authentication requirements **For User Documentation:** + - Step-by-step instructions with screenshots when helpful - Installation and setup guides - Configuration options and examples @@ -206,16 +210,18 @@ developers and end users. Focus on: - FAQ sections based on common user questions **For Developer Documentation:** + - Architecture overviews and design decisions - Code examples that actually work - Contributing guidelines - Development environment setup -Always verify code examples and ensure documentation stays current with +Always verify code examples and ensure documentation stays current with the actual implementation. Use clear headings, bullet points, and examples. ``` **Use Cases:** + - "Create API documentation for the user management endpoints" - "Write a comprehensive README for this project" - "Document the deployment process with troubleshooting steps" @@ -229,15 +235,12 @@ Focused on code quality, security, and best practices. name: code-reviewer description: Reviews code for best practices, security issues, performance, and maintainability tools: read_file, read_many_files -modelConfig: - temp: 0.3 -runConfig: - max_time_minutes: 15 --- You are an experienced code reviewer focused on quality, security, and maintainability. Review criteria: + - **Code Structure**: Organization, modularity, and separation of concerns - **Performance**: Algorithmic efficiency and resource usage - **Security**: Vulnerability assessment and secure coding practices @@ -247,6 +250,7 @@ Review criteria: - **Testing**: Test coverage and testability considerations Provide constructive feedback with: + 1. **Critical Issues**: Security vulnerabilities, major bugs 2. **Important Improvements**: Performance issues, design problems 3. **Minor Suggestions**: Style improvements, refactoring opportunities @@ -257,6 +261,7 @@ Prioritize issues by impact and provide rationale for recommendations. ``` **Use Cases:** + - "Review this authentication implementation for security issues" - "Check the performance implications of this database query logic" - "Evaluate the code structure and suggest improvements" @@ -272,13 +277,12 @@ Optimized for React development, hooks, and component patterns. name: react-specialist description: Expert in React development, hooks, component patterns, and modern React best practices tools: read_file, write_file, read_many_files, run_shell_command -modelConfig: - temp: 0.3 --- You are a React specialist with deep expertise in modern React development. Your expertise covers: + - **Component Design**: Functional components, custom hooks, composition patterns - **State Management**: useState, useReducer, Context API, and external libraries - **Performance**: React.memo, useMemo, useCallback, code splitting @@ -287,6 +291,7 @@ Your expertise covers: - **Modern Patterns**: Suspense, Error Boundaries, Concurrent Features For React tasks: + 1. Use functional components and hooks by default 2. Implement proper TypeScript typing 3. Follow React best practices and conventions @@ -299,6 +304,7 @@ Focus on accessibility and user experience considerations. ``` **Use Cases:** + - "Create a reusable data table component with sorting and filtering" - "Implement a custom hook for API data fetching with caching" - "Refactor this class component to use modern React patterns" @@ -312,13 +318,12 @@ Specialized in Python development, frameworks, and best practices. name: python-expert description: Expert in Python development, frameworks, testing, and Python-specific best practices tools: read_file, write_file, read_many_files, run_shell_command -modelConfig: - temp: 0.3 --- You are a Python expert with deep knowledge of the Python ecosystem. Your expertise includes: + - **Core Python**: Pythonic patterns, data structures, algorithms - **Frameworks**: Django, Flask, FastAPI, SQLAlchemy - **Testing**: pytest, unittest, mocking, test-driven development @@ -328,6 +333,7 @@ Your expertise includes: - **Code Quality**: PEP 8, type hints, linting with pylint/flake8 For Python tasks: + 1. Follow PEP 8 style guidelines 2. Use type hints for better code documentation 3. Implement proper error handling with specific exceptions @@ -340,6 +346,7 @@ Focus on writing clean, maintainable Python code that follows community standard ``` **Use Cases:** + - "Create a FastAPI service for user authentication with JWT tokens" - "Implement a data processing pipeline with pandas and error handling" - "Write a CLI tool using argparse with comprehensive help documentation" @@ -353,6 +360,7 @@ Focus on writing clean, maintainable Python code that follows community standard Each subagent should have a clear, focused purpose. **✅ Good:** + ```markdown --- name: testing-expert @@ -361,6 +369,7 @@ description: Writes comprehensive unit tests and integration tests ``` **❌ Avoid:** + ```markdown --- name: general-helper @@ -375,6 +384,7 @@ description: Helps with testing, documentation, code review, and deployment Define specific expertise areas rather than broad capabilities. **✅ Good:** + ```markdown --- name: react-performance-optimizer @@ -383,6 +393,7 @@ description: Optimizes React applications for performance using profiling and be ``` **❌ Avoid:** + ```markdown --- name: frontend-developer @@ -397,11 +408,13 @@ description: Works on frontend development tasks Write descriptions that clearly indicate when to use the agent. **✅ Good:** + ```markdown description: Reviews code for security vulnerabilities, performance issues, and maintainability concerns ``` **❌ Avoid:** + ```markdown description: A helpful code reviewer ``` @@ -413,8 +426,10 @@ description: A helpful code reviewer #### System Prompt Guidelines **Be Specific About Expertise:** + ```markdown You are a Python testing specialist with expertise in: + - pytest framework and fixtures - Mock objects and dependency injection - Test-driven development practices @@ -422,8 +437,10 @@ You are a Python testing specialist with expertise in: ``` **Include Step-by-Step Approaches:** + ```markdown For each testing task: + 1. Analyze the code structure and dependencies 2. Identify key functionality and edge cases 3. Create comprehensive test suites with clear naming @@ -432,8 +449,10 @@ For each testing task: ``` **Specify Output Standards:** + ```markdown Always follow these standards: + - Use descriptive test names that explain the scenario - Include both positive and negative test cases - Add docstrings for complex test functions diff --git a/packages/cli/src/ui/components/subagents/ActionSelectionStep.tsx b/packages/cli/src/ui/components/subagents/ActionSelectionStep.tsx index 1a6ec7ae..8b92ea60 100644 --- a/packages/cli/src/ui/components/subagents/ActionSelectionStep.tsx +++ b/packages/cli/src/ui/components/subagents/ActionSelectionStep.tsx @@ -8,26 +8,37 @@ import { useState } from 'react'; import { Box } from 'ink'; import { RadioButtonSelect } from '../shared/RadioButtonSelect.js'; import { MANAGEMENT_STEPS } from './types.js'; +import { SubagentConfig } from '@qwen-code/qwen-code-core'; interface ActionSelectionStepProps { + selectedAgent: SubagentConfig | null; onNavigateToStep: (step: string) => void; onNavigateBack: () => void; } export const ActionSelectionStep = ({ + selectedAgent, onNavigateToStep, onNavigateBack, }: ActionSelectionStepProps) => { const [selectedAction, setSelectedAction] = useState< 'view' | 'edit' | 'delete' | null >(null); - const actions = [ + + // Filter actions based on whether the agent is built-in + const allActions = [ { label: 'View Agent', value: 'view' as const }, { label: 'Edit Agent', value: 'edit' as const }, { label: 'Delete Agent', value: 'delete' as const }, { label: 'Back', value: 'back' as const }, ]; + const actions = selectedAgent?.isBuiltin + ? allActions.filter( + (action) => action.value === 'view' || action.value === 'back', + ) + : allActions; + const handleActionSelect = (value: 'view' | 'edit' | 'delete' | 'back') => { if (value === 'back') { onNavigateBack(); diff --git a/packages/cli/src/ui/components/subagents/AgentSelectionStep.tsx b/packages/cli/src/ui/components/subagents/AgentSelectionStep.tsx index aeecafe8..d91e6092 100644 --- a/packages/cli/src/ui/components/subagents/AgentSelectionStep.tsx +++ b/packages/cli/src/ui/components/subagents/AgentSelectionStep.tsx @@ -12,9 +12,10 @@ import { useKeypress } from '../../hooks/useKeypress.js'; import { SubagentConfig } from '@qwen-code/qwen-code-core'; interface NavigationState { - currentBlock: 'project' | 'user'; + currentBlock: 'project' | 'user' | 'builtin'; projectIndex: number; userIndex: number; + builtinIndex: number; } interface AgentSelectionStepProps { @@ -30,6 +31,7 @@ export const AgentSelectionStep = ({ currentBlock: 'project', projectIndex: 0, userIndex: 0, + builtinIndex: 0, }); // Group agents by level @@ -41,6 +43,10 @@ export const AgentSelectionStep = ({ () => availableAgents.filter((agent) => agent.level === 'user'), [availableAgents], ); + const builtinAgents = useMemo( + () => availableAgents.filter((agent) => agent.level === 'builtin'), + [availableAgents], + ); const projectNames = useMemo( () => new Set(projectAgents.map((agent) => agent.name)), [projectAgents], @@ -52,8 +58,10 @@ export const AgentSelectionStep = ({ setNavigation((prev) => ({ ...prev, currentBlock: 'project' })); } else if (userAgents.length > 0) { setNavigation((prev) => ({ ...prev, currentBlock: 'user' })); + } else if (builtinAgents.length > 0) { + setNavigation((prev) => ({ ...prev, currentBlock: 'builtin' })); } - }, [projectAgents, userAgents]); + }, [projectAgents, userAgents, builtinAgents]); // Custom keyboard navigation useKeypress( @@ -65,6 +73,13 @@ export const AgentSelectionStep = ({ if (prev.currentBlock === 'project') { if (prev.projectIndex > 0) { return { ...prev, projectIndex: prev.projectIndex - 1 }; + } else if (builtinAgents.length > 0) { + // Move to last item in builtin block + return { + ...prev, + currentBlock: 'builtin', + builtinIndex: builtinAgents.length - 1, + }; } else if (userAgents.length > 0) { // Move to last item in user block return { @@ -76,7 +91,7 @@ export const AgentSelectionStep = ({ // Wrap to last item in project block return { ...prev, projectIndex: projectAgents.length - 1 }; } - } else { + } else if (prev.currentBlock === 'user') { if (prev.userIndex > 0) { return { ...prev, userIndex: prev.userIndex - 1 }; } else if (projectAgents.length > 0) { @@ -86,10 +101,39 @@ export const AgentSelectionStep = ({ currentBlock: 'project', projectIndex: projectAgents.length - 1, }; + } else if (builtinAgents.length > 0) { + // Move to last item in builtin block + return { + ...prev, + currentBlock: 'builtin', + builtinIndex: builtinAgents.length - 1, + }; } else { // Wrap to last item in user block return { ...prev, userIndex: userAgents.length - 1 }; } + } else { + // builtin block + if (prev.builtinIndex > 0) { + return { ...prev, builtinIndex: prev.builtinIndex - 1 }; + } else if (userAgents.length > 0) { + // Move to last item in user block + return { + ...prev, + currentBlock: 'user', + userIndex: userAgents.length - 1, + }; + } else if (projectAgents.length > 0) { + // Move to last item in project block + return { + ...prev, + currentBlock: 'project', + projectIndex: projectAgents.length - 1, + }; + } else { + // Wrap to last item in builtin block + return { ...prev, builtinIndex: builtinAgents.length - 1 }; + } } }); } else if (name === 'down' || name === 'j') { @@ -100,13 +144,19 @@ export const AgentSelectionStep = ({ } else if (userAgents.length > 0) { // Move to first item in user block return { ...prev, currentBlock: 'user', userIndex: 0 }; + } else if (builtinAgents.length > 0) { + // Move to first item in builtin block + return { ...prev, currentBlock: 'builtin', builtinIndex: 0 }; } else { // Wrap to first item in project block return { ...prev, projectIndex: 0 }; } - } else { + } else if (prev.currentBlock === 'user') { if (prev.userIndex < userAgents.length - 1) { return { ...prev, userIndex: prev.userIndex + 1 }; + } else if (builtinAgents.length > 0) { + // Move to first item in builtin block + return { ...prev, currentBlock: 'builtin', builtinIndex: 0 }; } else if (projectAgents.length > 0) { // Move to first item in project block return { ...prev, currentBlock: 'project', projectIndex: 0 }; @@ -114,6 +164,20 @@ export const AgentSelectionStep = ({ // Wrap to first item in user block return { ...prev, userIndex: 0 }; } + } else { + // builtin block + if (prev.builtinIndex < builtinAgents.length - 1) { + return { ...prev, builtinIndex: prev.builtinIndex + 1 }; + } else if (projectAgents.length > 0) { + // Move to first item in project block + return { ...prev, currentBlock: 'project', projectIndex: 0 }; + } else if (userAgents.length > 0) { + // Move to first item in user block + return { ...prev, currentBlock: 'user', userIndex: 0 }; + } else { + // Wrap to first item in builtin block + return { ...prev, builtinIndex: 0 }; + } } }); } else if (name === 'return' || name === 'space') { @@ -121,9 +185,14 @@ export const AgentSelectionStep = ({ let globalIndex: number; if (navigation.currentBlock === 'project') { globalIndex = navigation.projectIndex; - } else { + } else if (navigation.currentBlock === 'user') { // User agents come after project agents in the availableAgents array globalIndex = projectAgents.length + navigation.userIndex; + } else { + // builtin block + // Builtin agents come after project and user agents in the availableAgents array + globalIndex = + projectAgents.length + userAgents.length + navigation.builtinIndex; } if (globalIndex >= 0 && globalIndex < availableAgents.length) { @@ -147,7 +216,11 @@ export const AgentSelectionStep = ({ // Render custom radio button items const renderAgentItem = ( - agent: { name: string; level: 'project' | 'user' }, + agent: { + name: string; + level: 'project' | 'user' | 'builtin'; + isBuiltin?: boolean; + }, index: number, isSelected: boolean, ) => { @@ -162,6 +235,12 @@ export const AgentSelectionStep = ({ {agent.name} + {agent.isBuiltin && ( + + {' '} + (built-in) + + )} {agent.level === 'user' && projectNames.has(agent.name) && ( {' '} @@ -176,7 +255,8 @@ export const AgentSelectionStep = ({ // Calculate enabled agents count (excluding conflicted user-level agents) const enabledAgentsCount = projectAgents.length + - userAgents.filter((agent) => !projectNames.has(agent.name)).length; + userAgents.filter((agent) => !projectNames.has(agent.name)).length + + builtinAgents.length; return ( @@ -199,7 +279,10 @@ export const AgentSelectionStep = ({ {/* User Level Agents */} {userAgents.length > 0 && ( - + 0 ? 1 : 0} + > User Level ({userAgents[0].filePath.replace(/\/[^/]+$/, '')}) @@ -214,8 +297,27 @@ export const AgentSelectionStep = ({ )} + {/* Built-in Agents */} + {builtinAgents.length > 0 && ( + + + Built-in Agents + + + {builtinAgents.map((agent, index) => { + const isSelected = + navigation.currentBlock === 'builtin' && + navigation.builtinIndex === index; + return renderAgentItem(agent, index, isSelected); + })} + + + )} + {/* Agent count summary */} - {(projectAgents.length > 0 || userAgents.length > 0) && ( + {(projectAgents.length > 0 || + userAgents.length > 0 || + builtinAgents.length > 0) && ( Using: {enabledAgentsCount} agents diff --git a/packages/cli/src/ui/components/subagents/AgentViewerStep.tsx b/packages/cli/src/ui/components/subagents/AgentViewerStep.tsx index f39800cf..3f6fb0eb 100644 --- a/packages/cli/src/ui/components/subagents/AgentViewerStep.tsx +++ b/packages/cli/src/ui/components/subagents/AgentViewerStep.tsx @@ -29,15 +29,6 @@ export const AgentViewerStep = ({ selectedAgent }: AgentViewerStepProps) => { return ( - - Location: - - {agent.level === 'project' - ? 'Project Level (.qwen/agents/)' - : 'User Level (~/.qwen/agents/)'} - - - File Path: {agent.filePath} diff --git a/packages/cli/src/ui/components/subagents/AgentsManagerDialog.tsx b/packages/cli/src/ui/components/subagents/AgentsManagerDialog.tsx index fa300e03..fbbfca16 100644 --- a/packages/cli/src/ui/components/subagents/AgentsManagerDialog.tsx +++ b/packages/cli/src/ui/components/subagents/AgentsManagerDialog.tsx @@ -50,14 +50,19 @@ export function AgentsManagerDialog({ const manager = config.getSubagentManager(); - // Load agents from both levels separately to show all agents including conflicts - const [projectAgents, userAgents] = await Promise.all([ + // Load agents from all levels separately to show all agents including conflicts + const [projectAgents, userAgents, builtinAgents] = await Promise.all([ manager.listSubagents({ level: 'project' }), manager.listSubagents({ level: 'user' }), + manager.listSubagents({ level: 'builtin' }), ]); - // Combine all agents (project and user level) - const allAgents = [...(projectAgents || []), ...(userAgents || [])]; + // Combine all agents (project, user, and builtin level) + const allAgents = [ + ...(projectAgents || []), + ...(userAgents || []), + ...(builtinAgents || []), + ]; setAvailableAgents(allAgents); }, [config]); @@ -208,7 +213,9 @@ export function AgentsManagerDialog({ /> ); case MANAGEMENT_STEPS.ACTION_SELECTION: - return ; + return ( + + ); case MANAGEMENT_STEPS.AGENT_VIEWER: return ( diff --git a/packages/cli/src/ui/components/subagents/SubagentExecutionDisplay.tsx b/packages/cli/src/ui/components/subagents/SubagentExecutionDisplay.tsx index 7cd50910..1451ef66 100644 --- a/packages/cli/src/ui/components/subagents/SubagentExecutionDisplay.tsx +++ b/packages/cli/src/ui/components/subagents/SubagentExecutionDisplay.tsx @@ -289,9 +289,7 @@ const ToolCallItem: React.FC<{ {statusIcon} - - {toolCall.name} - {' '} + {toolCall.name}{' '} {description} {toolCall.error && ( - {toolCall.error} diff --git a/packages/cli/src/ui/components/subagents/ToolSelector.tsx b/packages/cli/src/ui/components/subagents/ToolSelector.tsx index c0df99d3..17303bbf 100644 --- a/packages/cli/src/ui/components/subagents/ToolSelector.tsx +++ b/packages/cli/src/ui/components/subagents/ToolSelector.tsx @@ -65,7 +65,8 @@ export function ToolSelector({ (tool) => tool.kind === Kind.Read || tool.kind === Kind.Search || - tool.kind === Kind.Fetch, + tool.kind === Kind.Fetch || + tool.kind === Kind.Think, ) .map((tool) => tool.displayName) .sort(); @@ -75,8 +76,7 @@ export function ToolSelector({ (tool) => tool.kind === Kind.Edit || tool.kind === Kind.Delete || - tool.kind === Kind.Move || - tool.kind === Kind.Think, + tool.kind === Kind.Move, ) .map((tool) => tool.displayName) .sort(); diff --git a/packages/core/src/subagents/builtin-agents.test.ts b/packages/core/src/subagents/builtin-agents.test.ts new file mode 100644 index 00000000..9e7debc1 --- /dev/null +++ b/packages/core/src/subagents/builtin-agents.test.ts @@ -0,0 +1,95 @@ +/** + * @license + * Copyright 2025 Qwen + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect } from 'vitest'; +import { BuiltinAgentRegistry } from './builtin-agents.js'; + +describe('BuiltinAgentRegistry', () => { + describe('getBuiltinAgents', () => { + it('should return array of builtin agents with correct properties', () => { + const agents = BuiltinAgentRegistry.getBuiltinAgents(); + + expect(agents).toBeInstanceOf(Array); + expect(agents.length).toBeGreaterThan(0); + + agents.forEach((agent) => { + expect(agent).toMatchObject({ + name: expect.any(String), + description: expect.any(String), + systemPrompt: expect.any(String), + level: 'builtin', + filePath: ``, + isBuiltin: true, + }); + }); + }); + + it('should include general-purpose agent', () => { + const agents = BuiltinAgentRegistry.getBuiltinAgents(); + const generalAgent = agents.find( + (agent) => agent.name === 'general-purpose', + ); + + expect(generalAgent).toBeDefined(); + expect(generalAgent?.description).toContain('General-purpose agent'); + }); + }); + + describe('getBuiltinAgent', () => { + it('should return correct agent for valid name', () => { + const agent = BuiltinAgentRegistry.getBuiltinAgent('general-purpose'); + + expect(agent).toMatchObject({ + name: 'general-purpose', + level: 'builtin', + filePath: '', + isBuiltin: true, + }); + }); + + it('should return null for invalid name', () => { + expect(BuiltinAgentRegistry.getBuiltinAgent('invalid')).toBeNull(); + expect(BuiltinAgentRegistry.getBuiltinAgent('')).toBeNull(); + }); + }); + + describe('isBuiltinAgent', () => { + it('should return true for valid builtin agent names', () => { + expect(BuiltinAgentRegistry.isBuiltinAgent('general-purpose')).toBe(true); + }); + + it('should return false for invalid names', () => { + expect(BuiltinAgentRegistry.isBuiltinAgent('invalid')).toBe(false); + expect(BuiltinAgentRegistry.isBuiltinAgent('')).toBe(false); + }); + }); + + describe('getBuiltinAgentNames', () => { + it('should return array of agent names', () => { + const names = BuiltinAgentRegistry.getBuiltinAgentNames(); + + expect(names).toBeInstanceOf(Array); + expect(names).toContain('general-purpose'); + expect(names.every((name) => typeof name === 'string')).toBe(true); + }); + }); + + describe('consistency', () => { + it('should maintain consistency across all methods', () => { + const agents = BuiltinAgentRegistry.getBuiltinAgents(); + const names = BuiltinAgentRegistry.getBuiltinAgentNames(); + + // Names should match agents + expect(names).toEqual(agents.map((agent) => agent.name)); + + // Each name should be valid + names.forEach((name) => { + expect(BuiltinAgentRegistry.isBuiltinAgent(name)).toBe(true); + expect(BuiltinAgentRegistry.getBuiltinAgent(name)).toBeDefined(); + }); + }); + }); +}); diff --git a/packages/core/src/subagents/builtin-agents.ts b/packages/core/src/subagents/builtin-agents.ts new file mode 100644 index 00000000..437029cf --- /dev/null +++ b/packages/core/src/subagents/builtin-agents.ts @@ -0,0 +1,95 @@ +/** + * @license + * Copyright 2025 Qwen + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SubagentConfig } from './types.js'; + +/** + * Registry of built-in subagents that are always available to all users. + * These agents are embedded in the codebase and cannot be modified or deleted. + */ +export class BuiltinAgentRegistry { + private static readonly BUILTIN_AGENTS: Array< + Omit + > = [ + { + name: 'general-purpose', + description: + 'General-purpose agent for researching complex questions, searching for code, and executing multi-step tasks. When you are searching for a keyword or file and are not confident that you will find the right match in the first few tries use this agent to perform the search for you.', + systemPrompt: `You are a general-purpose research and code analysis agent. Given the user's message, you should use the tools available to complete the task. Do what has been asked; nothing more, nothing less. When you complete the task simply respond with a detailed writeup. + +Your strengths: +- Searching for code, configurations, and patterns across large codebases +- Analyzing multiple files to understand system architecture +- Investigating complex questions that require exploring many files +- Performing multi-step research tasks + +Guidelines: +- For file searches: Use Grep or Glob when you need to search broadly. Use Read when you know the specific file path. +- For analysis: Start broad and narrow down. Use multiple search strategies if the first doesn't yield results. +- Be thorough: Check multiple locations, consider different naming conventions, look for related files. +- NEVER create files unless they're absolutely necessary for achieving your goal. ALWAYS prefer editing an existing file to creating a new one. +- NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested. +- In your final response always share relevant file names and code snippets. Any file paths you return in your response MUST be absolute. Do NOT use relative paths. +- For clear communication, avoid using emojis. + + +Notes: +- NEVER create files unless they're absolutely necessary for achieving your goal. ALWAYS prefer editing an existing file to creating a new one. +- NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User. +- In your final response always share relevant file names and code snippets. Any file paths you return in your response MUST be absolute. Do NOT use relative paths. +- For clear communication with the user the assistant MUST avoid using emojis.`, + }, + ]; + + /** + * Gets all built-in agent configurations. + * @returns Array of built-in subagent configurations + */ + static getBuiltinAgents(): SubagentConfig[] { + return this.BUILTIN_AGENTS.map((agent) => ({ + ...agent, + level: 'builtin' as const, + filePath: ``, + isBuiltin: true, + })); + } + + /** + * Gets a specific built-in agent by name. + * @param name - Name of the built-in agent + * @returns Built-in agent configuration or null if not found + */ + static getBuiltinAgent(name: string): SubagentConfig | null { + const agent = this.BUILTIN_AGENTS.find((a) => a.name === name); + if (!agent) { + return null; + } + + return { + ...agent, + level: 'builtin' as const, + filePath: ``, + isBuiltin: true, + }; + } + + /** + * Checks if an agent name corresponds to a built-in agent. + * @param name - Agent name to check + * @returns True if the name is a built-in agent + */ + static isBuiltinAgent(name: string): boolean { + return this.BUILTIN_AGENTS.some((agent) => agent.name === name); + } + + /** + * Gets the names of all built-in agents. + * @returns Array of built-in agent names + */ + static getBuiltinAgentNames(): string[] { + return this.BUILTIN_AGENTS.map((agent) => agent.name); + } +} diff --git a/packages/core/src/subagents/index.ts b/packages/core/src/subagents/index.ts index 97b31cbb..edc006df 100644 --- a/packages/core/src/subagents/index.ts +++ b/packages/core/src/subagents/index.ts @@ -33,6 +33,9 @@ export type { export { SubagentError } from './types.js'; +// Built-in agents registry +export { BuiltinAgentRegistry } from './builtin-agents.js'; + // Validation system export { SubagentValidator } from './validation.js'; @@ -70,4 +73,3 @@ export type { SubagentStatsSummary, ToolUsageStats, } from './subagent-statistics.js'; -export { formatCompact, formatDetailed } from './subagent-result-format.js'; diff --git a/packages/core/src/subagents/subagent-manager.test.ts b/packages/core/src/subagents/subagent-manager.test.ts index 63705ff2..cd361d88 100644 --- a/packages/core/src/subagents/subagent-manager.test.ts +++ b/packages/core/src/subagents/subagent-manager.test.ts @@ -584,11 +584,12 @@ System prompt 3`); it('should list subagents from both levels', async () => { const subagents = await manager.listSubagents(); - expect(subagents).toHaveLength(3); // agent1 (project takes precedence), agent2, agent3 + expect(subagents).toHaveLength(4); // agent1 (project takes precedence), agent2, agent3, general-purpose (built-in) expect(subagents.map((s) => s.name)).toEqual([ 'agent1', 'agent2', 'agent3', + 'general-purpose', ]); }); @@ -615,7 +616,7 @@ System prompt 3`); }); const names = subagents.map((s) => s.name); - expect(names).toEqual(['agent1', 'agent2', 'agent3']); + expect(names).toEqual(['agent1', 'agent2', 'agent3', 'general-purpose']); }); it('should handle empty directories', async () => { @@ -626,7 +627,9 @@ System prompt 3`); const subagents = await manager.listSubagents(); - expect(subagents).toHaveLength(0); + expect(subagents).toHaveLength(1); // Only built-in agents remain + expect(subagents[0].name).toBe('general-purpose'); + expect(subagents[0].level).toBe('builtin'); }); it('should handle directory read errors', async () => { @@ -636,7 +639,9 @@ System prompt 3`); const subagents = await manager.listSubagents(); - expect(subagents).toHaveLength(0); + expect(subagents).toHaveLength(1); // Only built-in agents remain + expect(subagents[0].name).toBe('general-purpose'); + expect(subagents[0].level).toBe('builtin'); }); it('should skip invalid subagent files', async () => { @@ -656,7 +661,7 @@ System prompt 3`); const subagents = await manager.listSubagents(); - expect(subagents).toHaveLength(1); + expect(subagents).toHaveLength(2); // 1 valid file + 1 built-in agent expect(consoleSpy).toHaveBeenCalledWith( expect.stringContaining('Skipping invalid subagent file'), ); diff --git a/packages/core/src/subagents/subagent-manager.ts b/packages/core/src/subagents/subagent-manager.ts index 774aab9a..dfd18759 100644 --- a/packages/core/src/subagents/subagent-manager.ts +++ b/packages/core/src/subagents/subagent-manager.ts @@ -29,6 +29,7 @@ import { import { SubagentValidator } from './validation.js'; import { SubAgentScope } from './subagent.js'; import { Config } from '../config/config.js'; +import { BuiltinAgentRegistry } from './builtin-agents.js'; const QWEN_CONFIG_DIR = '.qwen'; const AGENT_CONFIG_DIR = 'agents'; @@ -104,7 +105,7 @@ export class SubagentManager { /** * Loads a subagent configuration by name. * If level is specified, only searches that level. - * If level is omitted, searches project-level first, then user-level. + * If level is omitted, searches project-level first, then user-level, then built-in. * * @param name - Name of the subagent to load * @param level - Optional level to limit search to specific level @@ -116,6 +117,10 @@ export class SubagentManager { ): Promise { if (level) { // Search only the specified level + if (level === 'builtin') { + return BuiltinAgentRegistry.getBuiltinAgent(name); + } + const path = this.getSubagentPath(name, level); try { const config = await this.parseSubagentFile(path); @@ -140,9 +145,11 @@ export class SubagentManager { const config = await this.parseSubagentFile(userPath); return config; } catch (_error) { - // Not found at either level - return null; + // Continue to built-in agents } + + // Try built-in agents as fallback + return BuiltinAgentRegistry.getBuiltinAgent(name); } /** @@ -166,6 +173,15 @@ export class SubagentManager { ); } + // Prevent updating built-in agents + if (existing.isBuiltin) { + throw new SubagentError( + `Cannot update built-in subagent "${name}"`, + SubagentErrorCode.INVALID_CONFIG, + name, + ); + } + // Merge updates with existing configuration const updatedConfig = this.mergeConfigurations(existing, updates); @@ -194,12 +210,26 @@ export class SubagentManager { * @throws SubagentError if deletion fails */ async deleteSubagent(name: string, level?: SubagentLevel): Promise { + // Check if it's a built-in agent first + if (BuiltinAgentRegistry.isBuiltinAgent(name)) { + throw new SubagentError( + `Cannot delete built-in subagent "${name}"`, + SubagentErrorCode.INVALID_CONFIG, + name, + ); + } + const levelsToCheck: SubagentLevel[] = level ? [level] : ['project', 'user']; let deleted = false; for (const currentLevel of levelsToCheck) { + // Skip builtin level for deletion + if (currentLevel === 'builtin') { + continue; + } + const filePath = this.getSubagentPath(name, currentLevel); try { @@ -233,14 +263,14 @@ export class SubagentManager { const levelsToCheck: SubagentLevel[] = options.level ? [options.level] - : ['project', 'user']; + : ['project', 'user', 'builtin']; - // Collect subagents from each level (project takes precedence) + // Collect subagents from each level (project takes precedence over user, user takes precedence over builtin) for (const level of levelsToCheck) { const levelSubagents = await this.listSubagentsAtLevel(level); for (const subagent of levelSubagents) { - // Skip if we've already seen this name (project takes precedence) + // Skip if we've already seen this name (precedence: project > user > builtin) if (seenNames.has(subagent.name)) { continue; } @@ -267,11 +297,12 @@ export class SubagentManager { case 'name': comparison = a.name.localeCompare(b.name); break; - case 'level': - // Project comes before user - comparison = - a.level === 'project' ? -1 : b.level === 'project' ? 1 : 0; + case 'level': { + // Project comes before user, user comes before builtin + const levelOrder = { project: 0, user: 1, builtin: 2 }; + comparison = levelOrder[a.level] - levelOrder[b.level]; break; + } default: comparison = 0; break; @@ -605,6 +636,10 @@ export class SubagentManager { * @returns Absolute file path */ getSubagentPath(name: string, level: SubagentLevel): string { + if (level === 'builtin') { + return ``; + } + const baseDir = level === 'project' ? path.join( @@ -626,6 +661,11 @@ export class SubagentManager { private async listSubagentsAtLevel( level: SubagentLevel, ): Promise { + // Handle built-in agents + if (level === 'builtin') { + return BuiltinAgentRegistry.getBuiltinAgents(); + } + const baseDir = level === 'project' ? path.join( diff --git a/packages/core/src/subagents/subagent-result-format.ts b/packages/core/src/subagents/subagent-result-format.ts deleted file mode 100644 index dad62817..00000000 --- a/packages/core/src/subagents/subagent-result-format.ts +++ /dev/null @@ -1,156 +0,0 @@ -/** - * @license - * Copyright 2025 Qwen - * SPDX-License-Identifier: Apache-2.0 - */ - -import { SubagentStatsSummary } from './subagent-statistics.js'; - -function fmtDuration(ms: number): string { - if (ms < 1000) return `${Math.round(ms)}ms`; - if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`; - if (ms < 3600000) { - const m = Math.floor(ms / 60000); - const s = Math.floor((ms % 60000) / 1000); - return `${m}m ${s}s`; - } - const h = Math.floor(ms / 3600000); - const m = Math.floor((ms % 3600000) / 60000); - return `${h}h ${m}m`; -} - -export function formatCompact( - stats: SubagentStatsSummary, - taskDesc: string, -): string { - const sr = - stats.totalToolCalls > 0 - ? (stats.successRate ?? - (stats.successfulToolCalls / stats.totalToolCalls) * 100) - : 0; - const lines = [ - `📋 Task Completed: ${taskDesc}`, - `🔧 Tool Usage: ${stats.totalToolCalls} calls${stats.totalToolCalls ? `, ${sr.toFixed(1)}% success` : ''}`, - `⏱️ Duration: ${fmtDuration(stats.totalDurationMs)} | 🔁 Rounds: ${stats.rounds}`, - ]; - if (typeof stats.totalTokens === 'number') { - lines.push( - `🔢 Tokens: ${stats.totalTokens.toLocaleString()}${stats.inputTokens || stats.outputTokens ? ` (in ${stats.inputTokens ?? 0}, out ${stats.outputTokens ?? 0})` : ''}`, - ); - } - return lines.join('\n'); -} - -export function formatDetailed( - stats: SubagentStatsSummary, - taskDesc: string, -): string { - const sr = - stats.totalToolCalls > 0 - ? (stats.successRate ?? - (stats.successfulToolCalls / stats.totalToolCalls) * 100) - : 0; - const lines: string[] = []; - lines.push(`📋 Task Completed: ${taskDesc}`); - lines.push( - `⏱️ Duration: ${fmtDuration(stats.totalDurationMs)} | 🔁 Rounds: ${stats.rounds}`, - ); - // Quality indicator - let quality = 'Poor execution'; - if (sr >= 95) quality = 'Excellent execution'; - else if (sr >= 85) quality = 'Good execution'; - else if (sr >= 70) quality = 'Fair execution'; - lines.push(`✅ Quality: ${quality} (${sr.toFixed(1)}% tool success)`); - // Speed category - const d = stats.totalDurationMs; - let speed = 'Long execution - consider breaking down tasks'; - if (d < 10_000) speed = 'Fast completion - under 10 seconds'; - else if (d < 60_000) speed = 'Good speed - under a minute'; - else if (d < 300_000) speed = 'Moderate duration - a few minutes'; - lines.push(`🚀 Speed: ${speed}`); - lines.push( - `🔧 Tools: ${stats.totalToolCalls} calls, ${sr.toFixed(1)}% success (${stats.successfulToolCalls} ok, ${stats.failedToolCalls} failed)`, - ); - if (typeof stats.totalTokens === 'number') { - lines.push( - `🔢 Tokens: ${stats.totalTokens.toLocaleString()} (in ${stats.inputTokens ?? 0}, out ${stats.outputTokens ?? 0})`, - ); - } - if (stats.toolUsage && stats.toolUsage.length) { - const sorted = [...stats.toolUsage] - .sort((a, b) => b.count - a.count) - .slice(0, 5); - lines.push('\nTop tools:'); - for (const t of sorted) { - const avg = - typeof t.averageDurationMs === 'number' - ? `, avg ${fmtDuration(Math.round(t.averageDurationMs))}` - : ''; - lines.push( - ` - ${t.name}: ${t.count} calls (${t.success} ok, ${t.failure} fail${avg}${t.lastError ? `, last error: ${t.lastError}` : ''})`, - ); - } - } - const tips = generatePerformanceTips(stats); - if (tips.length) { - lines.push('\n💡 Performance Insights:'); - for (const tip of tips.slice(0, 3)) lines.push(` - ${tip}`); - } - return lines.join('\n'); -} - -export function generatePerformanceTips(stats: SubagentStatsSummary): string[] { - const tips: string[] = []; - const totalCalls = stats.totalToolCalls; - const sr = - stats.totalToolCalls > 0 - ? (stats.successRate ?? - (stats.successfulToolCalls / stats.totalToolCalls) * 100) - : 0; - - // High failure rate - if (sr < 80) - tips.push('Low tool success rate - review inputs and error messages'); - - // Long duration - if (stats.totalDurationMs > 60_000) - tips.push('Long execution time - consider breaking down complex tasks'); - - // Token usage - if (typeof stats.totalTokens === 'number' && stats.totalTokens > 100_000) { - tips.push( - 'High token usage - consider optimizing prompts or narrowing scope', - ); - } - if (typeof stats.totalTokens === 'number' && totalCalls > 0) { - const avgTokPerCall = stats.totalTokens / totalCalls; - if (avgTokPerCall > 5_000) - tips.push( - `High token usage per tool call (~${Math.round(avgTokPerCall)} tokens/call)`, - ); - } - - // Network failures - const isNetworkTool = (name: string) => /web|fetch|search/i.test(name); - const hadNetworkFailure = (stats.toolUsage || []).some( - (t) => - isNetworkTool(t.name) && - t.lastError && - /timeout|network/i.test(t.lastError), - ); - if (hadNetworkFailure) - tips.push( - 'Network operations had failures - consider increasing timeout or checking connectivity', - ); - - // Slow tools - const slow = (stats.toolUsage || []) - .filter((t) => (t.averageDurationMs ?? 0) > 10_000) - .sort((a, b) => (b.averageDurationMs ?? 0) - (a.averageDurationMs ?? 0)); - if (slow.length) - tips.push( - `Consider optimizing ${slow[0].name} operations (avg ${fmtDuration(Math.round(slow[0].averageDurationMs!))})`, - ); - - return tips; -} diff --git a/packages/core/src/subagents/subagent-statistics.test.ts b/packages/core/src/subagents/subagent-statistics.test.ts new file mode 100644 index 00000000..5b4ae3c6 --- /dev/null +++ b/packages/core/src/subagents/subagent-statistics.test.ts @@ -0,0 +1,309 @@ +/** + * @license + * Copyright 2025 Qwen + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect, beforeEach } from 'vitest'; +import { SubagentStatistics } from './subagent-statistics.js'; + +describe('SubagentStatistics', () => { + let stats: SubagentStatistics; + const baseTime = 1000000000000; // Fixed timestamp for consistent testing + + beforeEach(() => { + stats = new SubagentStatistics(); + }); + + describe('basic statistics tracking', () => { + it('should track execution time', () => { + stats.start(baseTime); + const summary = stats.getSummary(baseTime + 5000); + + expect(summary.totalDurationMs).toBe(5000); + }); + + it('should track rounds', () => { + stats.setRounds(3); + const summary = stats.getSummary(); + + expect(summary.rounds).toBe(3); + }); + + it('should track tool calls', () => { + stats.recordToolCall('file_read', true, 100); + stats.recordToolCall('web_search', false, 200, 'Network timeout'); + + const summary = stats.getSummary(); + expect(summary.totalToolCalls).toBe(2); + expect(summary.successfulToolCalls).toBe(1); + expect(summary.failedToolCalls).toBe(1); + expect(summary.successRate).toBe(50); + }); + + it('should track tokens', () => { + stats.recordTokens(1000, 500); + stats.recordTokens(200, 100); + + const summary = stats.getSummary(); + expect(summary.inputTokens).toBe(1200); + expect(summary.outputTokens).toBe(600); + expect(summary.totalTokens).toBe(1800); + }); + }); + + describe('tool usage statistics', () => { + it('should track individual tool usage', () => { + stats.recordToolCall('file_read', true, 100); + stats.recordToolCall('file_read', false, 150, 'Permission denied'); + stats.recordToolCall('web_search', true, 300); + + const summary = stats.getSummary(); + const fileReadTool = summary.toolUsage.find( + (t) => t.name === 'file_read', + ); + const webSearchTool = summary.toolUsage.find( + (t) => t.name === 'web_search', + ); + + expect(fileReadTool).toEqual({ + name: 'file_read', + count: 2, + success: 1, + failure: 1, + lastError: 'Permission denied', + totalDurationMs: 250, + averageDurationMs: 125, + }); + + expect(webSearchTool).toEqual({ + name: 'web_search', + count: 1, + success: 1, + failure: 0, + lastError: undefined, + totalDurationMs: 300, + averageDurationMs: 300, + }); + }); + }); + + describe('formatCompact', () => { + it('should format basic execution summary', () => { + stats.start(baseTime); + stats.setRounds(2); + stats.recordToolCall('file_read', true, 100); + stats.recordTokens(1000, 500); + + const result = stats.formatCompact('Test task', baseTime + 5000); + + expect(result).toContain('📋 Task Completed: Test task'); + expect(result).toContain('🔧 Tool Usage: 1 calls, 100.0% success'); + expect(result).toContain('⏱️ Duration: 5.0s | 🔁 Rounds: 2'); + expect(result).toContain('🔢 Tokens: 1,500 (in 1000, out 500)'); + }); + + it('should handle zero tool calls', () => { + stats.start(baseTime); + + const result = stats.formatCompact('Empty task', baseTime + 1000); + + expect(result).toContain('🔧 Tool Usage: 0 calls'); + expect(result).not.toContain('% success'); + }); + + it('should show zero tokens when no tokens recorded', () => { + stats.start(baseTime); + stats.recordToolCall('test', true, 100); + + const result = stats.formatCompact('No tokens task', baseTime + 1000); + + expect(result).toContain('🔢 Tokens: 0'); + }); + }); + + describe('formatDetailed', () => { + beforeEach(() => { + stats.start(baseTime); + stats.setRounds(3); + stats.recordToolCall('file_read', true, 100); + stats.recordToolCall('file_read', true, 150); + stats.recordToolCall('web_search', false, 2000, 'Network timeout'); + stats.recordTokens(2000, 1000); + }); + + it('should include quality assessment', () => { + const result = stats.formatDetailed('Complex task', baseTime + 30000); + + expect(result).toContain( + '✅ Quality: Poor execution (66.7% tool success)', + ); + }); + + it('should include speed assessment', () => { + const result = stats.formatDetailed('Fast task', baseTime + 5000); + + expect(result).toContain('🚀 Speed: Fast completion - under 10 seconds'); + }); + + it('should show top tools', () => { + const result = stats.formatDetailed('Tool-heavy task', baseTime + 15000); + + expect(result).toContain('Top tools:'); + expect(result).toContain('- file_read: 2 calls (2 ok, 0 fail'); + expect(result).toContain('- web_search: 1 calls (0 ok, 1 fail'); + expect(result).toContain('last error: Network timeout'); + }); + + it('should include performance insights', () => { + const result = stats.formatDetailed('Slow task', baseTime + 120000); + + expect(result).toContain('💡 Performance Insights:'); + expect(result).toContain( + 'Long execution time - consider breaking down complex tasks', + ); + }); + }); + + describe('quality categories', () => { + it('should categorize excellent execution', () => { + stats.recordToolCall('test', true, 100); + stats.recordToolCall('test', true, 100); + + const result = stats.formatDetailed('Perfect task'); + expect(result).toContain('Excellent execution (100.0% tool success)'); + }); + + it('should categorize good execution', () => { + // Need 85% success rate for "Good execution" - 17 success, 3 failures = 85% + for (let i = 0; i < 17; i++) { + stats.recordToolCall('test', true, 100); + } + for (let i = 0; i < 3; i++) { + stats.recordToolCall('test', false, 100); + } + + const result = stats.formatDetailed('Good task'); + expect(result).toContain('Good execution (85.0% tool success)'); + }); + + it('should categorize poor execution', () => { + stats.recordToolCall('test', false, 100); + stats.recordToolCall('test', false, 100); + + const result = stats.formatDetailed('Poor task'); + expect(result).toContain('Poor execution (0.0% tool success)'); + }); + }); + + describe('speed categories', () => { + it('should categorize fast completion', () => { + stats.start(baseTime); + const result = stats.formatDetailed('Fast task', baseTime + 5000); + expect(result).toContain('Fast completion - under 10 seconds'); + }); + + it('should categorize good speed', () => { + stats.start(baseTime); + const result = stats.formatDetailed('Medium task', baseTime + 30000); + expect(result).toContain('Good speed - under a minute'); + }); + + it('should categorize moderate duration', () => { + stats.start(baseTime); + const result = stats.formatDetailed('Slow task', baseTime + 120000); + expect(result).toContain('Moderate duration - a few minutes'); + }); + + it('should categorize long execution', () => { + stats.start(baseTime); + const result = stats.formatDetailed('Very slow task', baseTime + 600000); + expect(result).toContain('Long execution - consider breaking down tasks'); + }); + }); + + describe('performance tips', () => { + it('should suggest reviewing low success rate', () => { + stats.recordToolCall('test', false, 100); + stats.recordToolCall('test', false, 100); + stats.recordToolCall('test', true, 100); + + const result = stats.formatDetailed('Failing task'); + expect(result).toContain( + 'Low tool success rate - review inputs and error messages', + ); + }); + + it('should suggest breaking down long tasks', () => { + stats.start(baseTime); + + const result = stats.formatDetailed('Long task', baseTime + 120000); + expect(result).toContain( + 'Long execution time - consider breaking down complex tasks', + ); + }); + + it('should suggest optimizing high token usage', () => { + stats.recordTokens(80000, 30000); + + const result = stats.formatDetailed('Token-heavy task'); + expect(result).toContain( + 'High token usage - consider optimizing prompts or narrowing scope', + ); + }); + + it('should identify high token usage per call', () => { + stats.recordToolCall('test', true, 100); + stats.recordTokens(6000, 0); + + const result = stats.formatDetailed('Verbose task'); + expect(result).toContain( + 'High token usage per tool call (~6000 tokens/call)', + ); + }); + + it('should identify network failures', () => { + stats.recordToolCall('web_search', false, 100, 'Network timeout'); + + const result = stats.formatDetailed('Network task'); + expect(result).toContain( + 'Network operations had failures - consider increasing timeout or checking connectivity', + ); + }); + + it('should identify slow tools', () => { + stats.recordToolCall('slow_tool', true, 15000); + + const result = stats.formatDetailed('Slow tool task'); + expect(result).toContain( + 'Consider optimizing slow_tool operations (avg 15.0s)', + ); + }); + }); + + describe('duration formatting', () => { + it('should format milliseconds', () => { + stats.start(baseTime); + const result = stats.formatCompact('Quick task', baseTime + 500); + expect(result).toContain('500ms'); + }); + + it('should format seconds', () => { + stats.start(baseTime); + const result = stats.formatCompact('Second task', baseTime + 2500); + expect(result).toContain('2.5s'); + }); + + it('should format minutes and seconds', () => { + stats.start(baseTime); + const result = stats.formatCompact('Minute task', baseTime + 125000); + expect(result).toContain('2m 5s'); + }); + + it('should format hours and minutes', () => { + stats.start(baseTime); + const result = stats.formatCompact('Hour task', baseTime + 4500000); + expect(result).toContain('1h 15m'); + }); + }); +}); diff --git a/packages/core/src/subagents/subagent-statistics.ts b/packages/core/src/subagents/subagent-statistics.ts index 61064a93..3ef120c6 100644 --- a/packages/core/src/subagents/subagent-statistics.ts +++ b/packages/core/src/subagents/subagent-statistics.ts @@ -102,4 +102,149 @@ export class SubagentStatistics { toolUsage: Array.from(this.toolUsage.values()), }; } + + formatCompact(taskDesc: string, now = Date.now()): string { + const stats = this.getSummary(now); + const sr = + stats.totalToolCalls > 0 + ? (stats.successRate ?? + (stats.successfulToolCalls / stats.totalToolCalls) * 100) + : 0; + const lines = [ + `📋 Task Completed: ${taskDesc}`, + `🔧 Tool Usage: ${stats.totalToolCalls} calls${stats.totalToolCalls ? `, ${sr.toFixed(1)}% success` : ''}`, + `⏱️ Duration: ${this.fmtDuration(stats.totalDurationMs)} | 🔁 Rounds: ${stats.rounds}`, + ]; + if (typeof stats.totalTokens === 'number') { + lines.push( + `🔢 Tokens: ${stats.totalTokens.toLocaleString()}${stats.inputTokens || stats.outputTokens ? ` (in ${stats.inputTokens ?? 0}, out ${stats.outputTokens ?? 0})` : ''}`, + ); + } + return lines.join('\n'); + } + + formatDetailed(taskDesc: string, now = Date.now()): string { + const stats = this.getSummary(now); + const sr = + stats.totalToolCalls > 0 + ? (stats.successRate ?? + (stats.successfulToolCalls / stats.totalToolCalls) * 100) + : 0; + const lines: string[] = []; + lines.push(`📋 Task Completed: ${taskDesc}`); + lines.push( + `⏱️ Duration: ${this.fmtDuration(stats.totalDurationMs)} | 🔁 Rounds: ${stats.rounds}`, + ); + // Quality indicator + let quality = 'Poor execution'; + if (sr >= 95) quality = 'Excellent execution'; + else if (sr >= 85) quality = 'Good execution'; + else if (sr >= 70) quality = 'Fair execution'; + lines.push(`✅ Quality: ${quality} (${sr.toFixed(1)}% tool success)`); + // Speed category + const d = stats.totalDurationMs; + let speed = 'Long execution - consider breaking down tasks'; + if (d < 10_000) speed = 'Fast completion - under 10 seconds'; + else if (d < 60_000) speed = 'Good speed - under a minute'; + else if (d < 300_000) speed = 'Moderate duration - a few minutes'; + lines.push(`🚀 Speed: ${speed}`); + lines.push( + `🔧 Tools: ${stats.totalToolCalls} calls, ${sr.toFixed(1)}% success (${stats.successfulToolCalls} ok, ${stats.failedToolCalls} failed)`, + ); + if (typeof stats.totalTokens === 'number') { + lines.push( + `🔢 Tokens: ${stats.totalTokens.toLocaleString()} (in ${stats.inputTokens ?? 0}, out ${stats.outputTokens ?? 0})`, + ); + } + if (stats.toolUsage && stats.toolUsage.length) { + const sorted = [...stats.toolUsage] + .sort((a, b) => b.count - a.count) + .slice(0, 5); + lines.push('\nTop tools:'); + for (const t of sorted) { + const avg = + typeof t.averageDurationMs === 'number' + ? `, avg ${this.fmtDuration(Math.round(t.averageDurationMs))}` + : ''; + lines.push( + ` - ${t.name}: ${t.count} calls (${t.success} ok, ${t.failure} fail${avg}${t.lastError ? `, last error: ${t.lastError}` : ''})`, + ); + } + } + const tips = this.generatePerformanceTips(stats); + if (tips.length) { + lines.push('\n💡 Performance Insights:'); + for (const tip of tips.slice(0, 3)) lines.push(` - ${tip}`); + } + return lines.join('\n'); + } + + private fmtDuration(ms: number): string { + if (ms < 1000) return `${Math.round(ms)}ms`; + if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`; + if (ms < 3600000) { + const m = Math.floor(ms / 60000); + const s = Math.floor((ms % 60000) / 1000); + return `${m}m ${s}s`; + } + const h = Math.floor(ms / 3600000); + const m = Math.floor((ms % 3600000) / 60000); + return `${h}h ${m}m`; + } + + private generatePerformanceTips(stats: SubagentStatsSummary): string[] { + const tips: string[] = []; + const totalCalls = stats.totalToolCalls; + const sr = + stats.totalToolCalls > 0 + ? (stats.successRate ?? + (stats.successfulToolCalls / stats.totalToolCalls) * 100) + : 0; + + // High failure rate + if (sr < 80) + tips.push('Low tool success rate - review inputs and error messages'); + + // Long duration + if (stats.totalDurationMs > 60_000) + tips.push('Long execution time - consider breaking down complex tasks'); + + // Token usage + if (typeof stats.totalTokens === 'number' && stats.totalTokens > 100_000) { + tips.push( + 'High token usage - consider optimizing prompts or narrowing scope', + ); + } + if (typeof stats.totalTokens === 'number' && totalCalls > 0) { + const avgTokPerCall = stats.totalTokens / totalCalls; + if (avgTokPerCall > 5_000) + tips.push( + `High token usage per tool call (~${Math.round(avgTokPerCall)} tokens/call)`, + ); + } + + // Network failures + const isNetworkTool = (name: string) => /web|fetch|search/i.test(name); + const hadNetworkFailure = (stats.toolUsage || []).some( + (t) => + isNetworkTool(t.name) && + t.lastError && + /timeout|network/i.test(t.lastError), + ); + if (hadNetworkFailure) + tips.push( + 'Network operations had failures - consider increasing timeout or checking connectivity', + ); + + // Slow tools + const slow = (stats.toolUsage || []) + .filter((t) => (t.averageDurationMs ?? 0) > 10_000) + .sort((a, b) => (b.averageDurationMs ?? 0) - (a.averageDurationMs ?? 0)); + if (slow.length) + tips.push( + `Consider optimizing ${slow[0].name} operations (avg ${this.fmtDuration(Math.round(slow[0].averageDurationMs!))})`, + ); + + return tips; + } } diff --git a/packages/core/src/subagents/subagent.ts b/packages/core/src/subagents/subagent.ts index dfe4110a..1b435b9d 100644 --- a/packages/core/src/subagents/subagent.ts +++ b/packages/core/src/subagents/subagent.ts @@ -38,7 +38,6 @@ import { SubAgentStreamTextEvent, SubAgentErrorEvent, } from './subagent-events.js'; -import { formatCompact } from './subagent-result-format.js'; import { SubagentStatistics, SubagentStatsSummary, @@ -551,8 +550,7 @@ export class SubAgentScope { { terminate_reason: this.output.terminate_reason, result: this.finalText, - execution_summary: formatCompact( - summary, + execution_summary: this.stats.formatCompact( 'Subagent execution completed', ), }, diff --git a/packages/core/src/subagents/types.ts b/packages/core/src/subagents/types.ts index 1d696374..4f7b420f 100644 --- a/packages/core/src/subagents/types.ts +++ b/packages/core/src/subagents/types.ts @@ -10,8 +10,9 @@ import { Content, FunctionDeclaration } from '@google/genai'; * Represents the storage level for a subagent configuration. * - 'project': Stored in `.qwen/agents/` within the project directory * - 'user': Stored in `~/.qwen/agents/` in the user's home directory + * - 'builtin': Built-in agents embedded in the codebase, always available */ -export type SubagentLevel = 'project' | 'user'; +export type SubagentLevel = 'project' | 'user' | 'builtin'; /** * Core configuration for a subagent as stored in Markdown files. @@ -60,6 +61,12 @@ export interface SubagentConfig { * If 'auto' or omitted, uses automatic color assignment. */ color?: string; + + /** + * Indicates whether this is a built-in agent. + * Built-in agents cannot be modified or deleted. + */ + readonly isBuiltin?: boolean; } /** diff --git a/packages/core/src/tools/task.test.ts b/packages/core/src/tools/task.test.ts index ba7cf376..85688cbf 100644 --- a/packages/core/src/tools/task.test.ts +++ b/packages/core/src/tools/task.test.ts @@ -96,7 +96,7 @@ describe('TaskTool', () => { it('should initialize with correct name and properties', () => { expect(taskTool.name).toBe('task'); expect(taskTool.displayName).toBe('Task'); - expect(taskTool.kind).toBe('execute'); + expect(taskTool.kind).toBe('other'); }); it('should load available subagents during initialization', () => { diff --git a/packages/core/src/tools/task.ts b/packages/core/src/tools/task.ts index 904579a9..7a69bf2c 100644 --- a/packages/core/src/tools/task.ts +++ b/packages/core/src/tools/task.ts @@ -70,7 +70,7 @@ export class TaskTool extends BaseDeclarativeTool { TaskTool.Name, 'Task', 'Delegate tasks to specialized subagents. Loading available subagents...', // Initial description - Kind.Execute, + Kind.Other, initialSchema, true, // isOutputMarkdown true, // canUpdateOutput - Enable live output updates for real-time progress