UNPKG

@gacua/backend

Version:

GACUA Backend

236 lines 9.52 kB
/** * @license * Copyright 2025 MuleRun * SPDX-License-Identifier: Apache-2.0 */ import { AuthType, Config, ApprovalMode } from '@gacua/gemini-cli-core'; import { getAuthType } from '../../auth/gemini.js'; import { runAgent, } from './agent.js'; import { sessionRepository } from '../../repository/index.js'; import { logger } from '../../logger.js'; const agentInterfaceLogger = logger.child({ module: 'agent-interface' }); export async function prepareComputerUseConfig(sessionId, model) { agentInterfaceLogger.info({ sessionId, model }, 'Preparing computer use config'); const authType = getAuthType(); if (!authType || !Object.values(AuthType).includes(authType)) { const errorMessage = `Invalid authType: ${authType}. Valid options are: ${Object.values(AuthType).join(', ')}`; agentInterfaceLogger.error({ authType }, errorMessage); throw new Error(errorMessage); } const config = new Config({ sessionId, targetDir: process.cwd(), // This is not used, let it be the current working directory. debugMode: false, coreTools: [], mcpServers: { '.computer': { httpUrl: process.env['GACUA_MCP_COMPUTER_URL'], }, }, approvalMode: ApprovalMode.YOLO, cwd: process.cwd(), model, }); await config.initialize(); await config.refreshAuth(authType); agentInterfaceLogger.info({ sessionId }, 'Computer use config prepared successfully'); return config; } async function persistentContentBlockToPart(block, sessionId) { if ('thought' in block) { return { text: block.thought, thought: true }; } if ('text' in block) { return { text: block.text }; } if ('functionCall' in block) { return { functionCall: block.functionCall }; } if ('functionResponse' in block) { return { functionResponse: block.functionResponse }; } if ('image' in block && block.image.src.startsWith('internal://')) { const [imageSessionId, fileName] = block.image.src.slice(11).split('/'); if (imageSessionId !== sessionId) { throw new Error(`Image session ID does not match: ${imageSessionId} !== ${sessionId}`); } return { inlineData: { data: (await sessionRepository.getImage(imageSessionId, fileName)).toString('base64'), mimeType: 'image/png', }, }; } throw new Error('Not implemented'); } function partToPersistentContentBlock(part) { if (part.thought) { if (part.functionCall) { throw new Error('If thought included, functionCall must be empty'); } if (!part.text) { throw new Error('If thought included, text must be included'); } return { thought: part.text }; } if (part.text) { if (part.functionCall) { throw new Error('If text included, functionCall must be empty'); } return { text: part.text }; } if (part.functionCall) { return { functionCall: part.functionCall, }; } if (part.functionResponse) { return { functionResponse: part.functionResponse }; } throw new Error('Either thought, text, or functionCall must be included'); } async function recoverHistory(sessionId) { const persistentMessages = await sessionRepository.getMessages(sessionId, true); const getHistoryMessages = async () => await Promise.all(persistentMessages .filter((message) => message.forDisplay !== true) .map(async (message) => ({ role: message.role === 'model' ? 'model' : 'user', parts: await Promise.all(message.content.map(async (block) => { return persistentContentBlockToPart(block, sessionId); })), }))); const toolReviewRequests = []; const toolReviewResponses = []; for (let i = persistentMessages.length - 1; i >= 0; i--) { const persistentMessage = persistentMessages[i]; if (persistentMessage.role === 'tool') { // Tool messages exist between tool reviews. continue; } if (!persistentMessage.toolReview) { break; } const toolReview = persistentMessage.toolReview; if ('functionCall' in toolReview) { toolReviewRequests.unshift(toolReview); } else { toolReviewResponses.unshift(toolReview); } } return { getHistoryMessages, toolReviewRequests, toolReviewResponses }; } export async function runComputerUseAgent(sessionId, input, model, emitEvent) { const setSessionStatus = async (status, message) => { await sessionRepository.updateSession(sessionId, { status, statusMessage: message, }); emitEvent?.({ type: 'session_status', sessionId, payload: { status, message }, }); }; const streamMessage = async (message) => { emitEvent?.({ type: 'stream_message', sessionId, payload: message, }); }; const saveImage = async (imageBuffer, nameSuffix) => { const fileName = `${new Date().toISOString().replace(/[:.]/g, '-')}_${nameSuffix}.png`; await sessionRepository.saveImage(imageBuffer, sessionId, fileName); return fileName; }; const persistMessage = async (message) => { const persistentMessage = { id: Date.now().toString(), role: message.role, content: message.parts.map((part) => { if ('imageFileName' in part) { return { image: { src: `internal://${sessionId}/${part.imageFileName}`, }, }; } return partToPersistentContentBlock(part); }), toolReview: message.toolReview, forDisplay: message.forDisplay, timestamp: new Date(), }; await sessionRepository.appendMessages(sessionId, [persistentMessage]); if (message.forDisplay !== false) { emitEvent?.({ type: 'persistent_message', sessionId, payload: { ...persistentMessage, timestamp: persistentMessage.timestamp.toISOString(), }, }); } }; const agentLogger = logger.child({ module: 'agent', sessionId }); const { getHistoryMessages, toolReviewRequests, toolReviewResponses } = await recoverHistory(sessionId); let agentInput; const sessionAcceptedTools = (await sessionRepository.getSession(sessionId)).acceptedTools || []; if (typeof input !== 'string') { if (!toolReviewRequests.find((r) => r.reviewId === input.reviewId)) { throw new Error(`Can not find the corresponding tool review request for reviewId: ${input.reviewId}`); } if (toolReviewResponses.find((r) => r.reviewId === input.reviewId)) { throw new Error(`The tool review response for reviewId: ${input.reviewId} already exists`); } toolReviewResponses.push(input); await persistMessage({ role: 'user', parts: [], toolReview: input, forDisplay: true, }); if (toolReviewResponses.length < toolReviewRequests.length) { return; } const newSessionAcceptedTools = []; const processedResponses = toolReviewRequests.map((request) => { const response = toolReviewResponses.find((response) => response.reviewId === request.reviewId); if (!response) { throw new Error(`Can not find the corresponding tool review response for reviewId: ${request.reviewId}`); } if (response.choice === 'accept_session') { if (sessionAcceptedTools.includes(request.originalFunctionCall.name)) { throw new Error(`Function ${request.originalFunctionCall.name} is already accepted in this session`); } newSessionAcceptedTools.push(request.originalFunctionCall.name); } return { ...request, response: response.choice }; }); if (newSessionAcceptedTools.length > 0) { sessionAcceptedTools.push(...newSessionAcceptedTools); await sessionRepository.updateSession(sessionId, { acceptedTools: sessionAcceptedTools, }); } agentInput = processedResponses; } else { if (toolReviewResponses.length < toolReviewRequests.length) { throw new Error(`Input is not allowed when there are pending tool review requests`); } agentInput = input; } const config = await prepareComputerUseConfig(sessionId, model ?? (await sessionRepository.getSession(sessionId)).model); const historyMessages = await getHistoryMessages(); try { await runAgent(config, historyMessages, agentInput, sessionAcceptedTools, setSessionStatus, streamMessage, saveImage, persistMessage, agentLogger); } catch (error) { agentInterfaceLogger.error({ error }, 'Internal error while running agent'); setSessionStatus('error', error instanceof Error ? error.message : String(error)); } } //# sourceMappingURL=interface.js.map