mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-20 16:57:46 +00:00
Merge branch 'main' into chore/sync-gemini-cli-v0.3.4
This commit is contained in:
@@ -207,7 +207,7 @@ describe('MemoryTool', () => {
|
||||
|
||||
it('should have correct name, displayName, description, and schema', () => {
|
||||
expect(memoryTool.name).toBe('save_memory');
|
||||
expect(memoryTool.displayName).toBe('Save Memory');
|
||||
expect(memoryTool.displayName).toBe('SaveMemory');
|
||||
expect(memoryTool.description).toContain(
|
||||
'Saves a specific piece of information',
|
||||
);
|
||||
|
||||
@@ -395,7 +395,7 @@ export class MemoryTool
|
||||
constructor() {
|
||||
super(
|
||||
MemoryTool.Name,
|
||||
'Save Memory',
|
||||
'SaveMemory',
|
||||
memoryToolDescription,
|
||||
Kind.Think,
|
||||
memoryToolSchemaData.parametersJsonSchema as Record<string, unknown>,
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
processSingleFileContent,
|
||||
DEFAULT_ENCODING,
|
||||
getSpecificMimeType,
|
||||
DEFAULT_MAX_LINES_TEXT_FILE,
|
||||
} from '../utils/fileUtils.js';
|
||||
import type { PartListUnion } from '@google/genai';
|
||||
import type { Config } from '../config/config.js';
|
||||
@@ -322,6 +323,8 @@ ${finalExclusionPatternsForDescription
|
||||
}
|
||||
|
||||
const sortedFiles = Array.from(filesToConsider).sort();
|
||||
const file_line_limit =
|
||||
DEFAULT_MAX_LINES_TEXT_FILE / Math.max(1, sortedFiles.length);
|
||||
|
||||
const fileProcessingPromises = sortedFiles.map(
|
||||
async (filePath): Promise<FileProcessingResult> => {
|
||||
@@ -360,6 +363,8 @@ ${finalExclusionPatternsForDescription
|
||||
filePath,
|
||||
this.config.getTargetDir(),
|
||||
this.config.getFileSystemService(),
|
||||
0,
|
||||
file_line_limit,
|
||||
);
|
||||
|
||||
if (fileReadResult.error) {
|
||||
|
||||
@@ -13,6 +13,7 @@ import { ToolErrorType } from './tool-error.js';
|
||||
import type {
|
||||
ToolInvocation,
|
||||
ToolResult,
|
||||
ToolResultDisplay,
|
||||
ToolCallConfirmationDetails,
|
||||
ToolExecuteConfirmationDetails,
|
||||
} from './tools.js';
|
||||
@@ -101,7 +102,7 @@ class ShellToolInvocation extends BaseToolInvocation<
|
||||
|
||||
async execute(
|
||||
signal: AbortSignal,
|
||||
updateOutput?: (output: string) => void,
|
||||
updateOutput?: (output: ToolResultDisplay) => void,
|
||||
terminalColumns?: number,
|
||||
terminalRows?: number,
|
||||
): Promise<ToolResult> {
|
||||
|
||||
497
packages/core/src/tools/task.test.ts
Normal file
497
packages/core/src/tools/task.test.ts
Normal file
@@ -0,0 +1,497 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { TaskTool, TaskParams } from './task.js';
|
||||
import type { PartListUnion } from '@google/genai';
|
||||
import type { ToolResultDisplay, TaskResultDisplay } from './tools.js';
|
||||
import { Config } from '../config/config.js';
|
||||
import { SubagentManager } from '../subagents/subagent-manager.js';
|
||||
import { SubagentConfig, SubagentTerminateMode } from '../subagents/types.js';
|
||||
import { SubAgentScope, ContextState } from '../subagents/subagent.js';
|
||||
import { partToString } from '../utils/partUtils.js';
|
||||
|
||||
// Type for accessing protected methods in tests
|
||||
type TaskToolWithProtectedMethods = TaskTool & {
|
||||
createInvocation: (params: TaskParams) => {
|
||||
execute: (
|
||||
signal?: AbortSignal,
|
||||
liveOutputCallback?: (chunk: string) => void,
|
||||
) => Promise<{
|
||||
llmContent: PartListUnion;
|
||||
returnDisplay: ToolResultDisplay;
|
||||
}>;
|
||||
getDescription: () => string;
|
||||
shouldConfirmExecute: () => Promise<boolean>;
|
||||
};
|
||||
};
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('../subagents/subagent-manager.js');
|
||||
vi.mock('../subagents/subagent.js');
|
||||
|
||||
const MockedSubagentManager = vi.mocked(SubagentManager);
|
||||
const MockedContextState = vi.mocked(ContextState);
|
||||
|
||||
describe('TaskTool', () => {
|
||||
let config: Config;
|
||||
let taskTool: TaskTool;
|
||||
let mockSubagentManager: SubagentManager;
|
||||
|
||||
const mockSubagents: SubagentConfig[] = [
|
||||
{
|
||||
name: 'file-search',
|
||||
description: 'Specialized agent for searching and analyzing files',
|
||||
systemPrompt: 'You are a file search specialist.',
|
||||
level: 'project',
|
||||
filePath: '/project/.qwen/agents/file-search.md',
|
||||
},
|
||||
{
|
||||
name: 'code-review',
|
||||
description: 'Agent for reviewing code quality and best practices',
|
||||
systemPrompt: 'You are a code review specialist.',
|
||||
level: 'user',
|
||||
filePath: '/home/user/.qwen/agents/code-review.md',
|
||||
},
|
||||
];
|
||||
|
||||
beforeEach(async () => {
|
||||
// Setup fake timers
|
||||
vi.useFakeTimers();
|
||||
|
||||
// Create mock config
|
||||
config = {
|
||||
getProjectRoot: vi.fn().mockReturnValue('/test/project'),
|
||||
getSessionId: vi.fn().mockReturnValue('test-session-id'),
|
||||
getSubagentManager: vi.fn(),
|
||||
} as unknown as Config;
|
||||
|
||||
// Setup SubagentManager mock
|
||||
mockSubagentManager = {
|
||||
listSubagents: vi.fn().mockResolvedValue(mockSubagents),
|
||||
loadSubagent: vi.fn(),
|
||||
createSubagentScope: vi.fn(),
|
||||
} as unknown as SubagentManager;
|
||||
|
||||
MockedSubagentManager.mockImplementation(() => mockSubagentManager);
|
||||
|
||||
// Make config return the mock SubagentManager
|
||||
vi.mocked(config.getSubagentManager).mockReturnValue(mockSubagentManager);
|
||||
|
||||
// Create TaskTool instance
|
||||
taskTool = new TaskTool(config);
|
||||
|
||||
// Allow async initialization to complete
|
||||
await vi.runAllTimersAsync();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe('initialization', () => {
|
||||
it('should initialize with correct name and properties', () => {
|
||||
expect(taskTool.name).toBe('task');
|
||||
expect(taskTool.displayName).toBe('Task');
|
||||
expect(taskTool.kind).toBe('other');
|
||||
});
|
||||
|
||||
it('should load available subagents during initialization', () => {
|
||||
expect(mockSubagentManager.listSubagents).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should update description with available subagents', () => {
|
||||
expect(taskTool.description).toContain('file-search');
|
||||
expect(taskTool.description).toContain(
|
||||
'Specialized agent for searching and analyzing files',
|
||||
);
|
||||
expect(taskTool.description).toContain('code-review');
|
||||
expect(taskTool.description).toContain(
|
||||
'Agent for reviewing code quality and best practices',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle empty subagents list gracefully', async () => {
|
||||
vi.mocked(mockSubagentManager.listSubagents).mockResolvedValue([]);
|
||||
|
||||
const emptyTaskTool = new TaskTool(config);
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(emptyTaskTool.description).toContain(
|
||||
'No subagents are currently configured',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle subagent loading errors gracefully', async () => {
|
||||
vi.mocked(mockSubagentManager.listSubagents).mockRejectedValue(
|
||||
new Error('Loading failed'),
|
||||
);
|
||||
|
||||
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
|
||||
new TaskTool(config);
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'Failed to load subagents for Task tool:',
|
||||
expect.any(Error),
|
||||
);
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('schema generation', () => {
|
||||
it('should generate schema with subagent names as enum', () => {
|
||||
const schema = taskTool.schema;
|
||||
const properties = schema.parametersJsonSchema as {
|
||||
properties: {
|
||||
subagent_type: {
|
||||
enum?: string[];
|
||||
};
|
||||
};
|
||||
};
|
||||
expect(properties.properties.subagent_type.enum).toEqual([
|
||||
'file-search',
|
||||
'code-review',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should generate schema without enum when no subagents available', async () => {
|
||||
vi.mocked(mockSubagentManager.listSubagents).mockResolvedValue([]);
|
||||
|
||||
const emptyTaskTool = new TaskTool(config);
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
const schema = emptyTaskTool.schema;
|
||||
const properties = schema.parametersJsonSchema as {
|
||||
properties: {
|
||||
subagent_type: {
|
||||
enum?: string[];
|
||||
};
|
||||
};
|
||||
};
|
||||
expect(properties.properties.subagent_type.enum).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateToolParams', () => {
|
||||
const validParams: TaskParams = {
|
||||
description: 'Search files',
|
||||
prompt: 'Find all TypeScript files in the project',
|
||||
subagent_type: 'file-search',
|
||||
};
|
||||
|
||||
it('should validate valid parameters', async () => {
|
||||
const result = taskTool.validateToolParams(validParams);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should reject empty description', async () => {
|
||||
const result = taskTool.validateToolParams({
|
||||
...validParams,
|
||||
description: '',
|
||||
});
|
||||
expect(result).toBe(
|
||||
'Parameter "description" must be a non-empty string.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject empty prompt', async () => {
|
||||
const result = taskTool.validateToolParams({
|
||||
...validParams,
|
||||
prompt: '',
|
||||
});
|
||||
expect(result).toBe('Parameter "prompt" must be a non-empty string.');
|
||||
});
|
||||
|
||||
it('should reject empty subagent_type', async () => {
|
||||
const result = taskTool.validateToolParams({
|
||||
...validParams,
|
||||
subagent_type: '',
|
||||
});
|
||||
expect(result).toBe(
|
||||
'Parameter "subagent_type" must be a non-empty string.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject non-existent subagent', async () => {
|
||||
const result = taskTool.validateToolParams({
|
||||
...validParams,
|
||||
subagent_type: 'non-existent',
|
||||
});
|
||||
expect(result).toBe(
|
||||
'Subagent "non-existent" not found. Available subagents: file-search, code-review',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('refreshSubagents', () => {
|
||||
it('should refresh available subagents and update description', async () => {
|
||||
const newSubagents: SubagentConfig[] = [
|
||||
{
|
||||
name: 'test-agent',
|
||||
description: 'A test agent',
|
||||
systemPrompt: 'Test prompt',
|
||||
level: 'project',
|
||||
filePath: '/project/.qwen/agents/test-agent.md',
|
||||
},
|
||||
];
|
||||
|
||||
vi.mocked(mockSubagentManager.listSubagents).mockResolvedValue(
|
||||
newSubagents,
|
||||
);
|
||||
|
||||
await taskTool.refreshSubagents();
|
||||
|
||||
expect(taskTool.description).toContain('test-agent');
|
||||
expect(taskTool.description).toContain('A test agent');
|
||||
});
|
||||
});
|
||||
|
||||
describe('TaskToolInvocation', () => {
|
||||
let mockSubagentScope: SubAgentScope;
|
||||
let mockContextState: ContextState;
|
||||
|
||||
beforeEach(() => {
|
||||
mockSubagentScope = {
|
||||
runNonInteractive: vi.fn().mockResolvedValue(undefined),
|
||||
result: 'Task completed successfully',
|
||||
terminateMode: SubagentTerminateMode.GOAL,
|
||||
getFinalText: vi.fn().mockReturnValue('Task completed successfully'),
|
||||
formatCompactResult: vi
|
||||
.fn()
|
||||
.mockReturnValue(
|
||||
'✅ Success: Search files completed with GOAL termination',
|
||||
),
|
||||
getExecutionSummary: vi.fn().mockReturnValue({
|
||||
rounds: 2,
|
||||
totalDurationMs: 1500,
|
||||
totalToolCalls: 3,
|
||||
successfulToolCalls: 3,
|
||||
failedToolCalls: 0,
|
||||
successRate: 100,
|
||||
inputTokens: 1000,
|
||||
outputTokens: 500,
|
||||
totalTokens: 1500,
|
||||
estimatedCost: 0.045,
|
||||
toolUsage: [
|
||||
{
|
||||
name: 'grep',
|
||||
count: 2,
|
||||
success: 2,
|
||||
failure: 0,
|
||||
totalDurationMs: 800,
|
||||
averageDurationMs: 400,
|
||||
},
|
||||
{
|
||||
name: 'read_file',
|
||||
count: 1,
|
||||
success: 1,
|
||||
failure: 0,
|
||||
totalDurationMs: 200,
|
||||
averageDurationMs: 200,
|
||||
},
|
||||
],
|
||||
}),
|
||||
getStatistics: vi.fn().mockReturnValue({
|
||||
rounds: 2,
|
||||
totalDurationMs: 1500,
|
||||
totalToolCalls: 3,
|
||||
successfulToolCalls: 3,
|
||||
failedToolCalls: 0,
|
||||
}),
|
||||
getTerminateMode: vi.fn().mockReturnValue(SubagentTerminateMode.GOAL),
|
||||
} as unknown as SubAgentScope;
|
||||
|
||||
mockContextState = {
|
||||
set: vi.fn(),
|
||||
} as unknown as ContextState;
|
||||
|
||||
MockedContextState.mockImplementation(() => mockContextState);
|
||||
|
||||
vi.mocked(mockSubagentManager.loadSubagent).mockResolvedValue(
|
||||
mockSubagents[0],
|
||||
);
|
||||
vi.mocked(mockSubagentManager.createSubagentScope).mockResolvedValue(
|
||||
mockSubagentScope,
|
||||
);
|
||||
});
|
||||
|
||||
it('should execute subagent successfully', async () => {
|
||||
const params: TaskParams = {
|
||||
description: 'Search files',
|
||||
prompt: 'Find all TypeScript files',
|
||||
subagent_type: 'file-search',
|
||||
};
|
||||
|
||||
const invocation = (
|
||||
taskTool as TaskToolWithProtectedMethods
|
||||
).createInvocation(params);
|
||||
const result = await invocation.execute();
|
||||
|
||||
expect(mockSubagentManager.loadSubagent).toHaveBeenCalledWith(
|
||||
'file-search',
|
||||
);
|
||||
expect(mockSubagentManager.createSubagentScope).toHaveBeenCalledWith(
|
||||
mockSubagents[0],
|
||||
config,
|
||||
expect.any(Object), // eventEmitter parameter
|
||||
);
|
||||
expect(mockSubagentScope.runNonInteractive).toHaveBeenCalledWith(
|
||||
mockContextState,
|
||||
undefined, // signal parameter (undefined when not provided)
|
||||
);
|
||||
|
||||
const llmText = partToString(result.llmContent);
|
||||
expect(llmText).toBe('Task completed successfully');
|
||||
const display = result.returnDisplay as TaskResultDisplay;
|
||||
expect(display.type).toBe('task_execution');
|
||||
expect(display.status).toBe('completed');
|
||||
expect(display.subagentName).toBe('file-search');
|
||||
});
|
||||
|
||||
it('should handle subagent not found error', async () => {
|
||||
vi.mocked(mockSubagentManager.loadSubagent).mockResolvedValue(null);
|
||||
|
||||
const params: TaskParams = {
|
||||
description: 'Search files',
|
||||
prompt: 'Find all TypeScript files',
|
||||
subagent_type: 'non-existent',
|
||||
};
|
||||
|
||||
const invocation = (
|
||||
taskTool as TaskToolWithProtectedMethods
|
||||
).createInvocation(params);
|
||||
const result = await invocation.execute();
|
||||
|
||||
const llmText = partToString(result.llmContent);
|
||||
expect(llmText).toContain('Subagent "non-existent" not found');
|
||||
const display = result.returnDisplay as TaskResultDisplay;
|
||||
expect(display.status).toBe('failed');
|
||||
expect(display.subagentName).toBe('non-existent');
|
||||
});
|
||||
|
||||
it('should handle execution errors gracefully', async () => {
|
||||
vi.mocked(mockSubagentManager.createSubagentScope).mockRejectedValue(
|
||||
new Error('Creation failed'),
|
||||
);
|
||||
|
||||
const params: TaskParams = {
|
||||
description: 'Search files',
|
||||
prompt: 'Find all TypeScript files',
|
||||
subagent_type: 'file-search',
|
||||
};
|
||||
|
||||
const invocation = (
|
||||
taskTool as TaskToolWithProtectedMethods
|
||||
).createInvocation(params);
|
||||
const result = await invocation.execute();
|
||||
|
||||
const llmText = partToString(result.llmContent);
|
||||
expect(llmText).toContain('Failed to run subagent: Creation failed');
|
||||
const display = result.returnDisplay as TaskResultDisplay;
|
||||
|
||||
expect(display.status).toBe('failed');
|
||||
});
|
||||
|
||||
it('should execute subagent without live output callback', async () => {
|
||||
const params: TaskParams = {
|
||||
description: 'Search files',
|
||||
prompt: 'Find all TypeScript files',
|
||||
subagent_type: 'file-search',
|
||||
};
|
||||
|
||||
const invocation = (
|
||||
taskTool as TaskToolWithProtectedMethods
|
||||
).createInvocation(params);
|
||||
const result = await invocation.execute();
|
||||
|
||||
// Verify that the task completed successfully
|
||||
expect(result.llmContent).toBeDefined();
|
||||
expect(result.returnDisplay).toBeDefined();
|
||||
|
||||
// Verify the result has the expected structure
|
||||
const text = partToString(result.llmContent);
|
||||
expect(text).toBe('Task completed successfully');
|
||||
const display = result.returnDisplay as TaskResultDisplay;
|
||||
expect(display.status).toBe('completed');
|
||||
expect(display.subagentName).toBe('file-search');
|
||||
});
|
||||
|
||||
it('should set context variables correctly', async () => {
|
||||
const params: TaskParams = {
|
||||
description: 'Search files',
|
||||
prompt: 'Find all TypeScript files',
|
||||
subagent_type: 'file-search',
|
||||
};
|
||||
|
||||
const invocation = (
|
||||
taskTool as TaskToolWithProtectedMethods
|
||||
).createInvocation(params);
|
||||
await invocation.execute();
|
||||
|
||||
expect(mockContextState.set).toHaveBeenCalledWith(
|
||||
'task_prompt',
|
||||
'Find all TypeScript files',
|
||||
);
|
||||
});
|
||||
|
||||
it('should return structured display object', async () => {
|
||||
const params: TaskParams = {
|
||||
description: 'Search files',
|
||||
prompt: 'Find all TypeScript files',
|
||||
subagent_type: 'file-search',
|
||||
};
|
||||
|
||||
const invocation = (
|
||||
taskTool as TaskToolWithProtectedMethods
|
||||
).createInvocation(params);
|
||||
const result = await invocation.execute();
|
||||
|
||||
expect(typeof result.returnDisplay).toBe('object');
|
||||
expect(result.returnDisplay).toHaveProperty('type', 'task_execution');
|
||||
expect(result.returnDisplay).toHaveProperty(
|
||||
'subagentName',
|
||||
'file-search',
|
||||
);
|
||||
expect(result.returnDisplay).toHaveProperty(
|
||||
'taskDescription',
|
||||
'Search files',
|
||||
);
|
||||
expect(result.returnDisplay).toHaveProperty('status', 'completed');
|
||||
});
|
||||
|
||||
it('should not require confirmation', async () => {
|
||||
const params: TaskParams = {
|
||||
description: 'Search files',
|
||||
prompt: 'Find all TypeScript files',
|
||||
subagent_type: 'file-search',
|
||||
};
|
||||
|
||||
const invocation = (
|
||||
taskTool as TaskToolWithProtectedMethods
|
||||
).createInvocation(params);
|
||||
const shouldConfirm = await invocation.shouldConfirmExecute();
|
||||
|
||||
expect(shouldConfirm).toBe(false);
|
||||
});
|
||||
|
||||
it('should provide correct description', async () => {
|
||||
const params: TaskParams = {
|
||||
description: 'Search files',
|
||||
prompt: 'Find all TypeScript files',
|
||||
subagent_type: 'file-search',
|
||||
};
|
||||
|
||||
const invocation = (
|
||||
taskTool as TaskToolWithProtectedMethods
|
||||
).createInvocation(params);
|
||||
const description = invocation.getDescription();
|
||||
|
||||
expect(description).toBe('file-search subagent: "Search files"');
|
||||
});
|
||||
});
|
||||
});
|
||||
558
packages/core/src/tools/task.ts
Normal file
558
packages/core/src/tools/task.ts
Normal file
@@ -0,0 +1,558 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {
|
||||
BaseDeclarativeTool,
|
||||
BaseToolInvocation,
|
||||
Kind,
|
||||
ToolResult,
|
||||
ToolResultDisplay,
|
||||
TaskResultDisplay,
|
||||
} from './tools.js';
|
||||
import { ToolConfirmationOutcome } from './tools.js';
|
||||
import type {
|
||||
ToolCallConfirmationDetails,
|
||||
ToolConfirmationPayload,
|
||||
} from './tools.js';
|
||||
import { Config } from '../config/config.js';
|
||||
import { SubagentManager } from '../subagents/subagent-manager.js';
|
||||
import { SubagentConfig, SubagentTerminateMode } from '../subagents/types.js';
|
||||
import { ContextState } from '../subagents/subagent.js';
|
||||
import {
|
||||
SubAgentEventEmitter,
|
||||
SubAgentToolCallEvent,
|
||||
SubAgentToolResultEvent,
|
||||
SubAgentFinishEvent,
|
||||
SubAgentEventType,
|
||||
SubAgentErrorEvent,
|
||||
SubAgentApprovalRequestEvent,
|
||||
} from '../subagents/subagent-events.js';
|
||||
|
||||
export interface TaskParams {
|
||||
description: string;
|
||||
prompt: string;
|
||||
subagent_type: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Task tool that enables primary agents to delegate tasks to specialized subagents.
|
||||
* The tool dynamically loads available subagents and includes them in its description
|
||||
* for the model to choose from.
|
||||
*/
|
||||
export class TaskTool extends BaseDeclarativeTool<TaskParams, ToolResult> {
|
||||
static readonly Name: string = 'task';
|
||||
|
||||
private subagentManager: SubagentManager;
|
||||
private availableSubagents: SubagentConfig[] = [];
|
||||
|
||||
constructor(private readonly config: Config) {
|
||||
// Initialize with a basic schema first
|
||||
const initialSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'A short (3-5 word) description of the task',
|
||||
},
|
||||
prompt: {
|
||||
type: 'string',
|
||||
description: 'The task for the agent to perform',
|
||||
},
|
||||
subagent_type: {
|
||||
type: 'string',
|
||||
description: 'The type of specialized agent to use for this task',
|
||||
},
|
||||
},
|
||||
required: ['description', 'prompt', 'subagent_type'],
|
||||
additionalProperties: false,
|
||||
$schema: 'http://json-schema.org/draft-07/schema#',
|
||||
};
|
||||
|
||||
super(
|
||||
TaskTool.Name,
|
||||
'Task',
|
||||
'Delegate tasks to specialized subagents. Loading available subagents...', // Initial description
|
||||
Kind.Other,
|
||||
initialSchema,
|
||||
true, // isOutputMarkdown
|
||||
true, // canUpdateOutput - Enable live output updates for real-time progress
|
||||
);
|
||||
|
||||
this.subagentManager = config.getSubagentManager();
|
||||
|
||||
// Initialize the tool asynchronously
|
||||
this.initializeAsync();
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously initializes the tool by loading available subagents
|
||||
* and updating the description and schema.
|
||||
*/
|
||||
private async initializeAsync(): Promise<void> {
|
||||
try {
|
||||
this.availableSubagents = await this.subagentManager.listSubagents();
|
||||
this.updateDescriptionAndSchema();
|
||||
} catch (error) {
|
||||
console.warn('Failed to load subagents for Task tool:', error);
|
||||
this.availableSubagents = [];
|
||||
this.updateDescriptionAndSchema();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the tool's description and schema based on available subagents.
|
||||
*/
|
||||
private updateDescriptionAndSchema(): void {
|
||||
let subagentDescriptions = '';
|
||||
if (this.availableSubagents.length === 0) {
|
||||
subagentDescriptions =
|
||||
'No subagents are currently configured. You can create subagents using the /agents command.';
|
||||
} else {
|
||||
subagentDescriptions = this.availableSubagents
|
||||
.map((subagent) => `- **${subagent.name}**: ${subagent.description}`)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
const baseDescription = `Launch a new agent to handle complex, multi-step tasks autonomously.
|
||||
|
||||
Available agent types and the tools they have access to:
|
||||
${subagentDescriptions}
|
||||
|
||||
When using the Task tool, you must specify a subagent_type parameter to select which agent type to use.
|
||||
|
||||
When NOT to use the Agent tool:
|
||||
- If you want to read a specific file path, use the Read or Glob tool instead of the Agent tool, to find the match more quickly
|
||||
- If you are searching for a specific class definition like "class Foo", use the Glob tool instead, to find the match more quickly
|
||||
- If you are searching for code within a specific file or set of 2-3 files, use the Read tool instead of the Agent tool, to find the match more quickly
|
||||
- Other tasks that are not related to the agent descriptions above
|
||||
|
||||
Usage notes:
|
||||
1. Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses
|
||||
2. When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result.
|
||||
3. Each agent invocation is stateless. You will not be able to send additional messages to the agent, nor will the agent be able to communicate with you outside of its final report. Therefore, your prompt should contain a highly detailed task description for the agent to perform autonomously and you should specify exactly what information the agent should return back to you in its final and only message to you.
|
||||
4. The agent's outputs should generally be trusted
|
||||
5. Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, web fetches, etc.), since it is not aware of the user's intent
|
||||
6. If the agent description mentions that it should be used proactively, then you should try your best to use it without the user having to ask for it first. Use your judgement.
|
||||
|
||||
Example usage:
|
||||
<example_agent_descriptions>
|
||||
"code-reviewer": use this agent after you are done writing a signficant piece of code
|
||||
"greeting-responder": use this agent when to respond to user greetings with a friendly joke
|
||||
</example_agent_description>
|
||||
|
||||
<example>
|
||||
user: "Please write a function that checks if a number is prime"
|
||||
assistant: Sure let me write a function that checks if a number is prime
|
||||
assistant: First let me use the Write tool to write a function that checks if a number is prime
|
||||
assistant: I'm going to use the Write tool to write the following code:
|
||||
<code>
|
||||
function isPrime(n) {
|
||||
if (n <= 1) return false
|
||||
for (let i = 2; i * i <= n; i++) {
|
||||
if (n % i === 0) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
</code>
|
||||
<commentary>
|
||||
Since a signficant piece of code was written and the task was completed, now use the code-reviewer agent to review the code
|
||||
</commentary>
|
||||
assistant: Now let me use the code-reviewer agent to review the code
|
||||
assistant: Uses the Task tool to launch the with the code-reviewer agent
|
||||
</example>
|
||||
|
||||
<example>
|
||||
user: "Hello"
|
||||
<commentary>
|
||||
Since the user is greeting, use the greeting-responder agent to respond with a friendly joke
|
||||
</commentary>
|
||||
assistant: "I'm going to use the Task tool to launch the with the greeting-responder agent"
|
||||
</example>
|
||||
`;
|
||||
|
||||
// Update description using object property assignment since it's readonly
|
||||
(this as { description: string }).description =
|
||||
baseDescription + subagentDescriptions;
|
||||
|
||||
// Generate dynamic schema with enum of available subagent names
|
||||
const subagentNames = this.availableSubagents.map((s) => s.name);
|
||||
|
||||
// Update the parameter schema by modifying the existing object
|
||||
const schema = this.parameterSchema as {
|
||||
properties?: {
|
||||
subagent_type?: {
|
||||
enum?: string[];
|
||||
};
|
||||
};
|
||||
};
|
||||
if (schema.properties && schema.properties.subagent_type) {
|
||||
if (subagentNames.length > 0) {
|
||||
schema.properties.subagent_type.enum = subagentNames;
|
||||
} else {
|
||||
delete schema.properties.subagent_type.enum;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the available subagents and updates the tool description.
|
||||
* This can be called when subagents are added or removed.
|
||||
*/
|
||||
async refreshSubagents(): Promise<void> {
|
||||
await this.initializeAsync();
|
||||
}
|
||||
|
||||
override validateToolParams(params: TaskParams): string | null {
|
||||
// Validate required fields
|
||||
if (
|
||||
!params.description ||
|
||||
typeof params.description !== 'string' ||
|
||||
params.description.trim() === ''
|
||||
) {
|
||||
return 'Parameter "description" must be a non-empty string.';
|
||||
}
|
||||
|
||||
if (
|
||||
!params.prompt ||
|
||||
typeof params.prompt !== 'string' ||
|
||||
params.prompt.trim() === ''
|
||||
) {
|
||||
return 'Parameter "prompt" must be a non-empty string.';
|
||||
}
|
||||
|
||||
if (
|
||||
!params.subagent_type ||
|
||||
typeof params.subagent_type !== 'string' ||
|
||||
params.subagent_type.trim() === ''
|
||||
) {
|
||||
return 'Parameter "subagent_type" must be a non-empty string.';
|
||||
}
|
||||
|
||||
// Validate that the subagent exists
|
||||
const subagentExists = this.availableSubagents.some(
|
||||
(subagent) => subagent.name === params.subagent_type,
|
||||
);
|
||||
|
||||
if (!subagentExists) {
|
||||
const availableNames = this.availableSubagents.map((s) => s.name);
|
||||
return `Subagent "${params.subagent_type}" not found. Available subagents: ${availableNames.join(', ')}`;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected createInvocation(params: TaskParams) {
|
||||
return new TaskToolInvocation(this.config, this.subagentManager, params);
|
||||
}
|
||||
}
|
||||
|
||||
class TaskToolInvocation extends BaseToolInvocation<TaskParams, ToolResult> {
|
||||
private readonly _eventEmitter: SubAgentEventEmitter;
|
||||
private currentDisplay: TaskResultDisplay | null = null;
|
||||
private currentToolCalls: TaskResultDisplay['toolCalls'] = [];
|
||||
|
||||
constructor(
|
||||
private readonly config: Config,
|
||||
private readonly subagentManager: SubagentManager,
|
||||
params: TaskParams,
|
||||
) {
|
||||
super(params);
|
||||
this._eventEmitter = new SubAgentEventEmitter();
|
||||
}
|
||||
|
||||
get eventEmitter(): SubAgentEventEmitter {
|
||||
return this._eventEmitter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the current display state and calls updateOutput if provided
|
||||
*/
|
||||
private updateDisplay(
|
||||
updates: Partial<TaskResultDisplay>,
|
||||
updateOutput?: (output: ToolResultDisplay) => void,
|
||||
): void {
|
||||
if (!this.currentDisplay) return;
|
||||
|
||||
this.currentDisplay = {
|
||||
...this.currentDisplay,
|
||||
...updates,
|
||||
};
|
||||
|
||||
if (updateOutput) {
|
||||
updateOutput(this.currentDisplay);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up event listeners for real-time subagent progress updates
|
||||
*/
|
||||
private setupEventListeners(
|
||||
updateOutput?: (output: ToolResultDisplay) => void,
|
||||
): void {
|
||||
this.eventEmitter.on(SubAgentEventType.START, () => {
|
||||
this.updateDisplay({ status: 'running' }, updateOutput);
|
||||
});
|
||||
|
||||
this.eventEmitter.on(SubAgentEventType.TOOL_CALL, (...args: unknown[]) => {
|
||||
const event = args[0] as SubAgentToolCallEvent;
|
||||
const newToolCall = {
|
||||
callId: event.callId,
|
||||
name: event.name,
|
||||
status: 'executing' as const,
|
||||
args: event.args,
|
||||
description: event.description,
|
||||
};
|
||||
this.currentToolCalls!.push(newToolCall);
|
||||
|
||||
this.updateDisplay(
|
||||
{
|
||||
toolCalls: [...this.currentToolCalls!],
|
||||
},
|
||||
updateOutput,
|
||||
);
|
||||
});
|
||||
|
||||
this.eventEmitter.on(
|
||||
SubAgentEventType.TOOL_RESULT,
|
||||
(...args: unknown[]) => {
|
||||
const event = args[0] as SubAgentToolResultEvent;
|
||||
const toolCallIndex = this.currentToolCalls!.findIndex(
|
||||
(call) => call.callId === event.callId,
|
||||
);
|
||||
if (toolCallIndex >= 0) {
|
||||
this.currentToolCalls![toolCallIndex] = {
|
||||
...this.currentToolCalls![toolCallIndex],
|
||||
status: event.success ? 'success' : 'failed',
|
||||
error: event.error,
|
||||
resultDisplay: event.resultDisplay,
|
||||
};
|
||||
|
||||
this.updateDisplay(
|
||||
{
|
||||
toolCalls: [...this.currentToolCalls!],
|
||||
},
|
||||
updateOutput,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
this.eventEmitter.on(SubAgentEventType.FINISH, (...args: unknown[]) => {
|
||||
const event = args[0] as SubAgentFinishEvent;
|
||||
this.updateDisplay(
|
||||
{
|
||||
status: event.terminateReason === 'GOAL' ? 'completed' : 'failed',
|
||||
terminateReason: event.terminateReason,
|
||||
},
|
||||
updateOutput,
|
||||
);
|
||||
});
|
||||
|
||||
this.eventEmitter.on(SubAgentEventType.ERROR, (...args: unknown[]) => {
|
||||
const event = args[0] as SubAgentErrorEvent;
|
||||
this.updateDisplay(
|
||||
{
|
||||
status: 'failed',
|
||||
terminateReason: event.error,
|
||||
},
|
||||
updateOutput,
|
||||
);
|
||||
});
|
||||
|
||||
// Indicate when a tool call is waiting for approval
|
||||
this.eventEmitter.on(
|
||||
SubAgentEventType.TOOL_WAITING_APPROVAL,
|
||||
(...args: unknown[]) => {
|
||||
const event = args[0] as SubAgentApprovalRequestEvent;
|
||||
const idx = this.currentToolCalls!.findIndex(
|
||||
(c) => c.callId === event.callId,
|
||||
);
|
||||
if (idx >= 0) {
|
||||
this.currentToolCalls![idx] = {
|
||||
...this.currentToolCalls![idx],
|
||||
status: 'awaiting_approval',
|
||||
};
|
||||
} else {
|
||||
this.currentToolCalls!.push({
|
||||
callId: event.callId,
|
||||
name: event.name,
|
||||
status: 'awaiting_approval',
|
||||
description: event.description,
|
||||
});
|
||||
}
|
||||
|
||||
// Bridge scheduler confirmation details to UI inline prompt
|
||||
const details: ToolCallConfirmationDetails = {
|
||||
...(event.confirmationDetails as Omit<
|
||||
ToolCallConfirmationDetails,
|
||||
'onConfirm'
|
||||
>),
|
||||
onConfirm: async (
|
||||
outcome: ToolConfirmationOutcome,
|
||||
payload?: ToolConfirmationPayload,
|
||||
) => {
|
||||
// Clear the inline prompt immediately
|
||||
// and optimistically mark the tool as executing for proceed outcomes.
|
||||
const proceedOutcomes = new Set<ToolConfirmationOutcome>([
|
||||
ToolConfirmationOutcome.ProceedOnce,
|
||||
ToolConfirmationOutcome.ProceedAlways,
|
||||
ToolConfirmationOutcome.ProceedAlwaysServer,
|
||||
ToolConfirmationOutcome.ProceedAlwaysTool,
|
||||
]);
|
||||
|
||||
if (proceedOutcomes.has(outcome)) {
|
||||
const idx2 = this.currentToolCalls!.findIndex(
|
||||
(c) => c.callId === event.callId,
|
||||
);
|
||||
if (idx2 >= 0) {
|
||||
this.currentToolCalls![idx2] = {
|
||||
...this.currentToolCalls![idx2],
|
||||
status: 'executing',
|
||||
};
|
||||
}
|
||||
this.updateDisplay(
|
||||
{
|
||||
toolCalls: [...this.currentToolCalls!],
|
||||
pendingConfirmation: undefined,
|
||||
},
|
||||
updateOutput,
|
||||
);
|
||||
} else {
|
||||
this.updateDisplay(
|
||||
{ pendingConfirmation: undefined },
|
||||
updateOutput,
|
||||
);
|
||||
}
|
||||
|
||||
await event.respond(outcome, payload);
|
||||
},
|
||||
} as ToolCallConfirmationDetails;
|
||||
|
||||
this.updateDisplay(
|
||||
{
|
||||
toolCalls: [...this.currentToolCalls!],
|
||||
pendingConfirmation: details,
|
||||
},
|
||||
updateOutput,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return `${this.params.subagent_type} subagent: "${this.params.description}"`;
|
||||
}
|
||||
|
||||
override async shouldConfirmExecute(): Promise<false> {
|
||||
// Task delegation should execute automatically without user confirmation
|
||||
return false;
|
||||
}
|
||||
|
||||
async execute(
|
||||
signal?: AbortSignal,
|
||||
updateOutput?: (output: ToolResultDisplay) => void,
|
||||
): Promise<ToolResult> {
|
||||
try {
|
||||
// Load the subagent configuration
|
||||
const subagentConfig = await this.subagentManager.loadSubagent(
|
||||
this.params.subagent_type,
|
||||
);
|
||||
|
||||
if (!subagentConfig) {
|
||||
const errorDisplay = {
|
||||
type: 'task_execution' as const,
|
||||
subagentName: this.params.subagent_type,
|
||||
taskDescription: this.params.description,
|
||||
taskPrompt: this.params.prompt,
|
||||
status: 'failed' as const,
|
||||
terminateReason: `Subagent "${this.params.subagent_type}" not found`,
|
||||
};
|
||||
|
||||
return {
|
||||
llmContent: `Subagent "${this.params.subagent_type}" not found`,
|
||||
returnDisplay: errorDisplay,
|
||||
};
|
||||
}
|
||||
|
||||
// Initialize the current display state
|
||||
this.currentDisplay = {
|
||||
type: 'task_execution' as const,
|
||||
subagentName: subagentConfig.name,
|
||||
taskDescription: this.params.description,
|
||||
taskPrompt: this.params.prompt,
|
||||
status: 'running' as const,
|
||||
subagentColor: subagentConfig.color,
|
||||
};
|
||||
|
||||
// Set up event listeners for real-time updates
|
||||
this.setupEventListeners(updateOutput);
|
||||
|
||||
// Send initial display
|
||||
if (updateOutput) {
|
||||
updateOutput(this.currentDisplay);
|
||||
}
|
||||
const subagentScope = await this.subagentManager.createSubagentScope(
|
||||
subagentConfig,
|
||||
this.config,
|
||||
{ eventEmitter: this.eventEmitter },
|
||||
);
|
||||
|
||||
// Create context state with the task prompt
|
||||
const contextState = new ContextState();
|
||||
contextState.set('task_prompt', this.params.prompt);
|
||||
|
||||
// Execute the subagent (blocking)
|
||||
await subagentScope.runNonInteractive(contextState, signal);
|
||||
|
||||
// Get the results
|
||||
const finalText = subagentScope.getFinalText();
|
||||
const terminateMode = subagentScope.getTerminateMode();
|
||||
const success = terminateMode === SubagentTerminateMode.GOAL;
|
||||
const executionSummary = subagentScope.getExecutionSummary();
|
||||
|
||||
if (signal?.aborted) {
|
||||
this.updateDisplay(
|
||||
{
|
||||
status: 'cancelled',
|
||||
terminateReason: 'Task was cancelled by user',
|
||||
executionSummary,
|
||||
},
|
||||
updateOutput,
|
||||
);
|
||||
} else {
|
||||
this.updateDisplay(
|
||||
{
|
||||
status: success ? 'completed' : 'failed',
|
||||
terminateReason: terminateMode,
|
||||
result: finalText,
|
||||
executionSummary,
|
||||
},
|
||||
updateOutput,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
llmContent: [{ text: finalText }],
|
||||
returnDisplay: this.currentDisplay!,
|
||||
};
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
console.error(`[TaskTool] Error running subagent: ${errorMessage}`);
|
||||
|
||||
const errorDisplay: TaskResultDisplay = {
|
||||
...this.currentDisplay!,
|
||||
status: 'failed',
|
||||
terminateReason: `Failed to run subagent: ${errorMessage}`,
|
||||
};
|
||||
|
||||
return {
|
||||
llmContent: `Failed to run subagent: ${errorMessage}`,
|
||||
returnDisplay: errorDisplay,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -243,7 +243,7 @@ describe('TodoWriteTool', () => {
|
||||
});
|
||||
|
||||
it('should have correct display name', () => {
|
||||
expect(tool.displayName).toBe('Todo Write');
|
||||
expect(tool.displayName).toBe('TodoWrite');
|
||||
});
|
||||
|
||||
it('should have correct kind', () => {
|
||||
|
||||
@@ -403,7 +403,7 @@ export class TodoWriteTool extends BaseDeclarativeTool<
|
||||
constructor(private readonly config: Config) {
|
||||
super(
|
||||
TodoWriteTool.Name,
|
||||
'Todo Write',
|
||||
'TodoWrite',
|
||||
todoWriteToolDescription,
|
||||
Kind.Think,
|
||||
todoWriteToolSchemaData.parametersJsonSchema as Record<string, unknown>,
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { FunctionDeclaration } from '@google/genai';
|
||||
import type {
|
||||
AnyDeclarativeTool,
|
||||
ToolResult,
|
||||
ToolResultDisplay,
|
||||
ToolInvocation,
|
||||
} from './tools.js';
|
||||
import { Kind, BaseDeclarativeTool, BaseToolInvocation } from './tools.js';
|
||||
@@ -41,7 +42,7 @@ class DiscoveredToolInvocation extends BaseToolInvocation<
|
||||
|
||||
async execute(
|
||||
_signal: AbortSignal,
|
||||
_updateOutput?: (output: string) => void,
|
||||
_updateOutput?: (output: ToolResultDisplay) => void,
|
||||
): Promise<ToolResult> {
|
||||
const callCommand = this.config.getToolCallCommand()!;
|
||||
const child = spawn(callCommand, [this.toolName]);
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { FunctionDeclaration, PartListUnion } from '@google/genai';
|
||||
import { ToolErrorType } from './tool-error.js';
|
||||
import type { DiffUpdateResult } from '../ide/ideContext.js';
|
||||
import { SchemaValidator } from '../utils/schemaValidator.js';
|
||||
import { SubagentStatsSummary } from '../subagents/subagent-statistics.js';
|
||||
|
||||
/**
|
||||
* Represents a validated and ready-to-execute tool call.
|
||||
@@ -51,7 +52,7 @@ export interface ToolInvocation<
|
||||
*/
|
||||
execute(
|
||||
signal: AbortSignal,
|
||||
updateOutput?: (output: string) => void,
|
||||
updateOutput?: (output: ToolResultDisplay) => void,
|
||||
): Promise<TResult>;
|
||||
}
|
||||
|
||||
@@ -79,7 +80,7 @@ export abstract class BaseToolInvocation<
|
||||
|
||||
abstract execute(
|
||||
signal: AbortSignal,
|
||||
updateOutput?: (output: string) => void,
|
||||
updateOutput?: (output: ToolResultDisplay) => void,
|
||||
): Promise<TResult>;
|
||||
}
|
||||
|
||||
@@ -197,7 +198,7 @@ export abstract class DeclarativeTool<
|
||||
async buildAndExecute(
|
||||
params: TParams,
|
||||
signal: AbortSignal,
|
||||
updateOutput?: (output: string) => void,
|
||||
updateOutput?: (output: ToolResultDisplay) => void,
|
||||
): Promise<TResult> {
|
||||
const invocation = this.build(params);
|
||||
return invocation.execute(signal, updateOutput);
|
||||
@@ -432,7 +433,38 @@ export function hasCycleInSchema(schema: object): boolean {
|
||||
return traverse(schema, new Set<string>(), new Set<string>());
|
||||
}
|
||||
|
||||
export type ToolResultDisplay = string | FileDiff | TodoResultDisplay;
|
||||
export interface TaskResultDisplay {
|
||||
type: 'task_execution';
|
||||
subagentName: string;
|
||||
subagentColor?: string;
|
||||
taskDescription: string;
|
||||
taskPrompt: string;
|
||||
status: 'running' | 'completed' | 'failed' | 'cancelled';
|
||||
terminateReason?: string;
|
||||
result?: string;
|
||||
executionSummary?: SubagentStatsSummary;
|
||||
|
||||
// If the subagent is awaiting approval for a tool call,
|
||||
// this contains the confirmation details for inline UI rendering.
|
||||
pendingConfirmation?: ToolCallConfirmationDetails;
|
||||
|
||||
toolCalls?: Array<{
|
||||
callId: string;
|
||||
name: string;
|
||||
status: 'executing' | 'awaiting_approval' | 'success' | 'failed';
|
||||
error?: string;
|
||||
args?: Record<string, unknown>;
|
||||
result?: string;
|
||||
resultDisplay?: string;
|
||||
description?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export type ToolResultDisplay =
|
||||
| string
|
||||
| FileDiff
|
||||
| TodoResultDisplay
|
||||
| TaskResultDisplay;
|
||||
|
||||
export interface FileDiff {
|
||||
fileDiff: string;
|
||||
|
||||
@@ -157,7 +157,7 @@ export class WebSearchTool extends BaseDeclarativeTool<
|
||||
constructor(private readonly config: Config) {
|
||||
super(
|
||||
WebSearchTool.Name,
|
||||
'TavilySearch',
|
||||
'WebSearch',
|
||||
'Performs a web search using the Tavily API and returns a concise answer with sources. Requires the TAVILY_API_KEY environment variable.',
|
||||
Kind.Search,
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user