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
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

View File

@@ -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();

View File

@@ -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 = ({
</Box>
<Text color={textColor} wrap="truncate">
{agent.name}
{agent.isBuiltin && (
<Text color={isSelected ? theme.text.accent : theme.text.secondary}>
{' '}
(built-in)
</Text>
)}
{agent.level === 'user' && projectNames.has(agent.name) && (
<Text color={isSelected ? theme.status.warning : Colors.Gray}>
{' '}
@@ -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 (
<Box flexDirection="column">
@@ -199,7 +279,10 @@ export const AgentSelectionStep = ({
{/* User Level Agents */}
{userAgents.length > 0 && (
<Box flexDirection="column">
<Box
flexDirection="column"
marginBottom={builtinAgents.length > 0 ? 1 : 0}
>
<Text color={theme.text.primary} bold>
User Level ({userAgents[0].filePath.replace(/\/[^/]+$/, '')})
</Text>
@@ -214,8 +297,27 @@ export const AgentSelectionStep = ({
</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 */}
{(projectAgents.length > 0 || userAgents.length > 0) && (
{(projectAgents.length > 0 ||
userAgents.length > 0 ||
builtinAgents.length > 0) && (
<Box marginTop={1}>
<Text color={theme.text.secondary}>
Using: {enabledAgentsCount} agents

View File

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

View File

@@ -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 <ActionSelectionStep {...commonProps} />;
return (
<ActionSelectionStep selectedAgent={selectedAgent} {...commonProps} />
);
case MANAGEMENT_STEPS.AGENT_VIEWER:
return (
<AgentViewerStep selectedAgent={selectedAgent} {...commonProps} />

View File

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

View File

@@ -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();

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';
// 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';

View File

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

View File

@@ -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<SubagentConfig | null> {
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<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
? [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 `<builtin:${name}>`;
}
const baseDir =
level === 'project'
? path.join(
@@ -626,6 +661,11 @@ export class SubagentManager {
private async listSubagentsAtLevel(
level: SubagentLevel,
): Promise<SubagentConfig[]> {
// Handle built-in agents
if (level === 'builtin') {
return BuiltinAgentRegistry.getBuiltinAgents();
}
const baseDir =
level === 'project'
? 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()),
};
}
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,
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',
),
},

View File

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

View File

@@ -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', () => {

View File

@@ -70,7 +70,7 @@ export class TaskTool extends BaseDeclarativeTool<TaskParams, ToolResult> {
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