mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
feat: add built-in agent(general-purpose)
This commit is contained in:
@@ -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
|
||||
```
|
||||
@@ -141,15 +146,12 @@ 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
|
||||
- Test-driven development practices
|
||||
@@ -157,6 +159,7 @@ Your expertise includes:
|
||||
- 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,8 +186,6 @@ 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}.
|
||||
@@ -192,6 +194,7 @@ 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,6 +210,7 @@ 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
|
||||
@@ -216,6 +221,7 @@ 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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
95
packages/core/src/subagents/builtin-agents.test.ts
Normal file
95
packages/core/src/subagents/builtin-agents.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
95
packages/core/src/subagents/builtin-agents.ts
Normal file
95
packages/core/src/subagents/builtin-agents.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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'),
|
||||
);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
309
packages/core/src/subagents/subagent-statistics.test.ts
Normal file
309
packages/core/src/subagents/subagent-statistics.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
),
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user