UNPKG

@xynehq/jaf

Version:

Juspay Agent Framework - A purely functional agent framework with immutable state and composable tools

651 lines 28.3 kB
/** * JAF ADK Layer - Runner System * * Functional agent execution system */ import { throwAgentError } from '../types'; import { generateRunnerGraph, generateAgentGraph, generateToolGraph } from '../../visualization/graphviz'; import { getOrCreateSession, addMessageToSession, addArtifactToSession } from '../sessions'; import { executeTool } from '../tools'; import { createModelMessage, getFunctionCalls, createUserMessage } from '../content'; import { createAdkLLMService } from '../providers/llm-service.js'; import { createAdkLLMConfigFromEnvironment, createAdkLLMServiceConfig } from '../config/llm-config.js'; // ========== Core Runner Functions ========== export const runAgent = async (config, context, message) => { const requestId = context.requestId || generateRequestId(); const startTime = Date.now(); try { // Get or create session const session = await getOrCreateSession(config.sessionProvider, { appName: context.metadata?.appName || 'default', userId: context.userId, sessionId: context.sessionId }); // Apply guardrails before adding to session const guardedMessage = await applyGuardrails(config.guardrails || [], message, { agent: config.agent, session, previousMessages: session.messages }); // Add guarded message to session const updatedSession = addMessageToSession(session, guardedMessage); // Execute agent const result = await executeAgent(config, updatedSession, guardedMessage, context); // Calculate metadata const executionTime = Date.now() - startTime; const metadata = { requestId, agentId: config.agent.id, timestamp: new Date(), executionTime, llmCalls: 1 // Simplified for now }; return { ...result, metadata }; } catch (error) { throwAgentError(`Agent execution failed: ${error instanceof Error ? error.message : String(error)}`, config.agent.id, { requestId, context }); } // This should never be reached due to throwAgentError throwing throw new Error('Unreachable code'); }; export const runAgentStream = async function* (config, context, message) { const requestId = context.requestId || generateRequestId(); try { yield createAgentEvent('message_start', { content: message }); // Get or create session const session = await getOrCreateSession(config.sessionProvider, { appName: context.metadata?.appName || 'default', userId: context.userId, sessionId: context.sessionId }); // Apply guardrails before adding to session const guardedMessage = await applyGuardrails(config.guardrails || [], message, { agent: config.agent, session, previousMessages: session.messages }); // Add guarded message to session const updatedSession = addMessageToSession(session, guardedMessage); // Execute agent with streaming yield* executeAgentStream(config, updatedSession, guardedMessage, context); yield createAgentEvent('message_complete'); } catch (error) { yield createAgentEvent('error', { error: error instanceof Error ? error.message : String(error) }); } }; // ========== Agent Execution ========== const executeAgent = async (config, session, message, context) => { const agent = config.agent; let currentSession = session; const toolCalls = []; const toolResponses = []; // Check if this is a multi-agent if (isMultiAgent(agent)) { return await executeMultiAgent(config, currentSession, message, context); } // Call real LLM service const llmResponse = await callRealLLM(agent, message, currentSession); // Check for function calls in the response const functionCalls = getFunctionCalls(llmResponse); if (functionCalls.length > 0) { // Execute tools const toolContext = createToolContext(agent, currentSession, message); for (const functionCall of functionCalls) { const tool = agent.config.tools.find(t => t.name === functionCall.name); if (tool) { try { const toolResult = await executeTool(tool, functionCall.args, toolContext); const functionResponse = { id: functionCall.id, name: functionCall.name, response: toolResult.data, success: toolResult.success, error: toolResult.error }; toolResponses.push(functionResponse); llmResponse.parts.push({ type: 'function_response', functionResponse }); // Handle tool actions if (toolContext.actions.transferToAgent) { // Handle agent transfer const targetAgent = agent.config.subAgents?.find(sub => sub.name === toolContext.actions.transferToAgent); if (targetAgent) { const transferConfig = { ...config, agent: { ...agent, config: targetAgent } }; return await executeAgent(transferConfig, currentSession, message, context); } } if (toolContext.actions.addArtifact) { // Artifacts are handled via the actions object } } catch (error) { const functionResponse = { id: functionCall.id, name: functionCall.name, response: null, success: false, error: error instanceof Error ? error.message : 'Tool execution failed' }; toolResponses.push(functionResponse); } } toolCalls.push(functionCall); } // If tools were executed, make a second LLM call to get final response with tool results if (toolResponses.length > 0) { // Update session with the tool calls and responses first currentSession = addMessageToSession(currentSession, llmResponse); // Create tool response messages for each tool call (required by OpenAI format) for (const toolResponse of toolResponses) { const toolResultMessage = { role: 'tool', // Tool responses must have role 'tool' for OpenAI API parts: [{ type: 'function_response', functionResponse: toolResponse }], metadata: {} }; // Add each tool response message to session currentSession = addMessageToSession(currentSession, toolResultMessage); } // Create a user message asking the LLM to provide a final response const followUpMessage = createUserMessage('Please provide a final response based on the tool results.'); // Make second LLM call to get final response const finalResponse = await callRealLLM(agent, followUpMessage, currentSession); // Update session with final response currentSession = addMessageToSession(currentSession, finalResponse); await config.sessionProvider.updateSession(currentSession); // Return the final response that includes tool results return { content: finalResponse, session: currentSession, toolCalls, toolResponses, metadata: { requestId: generateRequestId(), agentId: agent.id, llmCalls: 2, // Two LLM calls made timestamp: new Date() } }; } } // Update session with response (no tools were called) currentSession = addMessageToSession(currentSession, llmResponse); await config.sessionProvider.updateSession(currentSession); return { content: llmResponse, session: currentSession, toolCalls, toolResponses, metadata: { requestId: generateRequestId(), agentId: agent.id, llmCalls: 1, // One LLM call made timestamp: new Date() } }; }; const executeAgentStream = async function* (config, session, message, context) { const agent = config.agent; try { // Create LLM service instance with environment-based configuration const llmConfig = createAdkLLMConfigFromEnvironment(); const llmService = createAdkLLMService(createAdkLLMServiceConfig(llmConfig)); // Call real streaming LLM const streamGenerator = llmService.generateStreamingResponse(agent, session, message, { modelOverride: typeof agent.config.model === 'string' ? agent.config.model : undefined, temperature: 0.7, maxTokens: 2000 }); // Stream real LLM responses for await (const chunk of streamGenerator) { if (chunk.isDone) { break; } if (chunk.delta) { yield createAgentEvent('message_delta', { content: createModelMessage(chunk.delta) }); } if (chunk.functionCall) { yield createAgentEvent('function_call_start', { functionCall: chunk.functionCall }); } } } catch (error) { console.error('[ADK:STREAM] Real streaming failed:', error); // Fallback to error message yield createAgentEvent('error', { error: `Streaming error: ${error instanceof Error ? error.message : 'Unknown error'}` }); } }; // ========== Multi-Agent Helper Functions ========== const mergeParallelResponses = (responses, config) => { if (responses.length === 0) { throwAgentError('No responses to merge from parallel execution'); } // Merge all response content const mergedParts = []; const mergedArtifacts = {}; responses.forEach((response, index) => { // Add agent identifier to each response part const agentName = config.subAgents[index]?.name || `agent_${index}`; response.content.parts.forEach(part => { if (part.type === 'text') { mergedParts.push({ type: 'text', text: `[${agentName}]: ${part.text}` }); } else { mergedParts.push(part); } }); // Merge artifacts Object.entries(response.session.artifacts).forEach(([key, value]) => { mergedArtifacts[`${agentName}_${key}`] = value; }); // Merge metrics if available // Note: metrics field is not part of AgentResponse type yet }); // Use the first response as base and merge content const baseResponse = responses[0]; return { ...baseResponse, content: { role: 'model', parts: mergedParts }, session: { ...baseResponse.session, artifacts: mergedArtifacts } }; }; const selectBestAgent = (subAgents, message, context) => { const messageText = message.parts .filter(p => p.type === 'text') .map(p => p.text) .join(' ') .toLowerCase(); // Score each agent based on relevance const scores = subAgents.map((agent) => { let score = 0; const agentNameLower = agent.name.toLowerCase(); const instructionLower = agent.instruction.toLowerCase(); // Check for keyword matches in agent name and instruction const keywords = extractKeywords(messageText); keywords.forEach(keyword => { if (agentNameLower.includes(keyword)) score += 3; if (instructionLower.includes(keyword)) score += 2; }); // Check for tool relevance agent.tools.forEach((tool) => { const toolNameLower = tool.name.toLowerCase(); const toolDescLower = tool.description.toLowerCase(); keywords.forEach(keyword => { if (toolNameLower.includes(keyword)) score += 2; if (toolDescLower.includes(keyword)) score += 1; }); }); return { agent, score }; }); // Sort by score and return the best match scores.sort((a, b) => b.score - a.score); // If no clear winner, use the first agent if (scores[0].score === 0) { return subAgents[0]; } return scores[0].agent; }; const extractKeywords = (text) => { // Extract meaningful keywords from the message const commonWords = new Set(['the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'is', 'are', 'was', 'were', 'be', 'been', 'being', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could', 'should', 'may', 'might', 'must', 'can', 'what', 'how', 'when', 'where', 'why', 'who']); const words = text .split(/\s+/) .map(w => w.toLowerCase().replace(/[^a-z0-9]/g, '')) .filter(w => w.length > 2 && !commonWords.has(w)); return [...new Set(words)]; }; const executeWithCoordinationRules = async (config, session, message, context, multiConfig) => { // Evaluate coordination rules for (const rule of multiConfig.coordinationRules || []) { if (rule.condition(message, context)) { switch (rule.action) { case 'delegate': { // Delegate to specific agent(s) const targetAgent = rule.targetAgents?.[0]; if (targetAgent) { const agentConfig = multiConfig.subAgents.find(a => a.name === targetAgent); if (agentConfig) { const subAgent = { ...config.agent, config: agentConfig }; const subConfig = { ...config, agent: subAgent }; return await executeAgent(subConfig, session, message, context); } } break; } case 'parallel': { // Execute specified agents in parallel const targetConfigs = rule.targetAgents ? multiConfig.subAgents.filter(a => rule.targetAgents?.includes(a.name)) : multiConfig.subAgents; const promises = targetConfigs.map(subAgentConfig => { const subAgent = { ...config.agent, config: subAgentConfig }; const subConfig = { ...config, agent: subAgent }; return executeAgent(subConfig, session, message, context); }); const responses = await Promise.all(promises); return mergeParallelResponses(responses, multiConfig); } case 'sequential': { // Execute specified agents sequentially const targetConfigs = rule.targetAgents ? multiConfig.subAgents.filter(a => rule.targetAgents?.includes(a.name)) : multiConfig.subAgents; let currentSession = session; let currentMessage = message; let finalResponse = null; for (const subAgentConfig of targetConfigs) { const subAgent = { ...config.agent, config: subAgentConfig }; const subConfig = { ...config, agent: subAgent }; const response = await executeAgent(subConfig, currentSession, currentMessage, context); currentSession = response.session; currentMessage = response.content; finalResponse = response; } return finalResponse; } } } } // If no rules match, fall back to intelligent selection const selectedAgent = selectBestAgent(multiConfig.subAgents, message, context); const subAgent = { ...config.agent, config: selectedAgent }; const subConfig = { ...config, agent: subAgent }; return await executeAgent(subConfig, session, message, context); }; const extractDelegationDecision = (response) => { // Try to extract delegation decision from response const responseText = response.content.parts .filter(p => p.type === 'text') .map(p => p.text) .join(' '); // Look for delegation patterns const delegatePattern = /delegate to (\w+)|transfer to (\w+)|handoff to (\w+)/i; const match = responseText.match(delegatePattern); if (match) { const targetAgent = match[1] || match[2] || match[3]; return { targetAgent }; } // Check for tool calls that might indicate delegation const toolCalls = response.content.parts.filter(p => p.type === 'function_call'); for (const toolCall of toolCalls) { if (toolCall.type === 'function_call' && toolCall.functionCall) { const funcName = toolCall.functionCall.name; if (funcName === 'delegate' || funcName === 'handoff') { return { targetAgent: toolCall.functionCall.args?.targetAgent }; } } } return null; }; // ========== Multi-Agent Execution ========== const executeMultiAgent = async (config, session, message, context) => { const multiConfig = config.agent.config; switch (multiConfig.delegationStrategy) { case 'sequential': return await executeSequentialAgents(config, session, message, context); case 'parallel': return await executeParallelAgents(config, session, message, context); case 'conditional': return await executeConditionalAgents(config, session, message, context); case 'hierarchical': return await executeHierarchicalAgents(config, session, message, context); default: throwAgentError(`Unknown delegation strategy: ${multiConfig.delegationStrategy}`); } // This should never be reached due to throwAgentError throwing throw new Error('Unreachable code'); }; const executeSequentialAgents = async (config, session, message, context) => { const multiConfig = config.agent.config; let currentSession = session; let currentMessage = message; let finalResponse = null; for (const subAgentConfig of multiConfig.subAgents) { const subAgent = { ...config.agent, config: subAgentConfig }; const subConfig = { ...config, agent: subAgent }; const response = await executeAgent(subConfig, currentSession, currentMessage, context); currentSession = response.session; currentMessage = response.content; finalResponse = response; } return finalResponse; }; const executeParallelAgents = async (config, session, message, context) => { const multiConfig = config.agent.config; const promises = multiConfig.subAgents.map(subAgentConfig => { const subAgent = { ...config.agent, config: subAgentConfig }; const subConfig = { ...config, agent: subAgent }; return executeAgent(subConfig, session, message, context); }); const responses = await Promise.all(promises); // Merge parallel responses intelligently return mergeParallelResponses(responses, multiConfig); }; const executeConditionalAgents = async (config, session, message, context) => { const multiConfig = config.agent.config; // Use coordination rules if provided if (multiConfig.coordinationRules && multiConfig.coordinationRules.length > 0) { return await executeWithCoordinationRules(config, session, message, context, multiConfig); } // Otherwise use intelligent agent selection const selectedAgent = selectBestAgent(multiConfig.subAgents, message, context); const subAgent = { ...config.agent, config: selectedAgent }; const subConfig = { ...config, agent: subAgent }; return await executeAgent(subConfig, session, message, context); }; const executeHierarchicalAgents = async (config, session, message, context) => { const multiConfig = config.agent.config; // Execute coordinator agent first const coordinatorAgent = multiConfig.subAgents[0]; const coordinatorSubAgent = { ...config.agent, config: coordinatorAgent }; const coordinatorConfig = { ...config, agent: coordinatorSubAgent }; const coordinatorResponse = await executeAgent(coordinatorConfig, session, message, context); // Extract delegation decision from coordinator response const delegationDecision = extractDelegationDecision(coordinatorResponse); if (delegationDecision && delegationDecision.targetAgent) { // Find and execute the target agent const targetAgentConfig = multiConfig.subAgents.find(a => a.name === delegationDecision.targetAgent); if (targetAgentConfig) { const targetAgent = { ...config.agent, config: targetAgentConfig }; const targetConfig = { ...config, agent: targetAgent }; return await executeAgent(targetConfig, coordinatorResponse.session, coordinatorResponse.content, context); } } // If no delegation, return coordinator response return coordinatorResponse; }; // ========== Tool Context Creation ========== const createToolContext = (agent, session, message) => { const actions = { transferToAgent: undefined, endConversation: false, setOutputKey: undefined, addArtifact: (key, value) => { // This would update the session addArtifactToSession(session, key, value); }, getArtifact: (key) => { return session.artifacts[key]; } }; return { agent, session, message, actions, metadata: { timestamp: new Date() } }; }; // ========== Guardrails ========== const applyGuardrails = async (guardrails, message, context) => { let currentMessage = message; for (const guardrail of guardrails) { const result = await guardrail(currentMessage, { agent: context.agent, session: context.session, previousMessages: context.previousMessages }); if (!result.allowed) { throwAgentError(`Message blocked by guardrail: ${result.reason}`); } if (result.modifiedMessage) { currentMessage = result.modifiedMessage; } } return currentMessage; }; // ========== Real LLM Integration ========== const callRealLLM = async (agent, message, session) => { try { // Create LLM service instance with environment-based configuration const llmConfig = createAdkLLMConfigFromEnvironment(); const llmService = createAdkLLMService(createAdkLLMServiceConfig(llmConfig)); // Call real LLM const response = await llmService.generateResponse(agent, session, message, { modelOverride: typeof agent.config.model === 'string' ? agent.config.model : undefined, temperature: 0.7, maxTokens: 2000 }); return response.content; } catch (error) { console.error('[ADK:LLM] Real LLM call failed:', error); // Check if this is a critical error that should propagate if (error instanceof Error && error.message.includes('Invalid model')) { throw error; } // Fallback to a simple error response for other errors return createModelMessage(`I apologize, but I'm experiencing technical difficulties. Error: ${error instanceof Error ? error.message : 'Unknown error'}`); } }; // ========== Mock Tool Detection Logic Removed ========== // This mock logic has been removed since real LLM now handles tool calling decisions const generateCallId = () => { return `call_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`; }; // ========== Utility Functions ========== const generateRequestId = () => { // Use crypto-based ID generation for pure functional approach return `req_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`; }; const createAgentEvent = (type, data) => { return { type, timestamp: new Date(), ...data }; }; const isMultiAgent = (agent) => { return agent.config.subAgents !== undefined && agent.config.subAgents.length > 0; }; // ========== Runner Validation ========== export const validateRunnerConfig = (config) => { const errors = []; if (!config.agent) { errors.push('Agent is required'); } if (!config.sessionProvider) { errors.push('Session provider is required'); } if (config.maxLLMCalls && config.maxLLMCalls <= 0) { errors.push('Max LLM calls must be positive'); } if (config.timeout && config.timeout <= 0) { errors.push('Timeout must be positive'); } if (errors.length > 0) { throwAgentError(`Invalid runner config: ${errors.join(', ')}`); } }; export const validateRunContext = (context) => { const errors = []; if (!context.userId || context.userId.trim().length === 0) { errors.push('User ID is required'); } if (errors.length > 0) { throwAgentError(`Invalid run context: ${errors.join(', ')}`); } }; // ========== Runner Builder ========== export const createRunnerConfig = (agent, sessionProvider, options) => { const config = { agent, sessionProvider, ...options }; validateRunnerConfig(config); return config; }; // ========== Error Handling ========== export const withRunnerErrorHandling = (fn, agentId) => { return async (...args) => { try { return await fn(...args); } catch (error) { if (error instanceof Error && (error.name === 'AgentError' || error.name === 'ToolError' || error.name === 'SessionError')) { throw error; } throwAgentError(`Runner operation failed: ${error instanceof Error ? error.message : String(error)}`, agentId, { originalError: error }); // This should never be reached due to throwAgentError throwing throw new Error('Unreachable code'); } }; }; // ========== Runner Statistics ========== export const getRunnerStats = (config) => { return { agentId: config.agent.id, agentName: config.agent.config.name, toolCount: config.agent.config.tools.length, subAgentCount: config.agent.config.subAgents?.length || 0, hasGuardrails: (config.guardrails?.length || 0) > 0, maxLLMCalls: config.maxLLMCalls, timeout: config.timeout, isMultiAgent: isMultiAgent(config.agent) }; }; // ========== Visualization Functions ========== export const generateRunnerVisualization = async (config, options = {}) => { return await generateRunnerGraph(config, options); }; export const generateAgentVisualization = async (agents, options = {}) => { return await generateAgentGraph(agents, options); }; export const generateToolVisualization = async (tools, options = {}) => { return await generateToolGraph(tools, options); }; export const generateRunnerGraphPng = async (config, outputPath) => { const options = { title: `JAF Runner: ${config.agent.config.name}`, outputFormat: 'png', outputPath: outputPath || './runner-visualization', showToolDetails: true, showSubAgents: true, colorScheme: 'modern' }; return await generateRunnerVisualization(config, options); }; //# sourceMappingURL=index.js.map