feat: add built-in agent(general-purpose)

This commit is contained in:
tanzhenxin
2025-09-09 20:53:53 +08:00
parent 3c67dc0c0b
commit 549f296eb5
19 changed files with 896 additions and 228 deletions

View File

@@ -32,15 +32,19 @@ Subagents are independent AI assistants that:
### Quick Start ### Quick Start
1. **Create your first subagent**: 1. **Create your first subagent**:
``` ```
/agents create /agents create
``` ```
Follow the guided wizard to create a specialized agent. Follow the guided wizard to create a specialized agent.
2. **List existing agents**: 2. **List existing agents**:
``` ```
/agents list /agents list
``` ```
View and manage your configured subagents. View and manage your configured subagents.
3. **Use subagents automatically**: 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] [Returns with completed test files and execution summary]
``` ```
## Management ## Management
### CLI Commands ### 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. Creates a new subagent through a guided step wizard.
**Usage:** **Usage:**
``` ```
/agents create /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. Opens an interactive management dialog for viewing and managing existing subagents.
**Usage:** **Usage:**
``` ```
/agents list /agents list
``` ```
@@ -101,7 +106,7 @@ Subagents are configured using Markdown files with YAML frontmatter. This format
--- ---
name: agent-name name: agent-name
description: Brief description of when and how to use this agent 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. System prompt content goes here.
@@ -141,15 +146,12 @@ Perfect for comprehensive test creation and test-driven development.
name: testing-expert name: testing-expert
description: Writes comprehensive unit tests, integration tests, and handles test automation with best practices 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 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. You are a testing specialist focused on creating high-quality, maintainable tests.
Your expertise includes: Your expertise includes:
- Unit testing with appropriate mocking and isolation - Unit testing with appropriate mocking and isolation
- Integration testing for component interactions - Integration testing for component interactions
- Test-driven development practices - Test-driven development practices
@@ -157,6 +159,7 @@ Your expertise includes:
- Performance and load testing when appropriate - Performance and load testing when appropriate
For each testing task: For each testing task:
1. Analyze the code structure and dependencies 1. Analyze the code structure and dependencies
2. Identify key functionality, edge cases, and error conditions 2. Identify key functionality, edge cases, and error conditions
3. Create comprehensive test suites with descriptive names 3. Create comprehensive test suites with descriptive names
@@ -169,6 +172,7 @@ Focus on both positive and negative test cases.
``` ```
**Use Cases:** **Use Cases:**
- "Write unit tests for the authentication service" - "Write unit tests for the authentication service"
- "Create integration tests for the payment processing workflow" - "Create integration tests for the payment processing workflow"
- "Add test coverage for edge cases in the data validation module" - "Add test coverage for edge cases in the data validation module"
@@ -182,8 +186,6 @@ Specialized in creating clear, comprehensive documentation.
name: documentation-writer name: documentation-writer
description: Creates comprehensive documentation, README files, API docs, and user guides description: Creates comprehensive documentation, README files, API docs, and user guides
tools: read_file, write_file, read_many_files, web_search tools: read_file, write_file, read_many_files, web_search
modelConfig:
temp: 0.4
--- ---
You are a technical documentation specialist for ${project_name}. You are a technical documentation specialist for ${project_name}.
@@ -192,6 +194,7 @@ Your role is to create clear, comprehensive documentation that serves both
developers and end users. Focus on: developers and end users. Focus on:
**For API Documentation:** **For API Documentation:**
- Clear endpoint descriptions with examples - Clear endpoint descriptions with examples
- Parameter details with types and constraints - Parameter details with types and constraints
- Response format documentation - Response format documentation
@@ -199,6 +202,7 @@ developers and end users. Focus on:
- Authentication requirements - Authentication requirements
**For User Documentation:** **For User Documentation:**
- Step-by-step instructions with screenshots when helpful - Step-by-step instructions with screenshots when helpful
- Installation and setup guides - Installation and setup guides
- Configuration options and examples - Configuration options and examples
@@ -206,6 +210,7 @@ developers and end users. Focus on:
- FAQ sections based on common user questions - FAQ sections based on common user questions
**For Developer Documentation:** **For Developer Documentation:**
- Architecture overviews and design decisions - Architecture overviews and design decisions
- Code examples that actually work - Code examples that actually work
- Contributing guidelines - Contributing guidelines
@@ -216,6 +221,7 @@ the actual implementation. Use clear headings, bullet points, and examples.
``` ```
**Use Cases:** **Use Cases:**
- "Create API documentation for the user management endpoints" - "Create API documentation for the user management endpoints"
- "Write a comprehensive README for this project" - "Write a comprehensive README for this project"
- "Document the deployment process with troubleshooting steps" - "Document the deployment process with troubleshooting steps"
@@ -229,15 +235,12 @@ Focused on code quality, security, and best practices.
name: code-reviewer name: code-reviewer
description: Reviews code for best practices, security issues, performance, and maintainability description: Reviews code for best practices, security issues, performance, and maintainability
tools: read_file, read_many_files 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. You are an experienced code reviewer focused on quality, security, and maintainability.
Review criteria: Review criteria:
- **Code Structure**: Organization, modularity, and separation of concerns - **Code Structure**: Organization, modularity, and separation of concerns
- **Performance**: Algorithmic efficiency and resource usage - **Performance**: Algorithmic efficiency and resource usage
- **Security**: Vulnerability assessment and secure coding practices - **Security**: Vulnerability assessment and secure coding practices
@@ -247,6 +250,7 @@ Review criteria:
- **Testing**: Test coverage and testability considerations - **Testing**: Test coverage and testability considerations
Provide constructive feedback with: Provide constructive feedback with:
1. **Critical Issues**: Security vulnerabilities, major bugs 1. **Critical Issues**: Security vulnerabilities, major bugs
2. **Important Improvements**: Performance issues, design problems 2. **Important Improvements**: Performance issues, design problems
3. **Minor Suggestions**: Style improvements, refactoring opportunities 3. **Minor Suggestions**: Style improvements, refactoring opportunities
@@ -257,6 +261,7 @@ Prioritize issues by impact and provide rationale for recommendations.
``` ```
**Use Cases:** **Use Cases:**
- "Review this authentication implementation for security issues" - "Review this authentication implementation for security issues"
- "Check the performance implications of this database query logic" - "Check the performance implications of this database query logic"
- "Evaluate the code structure and suggest improvements" - "Evaluate the code structure and suggest improvements"
@@ -272,13 +277,12 @@ Optimized for React development, hooks, and component patterns.
name: react-specialist name: react-specialist
description: Expert in React development, hooks, component patterns, and modern React best practices description: Expert in React development, hooks, component patterns, and modern React best practices
tools: read_file, write_file, read_many_files, run_shell_command 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. You are a React specialist with deep expertise in modern React development.
Your expertise covers: Your expertise covers:
- **Component Design**: Functional components, custom hooks, composition patterns - **Component Design**: Functional components, custom hooks, composition patterns
- **State Management**: useState, useReducer, Context API, and external libraries - **State Management**: useState, useReducer, Context API, and external libraries
- **Performance**: React.memo, useMemo, useCallback, code splitting - **Performance**: React.memo, useMemo, useCallback, code splitting
@@ -287,6 +291,7 @@ Your expertise covers:
- **Modern Patterns**: Suspense, Error Boundaries, Concurrent Features - **Modern Patterns**: Suspense, Error Boundaries, Concurrent Features
For React tasks: For React tasks:
1. Use functional components and hooks by default 1. Use functional components and hooks by default
2. Implement proper TypeScript typing 2. Implement proper TypeScript typing
3. Follow React best practices and conventions 3. Follow React best practices and conventions
@@ -299,6 +304,7 @@ Focus on accessibility and user experience considerations.
``` ```
**Use Cases:** **Use Cases:**
- "Create a reusable data table component with sorting and filtering" - "Create a reusable data table component with sorting and filtering"
- "Implement a custom hook for API data fetching with caching" - "Implement a custom hook for API data fetching with caching"
- "Refactor this class component to use modern React patterns" - "Refactor this class component to use modern React patterns"
@@ -312,13 +318,12 @@ Specialized in Python development, frameworks, and best practices.
name: python-expert name: python-expert
description: Expert in Python development, frameworks, testing, and Python-specific best practices description: Expert in Python development, frameworks, testing, and Python-specific best practices
tools: read_file, write_file, read_many_files, run_shell_command 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. You are a Python expert with deep knowledge of the Python ecosystem.
Your expertise includes: Your expertise includes:
- **Core Python**: Pythonic patterns, data structures, algorithms - **Core Python**: Pythonic patterns, data structures, algorithms
- **Frameworks**: Django, Flask, FastAPI, SQLAlchemy - **Frameworks**: Django, Flask, FastAPI, SQLAlchemy
- **Testing**: pytest, unittest, mocking, test-driven development - **Testing**: pytest, unittest, mocking, test-driven development
@@ -328,6 +333,7 @@ Your expertise includes:
- **Code Quality**: PEP 8, type hints, linting with pylint/flake8 - **Code Quality**: PEP 8, type hints, linting with pylint/flake8
For Python tasks: For Python tasks:
1. Follow PEP 8 style guidelines 1. Follow PEP 8 style guidelines
2. Use type hints for better code documentation 2. Use type hints for better code documentation
3. Implement proper error handling with specific exceptions 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:** **Use Cases:**
- "Create a FastAPI service for user authentication with JWT tokens" - "Create a FastAPI service for user authentication with JWT tokens"
- "Implement a data processing pipeline with pandas and error handling" - "Implement a data processing pipeline with pandas and error handling"
- "Write a CLI tool using argparse with comprehensive help documentation" - "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. Each subagent should have a clear, focused purpose.
**✅ Good:** **✅ Good:**
```markdown ```markdown
--- ---
name: testing-expert name: testing-expert
@@ -361,6 +369,7 @@ description: Writes comprehensive unit tests and integration tests
``` ```
**❌ Avoid:** **❌ Avoid:**
```markdown ```markdown
--- ---
name: general-helper name: general-helper
@@ -375,6 +384,7 @@ description: Helps with testing, documentation, code review, and deployment
Define specific expertise areas rather than broad capabilities. Define specific expertise areas rather than broad capabilities.
**✅ Good:** **✅ Good:**
```markdown ```markdown
--- ---
name: react-performance-optimizer name: react-performance-optimizer
@@ -383,6 +393,7 @@ description: Optimizes React applications for performance using profiling and be
``` ```
**❌ Avoid:** **❌ Avoid:**
```markdown ```markdown
--- ---
name: frontend-developer name: frontend-developer
@@ -397,11 +408,13 @@ description: Works on frontend development tasks
Write descriptions that clearly indicate when to use the agent. Write descriptions that clearly indicate when to use the agent.
**✅ Good:** **✅ Good:**
```markdown ```markdown
description: Reviews code for security vulnerabilities, performance issues, and maintainability concerns description: Reviews code for security vulnerabilities, performance issues, and maintainability concerns
``` ```
**❌ Avoid:** **❌ Avoid:**
```markdown ```markdown
description: A helpful code reviewer description: A helpful code reviewer
``` ```
@@ -413,8 +426,10 @@ description: A helpful code reviewer
#### System Prompt Guidelines #### System Prompt Guidelines
**Be Specific About Expertise:** **Be Specific About Expertise:**
```markdown ```markdown
You are a Python testing specialist with expertise in: You are a Python testing specialist with expertise in:
- pytest framework and fixtures - pytest framework and fixtures
- Mock objects and dependency injection - Mock objects and dependency injection
- Test-driven development practices - Test-driven development practices
@@ -422,8 +437,10 @@ You are a Python testing specialist with expertise in:
``` ```
**Include Step-by-Step Approaches:** **Include Step-by-Step Approaches:**
```markdown ```markdown
For each testing task: For each testing task:
1. Analyze the code structure and dependencies 1. Analyze the code structure and dependencies
2. Identify key functionality and edge cases 2. Identify key functionality and edge cases
3. Create comprehensive test suites with clear naming 3. Create comprehensive test suites with clear naming
@@ -432,8 +449,10 @@ For each testing task:
``` ```
**Specify Output Standards:** **Specify Output Standards:**
```markdown ```markdown
Always follow these standards: Always follow these standards:
- Use descriptive test names that explain the scenario - Use descriptive test names that explain the scenario
- Include both positive and negative test cases - Include both positive and negative test cases
- Add docstrings for complex test functions - Add docstrings for complex test functions

View File

@@ -8,26 +8,37 @@ import { useState } from 'react';
import { Box } from 'ink'; import { Box } from 'ink';
import { RadioButtonSelect } from '../shared/RadioButtonSelect.js'; import { RadioButtonSelect } from '../shared/RadioButtonSelect.js';
import { MANAGEMENT_STEPS } from './types.js'; import { MANAGEMENT_STEPS } from './types.js';
import { SubagentConfig } from '@qwen-code/qwen-code-core';
interface ActionSelectionStepProps { interface ActionSelectionStepProps {
selectedAgent: SubagentConfig | null;
onNavigateToStep: (step: string) => void; onNavigateToStep: (step: string) => void;
onNavigateBack: () => void; onNavigateBack: () => void;
} }
export const ActionSelectionStep = ({ export const ActionSelectionStep = ({
selectedAgent,
onNavigateToStep, onNavigateToStep,
onNavigateBack, onNavigateBack,
}: ActionSelectionStepProps) => { }: ActionSelectionStepProps) => {
const [selectedAction, setSelectedAction] = useState< const [selectedAction, setSelectedAction] = useState<
'view' | 'edit' | 'delete' | null 'view' | 'edit' | 'delete' | null
>(null); >(null);
const actions = [
// Filter actions based on whether the agent is built-in
const allActions = [
{ label: 'View Agent', value: 'view' as const }, { label: 'View Agent', value: 'view' as const },
{ label: 'Edit Agent', value: 'edit' as const }, { label: 'Edit Agent', value: 'edit' as const },
{ label: 'Delete Agent', value: 'delete' as const }, { label: 'Delete Agent', value: 'delete' as const },
{ label: 'Back', value: 'back' 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') => { const handleActionSelect = (value: 'view' | 'edit' | 'delete' | 'back') => {
if (value === 'back') { if (value === 'back') {
onNavigateBack(); onNavigateBack();

View File

@@ -12,9 +12,10 @@ import { useKeypress } from '../../hooks/useKeypress.js';
import { SubagentConfig } from '@qwen-code/qwen-code-core'; import { SubagentConfig } from '@qwen-code/qwen-code-core';
interface NavigationState { interface NavigationState {
currentBlock: 'project' | 'user'; currentBlock: 'project' | 'user' | 'builtin';
projectIndex: number; projectIndex: number;
userIndex: number; userIndex: number;
builtinIndex: number;
} }
interface AgentSelectionStepProps { interface AgentSelectionStepProps {
@@ -30,6 +31,7 @@ export const AgentSelectionStep = ({
currentBlock: 'project', currentBlock: 'project',
projectIndex: 0, projectIndex: 0,
userIndex: 0, userIndex: 0,
builtinIndex: 0,
}); });
// Group agents by level // Group agents by level
@@ -41,6 +43,10 @@ export const AgentSelectionStep = ({
() => availableAgents.filter((agent) => agent.level === 'user'), () => availableAgents.filter((agent) => agent.level === 'user'),
[availableAgents], [availableAgents],
); );
const builtinAgents = useMemo(
() => availableAgents.filter((agent) => agent.level === 'builtin'),
[availableAgents],
);
const projectNames = useMemo( const projectNames = useMemo(
() => new Set(projectAgents.map((agent) => agent.name)), () => new Set(projectAgents.map((agent) => agent.name)),
[projectAgents], [projectAgents],
@@ -52,8 +58,10 @@ export const AgentSelectionStep = ({
setNavigation((prev) => ({ ...prev, currentBlock: 'project' })); setNavigation((prev) => ({ ...prev, currentBlock: 'project' }));
} else if (userAgents.length > 0) { } else if (userAgents.length > 0) {
setNavigation((prev) => ({ ...prev, currentBlock: 'user' })); setNavigation((prev) => ({ ...prev, currentBlock: 'user' }));
} else if (builtinAgents.length > 0) {
setNavigation((prev) => ({ ...prev, currentBlock: 'builtin' }));
} }
}, [projectAgents, userAgents]); }, [projectAgents, userAgents, builtinAgents]);
// Custom keyboard navigation // Custom keyboard navigation
useKeypress( useKeypress(
@@ -65,6 +73,13 @@ export const AgentSelectionStep = ({
if (prev.currentBlock === 'project') { if (prev.currentBlock === 'project') {
if (prev.projectIndex > 0) { if (prev.projectIndex > 0) {
return { ...prev, projectIndex: prev.projectIndex - 1 }; 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) { } else if (userAgents.length > 0) {
// Move to last item in user block // Move to last item in user block
return { return {
@@ -76,7 +91,7 @@ export const AgentSelectionStep = ({
// Wrap to last item in project block // Wrap to last item in project block
return { ...prev, projectIndex: projectAgents.length - 1 }; return { ...prev, projectIndex: projectAgents.length - 1 };
} }
} else { } else if (prev.currentBlock === 'user') {
if (prev.userIndex > 0) { if (prev.userIndex > 0) {
return { ...prev, userIndex: prev.userIndex - 1 }; return { ...prev, userIndex: prev.userIndex - 1 };
} else if (projectAgents.length > 0) { } else if (projectAgents.length > 0) {
@@ -86,10 +101,39 @@ export const AgentSelectionStep = ({
currentBlock: 'project', currentBlock: 'project',
projectIndex: projectAgents.length - 1, 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 { } else {
// Wrap to last item in user block // Wrap to last item in user block
return { ...prev, userIndex: userAgents.length - 1 }; 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') { } else if (name === 'down' || name === 'j') {
@@ -100,13 +144,19 @@ export const AgentSelectionStep = ({
} else if (userAgents.length > 0) { } else if (userAgents.length > 0) {
// Move to first item in user block // Move to first item in user block
return { ...prev, currentBlock: 'user', userIndex: 0 }; 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 { } else {
// Wrap to first item in project block // Wrap to first item in project block
return { ...prev, projectIndex: 0 }; return { ...prev, projectIndex: 0 };
} }
} else { } else if (prev.currentBlock === 'user') {
if (prev.userIndex < userAgents.length - 1) { if (prev.userIndex < userAgents.length - 1) {
return { ...prev, userIndex: prev.userIndex + 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) { } else if (projectAgents.length > 0) {
// Move to first item in project block // Move to first item in project block
return { ...prev, currentBlock: 'project', projectIndex: 0 }; return { ...prev, currentBlock: 'project', projectIndex: 0 };
@@ -114,6 +164,20 @@ export const AgentSelectionStep = ({
// Wrap to first item in user block // Wrap to first item in user block
return { ...prev, userIndex: 0 }; 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') { } else if (name === 'return' || name === 'space') {
@@ -121,9 +185,14 @@ export const AgentSelectionStep = ({
let globalIndex: number; let globalIndex: number;
if (navigation.currentBlock === 'project') { if (navigation.currentBlock === 'project') {
globalIndex = navigation.projectIndex; globalIndex = navigation.projectIndex;
} else { } else if (navigation.currentBlock === 'user') {
// User agents come after project agents in the availableAgents array // User agents come after project agents in the availableAgents array
globalIndex = projectAgents.length + navigation.userIndex; 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) { if (globalIndex >= 0 && globalIndex < availableAgents.length) {
@@ -147,7 +216,11 @@ export const AgentSelectionStep = ({
// Render custom radio button items // Render custom radio button items
const renderAgentItem = ( const renderAgentItem = (
agent: { name: string; level: 'project' | 'user' }, agent: {
name: string;
level: 'project' | 'user' | 'builtin';
isBuiltin?: boolean;
},
index: number, index: number,
isSelected: boolean, isSelected: boolean,
) => { ) => {
@@ -162,6 +235,12 @@ export const AgentSelectionStep = ({
</Box> </Box>
<Text color={textColor} wrap="truncate"> <Text color={textColor} wrap="truncate">
{agent.name} {agent.name}
{agent.isBuiltin && (
<Text color={isSelected ? theme.text.accent : theme.text.secondary}>
{' '}
(built-in)
</Text>
)}
{agent.level === 'user' && projectNames.has(agent.name) && ( {agent.level === 'user' && projectNames.has(agent.name) && (
<Text color={isSelected ? theme.status.warning : Colors.Gray}> <Text color={isSelected ? theme.status.warning : Colors.Gray}>
{' '} {' '}
@@ -176,7 +255,8 @@ export const AgentSelectionStep = ({
// Calculate enabled agents count (excluding conflicted user-level agents) // Calculate enabled agents count (excluding conflicted user-level agents)
const enabledAgentsCount = const enabledAgentsCount =
projectAgents.length + projectAgents.length +
userAgents.filter((agent) => !projectNames.has(agent.name)).length; userAgents.filter((agent) => !projectNames.has(agent.name)).length +
builtinAgents.length;
return ( return (
<Box flexDirection="column"> <Box flexDirection="column">
@@ -199,7 +279,10 @@ export const AgentSelectionStep = ({
{/* User Level Agents */} {/* User Level Agents */}
{userAgents.length > 0 && ( {userAgents.length > 0 && (
<Box flexDirection="column"> <Box
flexDirection="column"
marginBottom={builtinAgents.length > 0 ? 1 : 0}
>
<Text color={theme.text.primary} bold> <Text color={theme.text.primary} bold>
User Level ({userAgents[0].filePath.replace(/\/[^/]+$/, '')}) User Level ({userAgents[0].filePath.replace(/\/[^/]+$/, '')})
</Text> </Text>
@@ -214,8 +297,27 @@ export const AgentSelectionStep = ({
</Box> </Box>
)} )}
{/* Built-in Agents */}
{builtinAgents.length > 0 && (
<Box flexDirection="column">
<Text color={theme.text.primary} bold>
Built-in Agents
</Text>
<Box marginTop={1} flexDirection="column">
{builtinAgents.map((agent, index) => {
const isSelected =
navigation.currentBlock === 'builtin' &&
navigation.builtinIndex === index;
return renderAgentItem(agent, index, isSelected);
})}
</Box>
</Box>
)}
{/* Agent count summary */} {/* Agent count summary */}
{(projectAgents.length > 0 || userAgents.length > 0) && ( {(projectAgents.length > 0 ||
userAgents.length > 0 ||
builtinAgents.length > 0) && (
<Box marginTop={1}> <Box marginTop={1}>
<Text color={theme.text.secondary}> <Text color={theme.text.secondary}>
Using: {enabledAgentsCount} agents Using: {enabledAgentsCount} agents

View File

@@ -29,15 +29,6 @@ export const AgentViewerStep = ({ selectedAgent }: AgentViewerStepProps) => {
return ( return (
<Box flexDirection="column" gap={1}> <Box flexDirection="column" gap={1}>
<Box flexDirection="column"> <Box flexDirection="column">
<Box>
<Text bold>Location: </Text>
<Text>
{agent.level === 'project'
? 'Project Level (.qwen/agents/)'
: 'User Level (~/.qwen/agents/)'}
</Text>
</Box>
<Box> <Box>
<Text bold>File Path: </Text> <Text bold>File Path: </Text>
<Text>{agent.filePath}</Text> <Text>{agent.filePath}</Text>

View File

@@ -50,14 +50,19 @@ export function AgentsManagerDialog({
const manager = config.getSubagentManager(); const manager = config.getSubagentManager();
// Load agents from both levels separately to show all agents including conflicts // Load agents from all levels separately to show all agents including conflicts
const [projectAgents, userAgents] = await Promise.all([ const [projectAgents, userAgents, builtinAgents] = await Promise.all([
manager.listSubagents({ level: 'project' }), manager.listSubagents({ level: 'project' }),
manager.listSubagents({ level: 'user' }), manager.listSubagents({ level: 'user' }),
manager.listSubagents({ level: 'builtin' }),
]); ]);
// Combine all agents (project and user level) // Combine all agents (project, user, and builtin level)
const allAgents = [...(projectAgents || []), ...(userAgents || [])]; const allAgents = [
...(projectAgents || []),
...(userAgents || []),
...(builtinAgents || []),
];
setAvailableAgents(allAgents); setAvailableAgents(allAgents);
}, [config]); }, [config]);
@@ -208,7 +213,9 @@ export function AgentsManagerDialog({
/> />
); );
case MANAGEMENT_STEPS.ACTION_SELECTION: case MANAGEMENT_STEPS.ACTION_SELECTION:
return <ActionSelectionStep {...commonProps} />; return (
<ActionSelectionStep selectedAgent={selectedAgent} {...commonProps} />
);
case MANAGEMENT_STEPS.AGENT_VIEWER: case MANAGEMENT_STEPS.AGENT_VIEWER:
return ( return (
<AgentViewerStep selectedAgent={selectedAgent} {...commonProps} /> <AgentViewerStep selectedAgent={selectedAgent} {...commonProps} />

View File

@@ -289,9 +289,7 @@ const ToolCallItem: React.FC<{
<Box flexDirection="row"> <Box flexDirection="row">
<Box minWidth={STATUS_INDICATOR_WIDTH}>{statusIcon}</Box> <Box minWidth={STATUS_INDICATOR_WIDTH}>{statusIcon}</Box>
<Text wrap="truncate-end"> <Text wrap="truncate-end">
<Text> <Text>{toolCall.name}</Text>{' '}
{toolCall.name}
</Text>{' '}
<Text color={Colors.Gray}>{description}</Text> <Text color={Colors.Gray}>{description}</Text>
{toolCall.error && ( {toolCall.error && (
<Text color={theme.status.error}> - {toolCall.error}</Text> <Text color={theme.status.error}> - {toolCall.error}</Text>

View File

@@ -65,7 +65,8 @@ export function ToolSelector({
(tool) => (tool) =>
tool.kind === Kind.Read || tool.kind === Kind.Read ||
tool.kind === Kind.Search || tool.kind === Kind.Search ||
tool.kind === Kind.Fetch, tool.kind === Kind.Fetch ||
tool.kind === Kind.Think,
) )
.map((tool) => tool.displayName) .map((tool) => tool.displayName)
.sort(); .sort();
@@ -75,8 +76,7 @@ export function ToolSelector({
(tool) => (tool) =>
tool.kind === Kind.Edit || tool.kind === Kind.Edit ||
tool.kind === Kind.Delete || tool.kind === Kind.Delete ||
tool.kind === Kind.Move || tool.kind === Kind.Move,
tool.kind === Kind.Think,
) )
.map((tool) => tool.displayName) .map((tool) => tool.displayName)
.sort(); .sort();

View File

@@ -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: `<builtin:${agent.name}>`,
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: '<builtin:general-purpose>',
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();
});
});
});
});

View File

@@ -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<SubagentConfig, 'level' | 'filePath'>
> = [
{
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: `<builtin:${agent.name}>`,
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: `<builtin:${name}>`,
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);
}
}

View File

@@ -33,6 +33,9 @@ export type {
export { SubagentError } from './types.js'; export { SubagentError } from './types.js';
// Built-in agents registry
export { BuiltinAgentRegistry } from './builtin-agents.js';
// Validation system // Validation system
export { SubagentValidator } from './validation.js'; export { SubagentValidator } from './validation.js';
@@ -70,4 +73,3 @@ export type {
SubagentStatsSummary, SubagentStatsSummary,
ToolUsageStats, ToolUsageStats,
} from './subagent-statistics.js'; } from './subagent-statistics.js';
export { formatCompact, formatDetailed } from './subagent-result-format.js';

View File

@@ -584,11 +584,12 @@ System prompt 3`);
it('should list subagents from both levels', async () => { it('should list subagents from both levels', async () => {
const subagents = await manager.listSubagents(); 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([ expect(subagents.map((s) => s.name)).toEqual([
'agent1', 'agent1',
'agent2', 'agent2',
'agent3', 'agent3',
'general-purpose',
]); ]);
}); });
@@ -615,7 +616,7 @@ System prompt 3`);
}); });
const names = subagents.map((s) => s.name); 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 () => { it('should handle empty directories', async () => {
@@ -626,7 +627,9 @@ System prompt 3`);
const subagents = await manager.listSubagents(); 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 () => { it('should handle directory read errors', async () => {
@@ -636,7 +639,9 @@ System prompt 3`);
const subagents = await manager.listSubagents(); 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 () => { it('should skip invalid subagent files', async () => {
@@ -656,7 +661,7 @@ System prompt 3`);
const subagents = await manager.listSubagents(); const subagents = await manager.listSubagents();
expect(subagents).toHaveLength(1); expect(subagents).toHaveLength(2); // 1 valid file + 1 built-in agent
expect(consoleSpy).toHaveBeenCalledWith( expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining('Skipping invalid subagent file'), expect.stringContaining('Skipping invalid subagent file'),
); );

View File

@@ -29,6 +29,7 @@ import {
import { SubagentValidator } from './validation.js'; import { SubagentValidator } from './validation.js';
import { SubAgentScope } from './subagent.js'; import { SubAgentScope } from './subagent.js';
import { Config } from '../config/config.js'; import { Config } from '../config/config.js';
import { BuiltinAgentRegistry } from './builtin-agents.js';
const QWEN_CONFIG_DIR = '.qwen'; const QWEN_CONFIG_DIR = '.qwen';
const AGENT_CONFIG_DIR = 'agents'; const AGENT_CONFIG_DIR = 'agents';
@@ -104,7 +105,7 @@ export class SubagentManager {
/** /**
* Loads a subagent configuration by name. * Loads a subagent configuration by name.
* If level is specified, only searches that level. * 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 name - Name of the subagent to load
* @param level - Optional level to limit search to specific level * @param level - Optional level to limit search to specific level
@@ -116,6 +117,10 @@ export class SubagentManager {
): Promise<SubagentConfig | null> { ): Promise<SubagentConfig | null> {
if (level) { if (level) {
// Search only the specified level // Search only the specified level
if (level === 'builtin') {
return BuiltinAgentRegistry.getBuiltinAgent(name);
}
const path = this.getSubagentPath(name, level); const path = this.getSubagentPath(name, level);
try { try {
const config = await this.parseSubagentFile(path); const config = await this.parseSubagentFile(path);
@@ -140,9 +145,11 @@ export class SubagentManager {
const config = await this.parseSubagentFile(userPath); const config = await this.parseSubagentFile(userPath);
return config; return config;
} catch (_error) { } catch (_error) {
// Not found at either level // Continue to built-in agents
return null;
} }
// 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 // Merge updates with existing configuration
const updatedConfig = this.mergeConfigurations(existing, updates); const updatedConfig = this.mergeConfigurations(existing, updates);
@@ -194,12 +210,26 @@ export class SubagentManager {
* @throws SubagentError if deletion fails * @throws SubagentError if deletion fails
*/ */
async deleteSubagent(name: string, level?: SubagentLevel): Promise<void> { async deleteSubagent(name: string, level?: SubagentLevel): Promise<void> {
// 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 const levelsToCheck: SubagentLevel[] = level
? [level] ? [level]
: ['project', 'user']; : ['project', 'user'];
let deleted = false; let deleted = false;
for (const currentLevel of levelsToCheck) { for (const currentLevel of levelsToCheck) {
// Skip builtin level for deletion
if (currentLevel === 'builtin') {
continue;
}
const filePath = this.getSubagentPath(name, currentLevel); const filePath = this.getSubagentPath(name, currentLevel);
try { try {
@@ -233,14 +263,14 @@ export class SubagentManager {
const levelsToCheck: SubagentLevel[] = options.level const levelsToCheck: SubagentLevel[] = options.level
? [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) { for (const level of levelsToCheck) {
const levelSubagents = await this.listSubagentsAtLevel(level); const levelSubagents = await this.listSubagentsAtLevel(level);
for (const subagent of levelSubagents) { 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)) { if (seenNames.has(subagent.name)) {
continue; continue;
} }
@@ -267,11 +297,12 @@ export class SubagentManager {
case 'name': case 'name':
comparison = a.name.localeCompare(b.name); comparison = a.name.localeCompare(b.name);
break; break;
case 'level': case 'level': {
// Project comes before user // Project comes before user, user comes before builtin
comparison = const levelOrder = { project: 0, user: 1, builtin: 2 };
a.level === 'project' ? -1 : b.level === 'project' ? 1 : 0; comparison = levelOrder[a.level] - levelOrder[b.level];
break; break;
}
default: default:
comparison = 0; comparison = 0;
break; break;
@@ -605,6 +636,10 @@ export class SubagentManager {
* @returns Absolute file path * @returns Absolute file path
*/ */
getSubagentPath(name: string, level: SubagentLevel): string { getSubagentPath(name: string, level: SubagentLevel): string {
if (level === 'builtin') {
return `<builtin:${name}>`;
}
const baseDir = const baseDir =
level === 'project' level === 'project'
? path.join( ? path.join(
@@ -626,6 +661,11 @@ export class SubagentManager {
private async listSubagentsAtLevel( private async listSubagentsAtLevel(
level: SubagentLevel, level: SubagentLevel,
): Promise<SubagentConfig[]> { ): Promise<SubagentConfig[]> {
// Handle built-in agents
if (level === 'builtin') {
return BuiltinAgentRegistry.getBuiltinAgents();
}
const baseDir = const baseDir =
level === 'project' level === 'project'
? path.join( ? path.join(

View File

@@ -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;
}

View File

@@ -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');
});
});
});

View File

@@ -102,4 +102,149 @@ export class SubagentStatistics {
toolUsage: Array.from(this.toolUsage.values()), 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;
}
} }

View File

@@ -38,7 +38,6 @@ import {
SubAgentStreamTextEvent, SubAgentStreamTextEvent,
SubAgentErrorEvent, SubAgentErrorEvent,
} from './subagent-events.js'; } from './subagent-events.js';
import { formatCompact } from './subagent-result-format.js';
import { import {
SubagentStatistics, SubagentStatistics,
SubagentStatsSummary, SubagentStatsSummary,
@@ -551,8 +550,7 @@ export class SubAgentScope {
{ {
terminate_reason: this.output.terminate_reason, terminate_reason: this.output.terminate_reason,
result: this.finalText, result: this.finalText,
execution_summary: formatCompact( execution_summary: this.stats.formatCompact(
summary,
'Subagent execution completed', 'Subagent execution completed',
), ),
}, },

View File

@@ -10,8 +10,9 @@ import { Content, FunctionDeclaration } from '@google/genai';
* Represents the storage level for a subagent configuration. * Represents the storage level for a subagent configuration.
* - 'project': Stored in `.qwen/agents/` within the project directory * - 'project': Stored in `.qwen/agents/` within the project directory
* - 'user': Stored in `~/.qwen/agents/` in the user's home 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. * 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. * If 'auto' or omitted, uses automatic color assignment.
*/ */
color?: string; color?: string;
/**
* Indicates whether this is a built-in agent.
* Built-in agents cannot be modified or deleted.
*/
readonly isBuiltin?: boolean;
} }
/** /**

View File

@@ -96,7 +96,7 @@ describe('TaskTool', () => {
it('should initialize with correct name and properties', () => { it('should initialize with correct name and properties', () => {
expect(taskTool.name).toBe('task'); expect(taskTool.name).toBe('task');
expect(taskTool.displayName).toBe('Task'); expect(taskTool.displayName).toBe('Task');
expect(taskTool.kind).toBe('execute'); expect(taskTool.kind).toBe('other');
}); });
it('should load available subagents during initialization', () => { it('should load available subagents during initialization', () => {

View File

@@ -70,7 +70,7 @@ export class TaskTool extends BaseDeclarativeTool<TaskParams, ToolResult> {
TaskTool.Name, TaskTool.Name,
'Task', 'Task',
'Delegate tasks to specialized subagents. Loading available subagents...', // Initial description 'Delegate tasks to specialized subagents. Loading available subagents...', // Initial description
Kind.Execute, Kind.Other,
initialSchema, initialSchema,
true, // isOutputMarkdown true, // isOutputMarkdown
true, // canUpdateOutput - Enable live output updates for real-time progress true, // canUpdateOutput - Enable live output updates for real-time progress