import { DynamicStructuredTool } from '@langchain/core/tools'; import { z } from 'zod'; import type { FastifyBaseLogger } from 'fastify'; import type { ResearchSubagent } from '../../harness/subagents/research/index.js'; import type { SubagentContext } from '../../harness/subagents/base-subagent.js'; export interface ResearchAgentToolConfig { researchSubagent: ResearchSubagent; context: SubagentContext; logger: FastifyBaseLogger; } /** * Creates a LangChain tool that delegates to the research subagent. * This is the standard LangChain pattern for exposing a subagent as a tool * to a parent agent. */ export function createResearchAgentTool(config: ResearchAgentToolConfig): DynamicStructuredTool { const { researchSubagent, context, logger } = config; return new DynamicStructuredTool({ name: 'research', description: `Delegate to the research subagent for data analysis, charting, statistics, and Python script execution. Use this tool for: - Plotting charts with technical indicators (EMA, RSI, MACD, Bollinger Bands, etc.) - Statistical analysis of price data - Custom research scripts using the DataAPI and ChartingAPI - Any task requiring code execution or matplotlib charts The research subagent will write and execute Python scripts, capture output and charts, and return results.`, schema: z.object({ name: z.string().describe('The name of the research script to create or update (e.g. "btc_ema_analysis"). Use the same name across calls to revise the same script rather than creating a new one.'), instruction: z.string().describe('The research task or analysis to perform. Be specific about what data, indicators, timeframes, and output you want.'), }), func: async ({ name, instruction }: { name: string; instruction: string }): Promise => { logger.info({ name, instruction: instruction.substring(0, 100) }, 'Delegating to research subagent'); const prompt = `Research script name: "${name}"\n\n${instruction}`; try { const result = await researchSubagent.executeWithImages(context, prompt); // Return in the format that AgentHarness.processToolResult() knows how to handle // (extracts images and passes them to channelAdapter) return JSON.stringify({ text: result.text, images: result.images, }); } catch (error) { logger.error({ error, errorMessage: (error as Error)?.message }, 'Research subagent failed'); throw error; } }, }); }