Skip to main content
Glama
NorthSeacoder

Frontend Test Generation & Code Review MCP Server

react-engine.ts10.9 kB
/** * ReAct Engine - Agent 执行引擎(Reasoning + Acting) * * 核心循环: * 1. Thought: Agent 思考(调用 LLM) * 2. Action: Agent 决定行动(调用工具或返回结果) * 3. Observation: 记录观察结果(工具输出) * 4. 重复直到任务完成或达到最大步数 */ import { OpenAIClient } from '../clients/openai.js'; import { ToolRegistry } from './tool-registry.js'; import { AgentContext, ContextStore, Thought, Action, Observation } from './context.js'; import { logger } from '../utils/logger.js'; import { getMetrics } from '../utils/metrics.js'; export interface ReActConfig { maxSteps: number; temperature: number; stopReasons: string[]; useFunctionCalling?: boolean; // 是否使用 Function Calling(默认 true) } export interface ReActResult { success: boolean; finalAnswer?: unknown; error?: string; context: AgentContext; } /** * ReActEngine - 核心执行引擎 */ export class ReActEngine { constructor( private llm: OpenAIClient, private toolRegistry: ToolRegistry, private contextStore: ContextStore, private config: ReActConfig ) {} /** * 执行 ReAct 循环 */ async run(params: { agentName: string; task: string; systemPrompt: string; goal?: string; constraints?: string[]; initialData?: Record<string, unknown>; }): Promise<ReActResult> { const sessionId = this.generateSessionId(); // 创建上下文 const context = this.contextStore.create(sessionId, params.agentName, params.task, { goal: params.goal, constraints: params.constraints, maxSteps: this.config.maxSteps, initialData: params.initialData, }); logger.info(`[ReActEngine] Starting session ${sessionId}`, { agent: params.agentName, task: params.task, }); getMetrics().recordCounter('react.session.started', 1, { agent: params.agentName }); try { while (!context.isComplete && context.currentStep < context.maxSteps) { // Step 1: Thought const thought = await this.think(context, params.systemPrompt); logger.debug(`[ReActEngine] Thought: ${thought.content}`); // Step 2: Action const action = await this.decide(context, thought); logger.debug(`[ReActEngine] Action: ${action.type}`); // Step 3: Execute & Observe const observation = await this.act(action, context); logger.debug(`[ReActEngine] Observation: ${observation.type}`); // Record history this.contextStore.addHistory(sessionId, { thought, action, observation }); // Check termination if (action.type === 'terminate' || action.type === 'respond') { context.isComplete = true; break; } } if (context.currentStep >= context.maxSteps) { logger.warn(`[ReActEngine] Max steps reached for session ${sessionId}`); } getMetrics().recordCounter('react.session.completed', 1, { agent: params.agentName, status: 'success', }); getMetrics().recordHistogram('react.session.steps', context.currentStep, { agent: params.agentName, }); return { success: true, finalAnswer: context.data.finalAnswer, context, }; } catch (error) { logger.error(`[ReActEngine] Session ${sessionId} failed`, { error }); getMetrics().recordCounter('react.session.completed', 1, { agent: params.agentName, status: 'error', }); return { success: false, error: error instanceof Error ? error.message : String(error), context, }; } } /** * Thought: Agent 思考下一步 */ private async think(context: AgentContext, systemPrompt: string): Promise<Thought> { const historyText = this.formatHistory(context); const userPrompt = ` Task: ${context.task} ${context.goal ? `Goal: ${context.goal}` : ''} History: ${historyText} Current Step: ${context.currentStep + 1}/${context.maxSteps} What should I do next? Think step by step. `; let response: string; if (this.config.useFunctionCalling !== false) { // 使用 Function Calling try { const tools = await this.buildToolDefinitions(); const choice = await this.llm.completeWithToolCalls( [ { role: 'system', content: systemPrompt }, { role: 'user', content: userPrompt }, ], { temperature: this.config.temperature, tools: tools.length > 0 ? tools : undefined, toolChoice: tools.length > 0 ? 'auto' : undefined, } ); // 如果有 tool calls,存储到上下文中以便 decide() 使用 if (choice.message.tool_calls && choice.message.tool_calls.length > 0) { context.data._pendingToolCalls = choice.message.tool_calls; } response = choice.message.content || JSON.stringify(choice.message.tool_calls); } catch (error) { logger.warn('[ReActEngine] Function calling failed, falling back to regex parsing', { error, }); response = await this.llm.complete( [ { role: 'system', content: systemPrompt }, { role: 'user', content: userPrompt }, ], { temperature: this.config.temperature } ); } } else { // 使用正则匹配(fallback) response = await this.llm.complete( [ { role: 'system', content: systemPrompt }, { role: 'user', content: userPrompt }, ], { temperature: this.config.temperature } ); } return { content: response, timestamp: Date.now(), }; } /** * Action: 从 Thought 中提取行动 */ private async decide(context: AgentContext, thought: Thought): Promise<Action> { // 优先使用 Function Calling 结果 if (context.data._pendingToolCalls) { const toolCalls = context.data._pendingToolCalls as Array<{ id: string; type: 'function'; function: { name: string; arguments: string }; }>; if (toolCalls.length > 0) { const toolCall = toolCalls[0]; let parameters: Record<string, unknown> = {}; try { parameters = JSON.parse(toolCall.function.arguments); } catch (error) { logger.warn('[ReActEngine] Failed to parse tool call arguments', { arguments: toolCall.function.arguments, error, }); } // 清除已使用的 tool calls delete context.data._pendingToolCalls; return { type: 'call_tool', toolName: toolCall.function.name, parameters, timestamp: Date.now(), }; } } // Fallback: 解析 LLM 输出中的行动指令(正则匹配) const content = thought.content; const contentLower = content.toLowerCase(); if (contentLower.includes('call tool:') || contentLower.includes('use tool:')) { // 提取工具名和参数(使用原始内容进行匹配,保留大小写) const toolMatch = content.match(/(?:call tool|use tool):\s*([\w-]+)/i); const toolName = toolMatch ? toolMatch[1] : ''; // 尝试解析参数 let parameters: Record<string, unknown> = {}; const paramsMatch = content.match(/parameters?:\s*(\{[^}]+\}|\{[\s\S]+?\})/i); if (paramsMatch) { try { parameters = JSON.parse(paramsMatch[1]); } catch (error) { logger.warn('[ReActEngine] Failed to parse parameters from text', { text: paramsMatch[1], error, }); } } return { type: 'call_tool', toolName, parameters, timestamp: Date.now(), }; } if (contentLower.includes('final answer:') || contentLower.includes('complete')) { return { type: 'terminate', message: thought.content, timestamp: Date.now(), }; } // 默认继续思考 return { type: 'respond', message: thought.content, timestamp: Date.now(), }; } /** * Act: 执行行动并返回观察结果 */ private async act(action: Action, context: AgentContext): Promise<Observation> { if (action.type === 'call_tool' && action.toolName) { const tool = await this.toolRegistry.get(action.toolName); if (!tool) { return { type: 'error', source: 'system', content: `Tool "${action.toolName}" not found`, timestamp: Date.now(), }; } try { const result = await tool.execute(action.parameters || {}); return { type: 'tool_result', source: action.toolName, content: result, timestamp: Date.now(), }; } catch (error) { return { type: 'error', source: action.toolName, content: error instanceof Error ? error.message : String(error), timestamp: Date.now(), }; } } if (action.type === 'terminate' || action.type === 'respond') { context.data.finalAnswer = action.message; return { type: 'system_event', source: 'engine', content: 'Task completed', timestamp: Date.now(), }; } return { type: 'error', source: 'engine', content: `Unknown action type: ${action.type}`, timestamp: Date.now(), }; } private formatHistory(context: AgentContext): string { return context.history .map((entry, idx) => { const parts: string[] = [`Step ${idx + 1}:`]; if (entry.thought) parts.push(`Thought: ${entry.thought.content}`); if (entry.action) parts.push(`Action: ${entry.action.type}`); if (entry.observation) parts.push(`Observation: ${JSON.stringify(entry.observation.content)}`); return parts.join('\n'); }) .join('\n\n'); } private async buildToolDefinitions(): Promise< Array<{ type: 'function'; function: { name: string; description?: string; parameters?: Record<string, unknown>; }; }> > { const metadataList = this.toolRegistry.listMetadata(); return metadataList.map((metadata) => { const parameters = metadata.inputSchema && Object.keys(metadata.inputSchema).length > 0 ? metadata.inputSchema : { type: 'object', properties: {}, }; return { type: 'function' as const, function: { name: metadata.name, description: metadata.description, parameters, }, }; }); } private generateSessionId(): string { return `session-${Date.now()}-${Math.random().toString(36).substring(7)}`; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/NorthSeacoder/fe-testgen-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server