diff --git a/src/lib/bot/tools/implementations/spawn-cloud-agent.ts b/src/lib/bot/tools/implementations/spawn-cloud-agent.ts new file mode 100644 index 000000000..49e33644b --- /dev/null +++ b/src/lib/bot/tools/implementations/spawn-cloud-agent.ts @@ -0,0 +1,218 @@ +import { + createCloudAgentClient, + type InitiateSessionInput, +} from '@/lib/cloud-agent/cloud-agent-client'; +import { + getGitHubTokenForUser, + getGitHubTokenForOrganization, +} from '@/lib/cloud-agent/github-integration-helpers'; +import type { BotTool, ToolExecutionContext, ToolResult, RequesterInfo } from '../types'; + +/** + * Arguments for the spawn_cloud_agent tool + */ +type SpawnCloudAgentArgs = { + githubRepo: string; + prompt: string; + mode?: 'architect' | 'code' | 'ask' | 'debug' | 'orchestrator'; +}; + +/** + * Build the PR signature to append to the Cloud Agent prompt. + * This adds attribution for who requested the PR. + */ +function buildPrSignature(requesterInfo: RequesterInfo): string { + const requesterPart = requesterInfo.messagePermalink + ? `[${requesterInfo.displayName}](${requesterInfo.messagePermalink})` + : requesterInfo.displayName; + + return ` + +--- +**PR Signature to include in the PR description:** +When you create a pull request, include the following signature at the end of the PR description: + +Built for ${requesterPart} by [Kilo for Slack](https://kilo.ai/features/slack-integration)`; +} + +/** + * Execute the spawn_cloud_agent tool. + * Spawns a Cloud Agent session and streams the results. + */ +async function executeSpawnCloudAgent( + args: SpawnCloudAgentArgs, + context: ToolExecutionContext +): Promise { + const { owner, authToken, model, requesterInfo } = context; + + console.log('[SpawnCloudAgent] Called with args:', JSON.stringify(args, null, 2)); + console.log('[SpawnCloudAgent] Owner:', JSON.stringify(owner, null, 2)); + + let githubToken: string | undefined; + let kilocodeOrganizationId: string | undefined; + + // Handle organization-owned integrations + if (owner.type === 'org') { + // Get GitHub token for the organization + githubToken = await getGitHubTokenForOrganization(owner.id); + // Set the organization ID for cloud agent usage attribution + kilocodeOrganizationId = owner.id; + } else { + // Get GitHub token for the user + githubToken = await getGitHubTokenForUser(owner.id); + } + + // Skip balance check for bot users - bot integration has its own billing model + const cloudAgentClient = createCloudAgentClient(authToken, { skipBalanceCheck: true }); + + // Append PR signature to the prompt if we have requester info + const promptWithSignature = requesterInfo + ? args.prompt + buildPrSignature(requesterInfo) + : args.prompt; + + const input: InitiateSessionInput = { + githubRepo: args.githubRepo, + prompt: promptWithSignature, + mode: args.mode || 'code', + model: model, + githubToken, + kilocodeOrganizationId, + createdOnPlatform: 'slack', + }; + + const statusMessages: string[] = []; + let completionResult: string | undefined; + let sessionId: string | undefined; + let hasError = false; + + try { + console.log('[SpawnCloudAgent] Starting to stream events from Cloud Agent...'); + for await (const event of cloudAgentClient.initiateSessionStream(input)) { + if (event.sessionId) sessionId = event.sessionId; + + switch (event.streamEventType) { + case 'complete': + statusMessages.push( + `Session completed in ${event.metadata.executionTimeMs}ms with exit code ${event.exitCode}` + ); + break; + case 'error': + statusMessages.push(`Error: ${event.error}`); + hasError = true; + break; + case 'kilocode': { + const payload = event.payload; + if (payload.say === 'completion_result' && typeof payload.content === 'string') { + completionResult = payload.content; + } + break; + } + case 'output': + if (event.source === 'stderr') { + statusMessages.push(`[stderr] ${event.content}`); + hasError = true; + console.log('[SpawnCloudAgent] Error flag set to true'); + } + break; + case 'interrupted': + statusMessages.push(`Session interrupted: ${event.reason}`); + hasError = true; + console.log('[SpawnCloudAgent] Error flag set to true'); + break; + } + } + console.log( + `[SpawnCloudAgent] Stream completed. Total status messages: ${statusMessages.length}, Has completion result: ${!!completionResult}` + ); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.error('[SpawnCloudAgent] Error during stream:', errorMessage, error); + return { + success: false, + response: `Error spawning Cloud Agent: ${errorMessage}`, + metadata: { sessionId }, + }; + } + + if (hasError) { + const errorResult = `Cloud Agent session ${sessionId || 'unknown'} encountered errors:\n${statusMessages.join('\n')}`; + console.log('[SpawnCloudAgent] Returning error result:', errorResult); + return { + success: false, + response: errorResult, + metadata: { sessionId }, + }; + } + + // Return the completion result if available, otherwise show status messages + if (completionResult) { + const successResult = `Cloud Agent session ${sessionId || 'unknown'} completed:\n\n${completionResult}`; + console.log('[SpawnCloudAgent] Returning success result'); + return { + success: true, + response: successResult, + metadata: { sessionId }, + }; + } + + const fallbackResult = `Cloud Agent session ${sessionId || 'unknown'} completed successfully.\n\nStatus:\n${statusMessages.slice(-5).join('\n')}`; + console.log('[SpawnCloudAgent] Returning fallback result:', fallbackResult); + return { + success: true, + response: fallbackResult, + metadata: { sessionId }, + }; +} + +/** + * The spawn_cloud_agent tool definition. + * This tool spawns a Cloud Agent session to perform coding tasks on a GitHub repository. + */ +export const spawnCloudAgentTool: BotTool = { + name: 'spawn_cloud_agent', + requiredIntegration: 'github', + + definition: { + type: 'function', + function: { + name: 'spawn_cloud_agent', + description: + 'Spawn a Cloud Agent session to perform coding tasks on a GitHub repository. The agent can make code changes, fix bugs, implement features, and more.', + parameters: { + type: 'object', + properties: { + githubRepo: { + type: 'string', + description: 'The GitHub repository in owner/repo format (e.g., "facebook/react")', + pattern: '^[-a-zA-Z0-9_.]+/[-a-zA-Z0-9_.]+$', + }, + prompt: { + type: 'string', + description: + 'The task description for the Cloud Agent. Be specific about what changes or analysis you want.', + }, + mode: { + type: 'string', + enum: ['architect', 'code', 'ask', 'debug', 'orchestrator'], + description: + 'The agent mode: "code" for making changes, "architect" for design tasks, "ask" for questions, "debug" for troubleshooting, "orchestrator" for complex multi-step tasks', + default: 'code', + }, + }, + required: ['githubRepo', 'prompt'], + }, + }, + }, + + async execute(args: unknown, context: ToolExecutionContext): Promise { + // Validate and cast args + const typedArgs = args as SpawnCloudAgentArgs; + if (!typedArgs.githubRepo || !typedArgs.prompt) { + return { + success: false, + response: 'Missing required arguments: githubRepo and prompt are required', + }; + } + return executeSpawnCloudAgent(typedArgs, context); + }, +}; diff --git a/src/lib/bot/tools/index.ts b/src/lib/bot/tools/index.ts new file mode 100644 index 000000000..7351c6ea2 --- /dev/null +++ b/src/lib/bot/tools/index.ts @@ -0,0 +1,40 @@ +/** + * Bot Tools System + * + * This module provides a registry and loader for bot tools that can be called + * by the AI model. Tools are registered at module load time and dynamically + * loaded per-request based on which integrations the owner has enabled. + * + * Usage: + * ```typescript + * import { getToolsForOwner, getTool } from '@/lib/bot/tools'; + * + * // Get tools available to an owner + * const tools = await getToolsForOwner(owner); + * + * // Look up a specific tool + * const tool = getTool('spawn_cloud_agent'); + * ``` + */ + +// Export types +export type { BotTool, ToolResult, ToolExecutionContext, RequesterInfo } from './types'; + +// Export registry functions +export { + registerTool, + getTool, + getAllTools, + hasRegisteredTool, + getRegisteredToolCount, +} from './registry'; + +// Export loader functions +export { getToolsForOwner } from './tool-loader'; + +// Register all tool implementations +// This runs when the module is first imported +import { registerTool } from './registry'; +import { spawnCloudAgentTool } from './implementations/spawn-cloud-agent'; + +registerTool(spawnCloudAgentTool); diff --git a/src/lib/bot/tools/registry.ts b/src/lib/bot/tools/registry.ts new file mode 100644 index 000000000..0370fe289 --- /dev/null +++ b/src/lib/bot/tools/registry.ts @@ -0,0 +1,61 @@ +import type { BotTool } from './types'; + +/** + * Global registry of all available bot tools. + * Tools are registered at module load time and looked up at runtime. + */ +const toolRegistry = new Map(); + +/** + * Register a tool in the global registry. + * Should be called at module initialization time. + * + * @param tool - The tool to register + * @throws Error if a tool with the same name is already registered + */ +export function registerTool(tool: BotTool): void { + if (toolRegistry.has(tool.name)) { + throw new Error(`Tool "${tool.name}" is already registered`); + } + toolRegistry.set(tool.name, tool); + console.log(`[ToolRegistry] Registered tool: ${tool.name}`); +} + +/** + * Get a tool by name from the registry. + * + * @param name - The name of the tool to retrieve + * @returns The tool if found, undefined otherwise + */ +export function getTool(name: string): BotTool | undefined { + return toolRegistry.get(name); +} + +/** + * Get all registered tools. + * + * @returns Array of all registered tools + */ +export function getAllTools(): BotTool[] { + return Array.from(toolRegistry.values()); +} + +/** + * Check if a tool is registered. + * + * @param name - The name of the tool to check + * @returns true if the tool is registered + */ +export function hasRegisteredTool(name: string): boolean { + return toolRegistry.has(name); +} + +/** + * Get the count of registered tools. + * Useful for debugging and testing. + * + * @returns Number of registered tools + */ +export function getRegisteredToolCount(): number { + return toolRegistry.size; +} diff --git a/src/lib/bot/tools/tool-loader.ts b/src/lib/bot/tools/tool-loader.ts new file mode 100644 index 000000000..d59fe959d --- /dev/null +++ b/src/lib/bot/tools/tool-loader.ts @@ -0,0 +1,47 @@ +import type { Owner } from '@/lib/integrations/core/types'; +import { getAllActiveIntegrationsForOwner } from '@/lib/integrations/db/platform-integrations'; +import { getAllTools } from './registry'; +import type { BotTool } from './types'; + +/** + * Get the set of active integration platforms for an owner. + * Used to filter which tools are available for a given owner. + * + * @param owner - The owner (user or org) to check integrations for + * @returns Set of platform names that the owner has active integrations for + */ +async function getActiveIntegrationPlatforms(owner: Owner): Promise { + const integrations = await getAllActiveIntegrationsForOwner(owner); + + return integrations.map(integration => integration.platform); +} + +/** + * Get all tools available to an owner based on their active integrations. + * Tools with no requiredIntegration are always available. + * Tools with a requiredIntegration are only available if the owner has that integration. + * + * @param owner - The owner (user or org) to get tools for + * @returns Array of tools available to this owner + */ +export async function getToolsForOwner(owner: Owner): Promise { + const activePlatforms = await getActiveIntegrationPlatforms(owner); + const allTools = getAllTools(); + + const availableTools = allTools.filter(tool => { + // Tools without a required integration are always available + if (!tool.requiredIntegration) { + return true; + } + + // Tools with a required integration are only available if owner has it + return activePlatforms.includes(tool.requiredIntegration); + }); + + console.log( + `[ToolLoader] Owner has ${activePlatforms.length} active integrations, ` + + `${availableTools.length}/${allTools.length} tools available` + ); + + return availableTools; +} diff --git a/src/lib/bot/tools/types.ts b/src/lib/bot/tools/types.ts new file mode 100644 index 000000000..8d177bcb7 --- /dev/null +++ b/src/lib/bot/tools/types.ts @@ -0,0 +1,64 @@ +import type OpenAI from 'openai'; +import type { Owner } from '@/lib/integrations/core/types'; + +/** + * Result returned from executing a tool + */ +export type ToolResult = { + success: boolean; + response: string; + metadata?: Record; +}; + +/** + * Information about the user who triggered the bot request. + * Used for attribution in PRs and other tool outputs. + */ +export type RequesterInfo = { + displayName: string; + messagePermalink?: string; +}; + +/** + * Context passed to tool execution. + * Contains all information a tool needs to perform its action. + */ +export type ToolExecutionContext = { + /** The owner (user or org) of the integration */ + owner: Owner; + /** Authentication token for API calls */ + authToken: string; + /** The AI model being used */ + model: string; + /** Information about the user who triggered the request */ + requesterInfo?: RequesterInfo; +}; + +/** + * A bot tool that can be called by the AI model. + * + * Tools are registered at startup and dynamically loaded per-request + * based on which integrations the owner has enabled. + */ +export type BotTool = { + /** Unique name of the tool (must match function.name in definition) */ + name: string; + + /** + * The integration required for this tool to be available. + * If specified, the tool will only be included when the owner has this integration. + * Examples: 'github', 'gitlab', 'sentry', 'axiom' + */ + requiredIntegration?: string; + + /** OpenAI function calling tool definition */ + definition: OpenAI.Chat.Completions.ChatCompletionTool; + + /** + * Execute the tool with the given arguments. + * @param args - Parsed arguments from the AI model's tool call + * @param context - Execution context with owner, auth, and model info + * @returns Result containing success status and response text + */ + execute(args: unknown, context: ToolExecutionContext): Promise; +}; diff --git a/src/lib/integrations/db/platform-integrations.ts b/src/lib/integrations/db/platform-integrations.ts index c64ac9b22..1955c33d2 100644 --- a/src/lib/integrations/db/platform-integrations.ts +++ b/src/lib/integrations/db/platform-integrations.ts @@ -443,15 +443,23 @@ export async function getIntegrationForOwner(owner: Owner, platform: string) { } /** - * Gets all platform integrations for an owner (user or organization) + * Gets all active platform integrations for an owner (user or organization) */ -export async function getAllIntegrationsForOwner(owner: Owner) { +export async function getAllActiveIntegrationsForOwner(owner: Owner) { const ownershipCondition = owner.type === 'user' ? eq(platform_integrations.owned_by_user_id, owner.id) : eq(platform_integrations.owned_by_organization_id, owner.id); - const integrations = await db.select().from(platform_integrations).where(ownershipCondition); + const integrations = await db + .select() + .from(platform_integrations) + .where( + and( + ownershipCondition, + eq(platform_integrations.integration_status, INTEGRATION_STATUS.ACTIVE) + ) + ); return integrations; } diff --git a/src/lib/slack-bot.ts b/src/lib/slack-bot.ts index 568ae1cb1..67c8e7647 100644 --- a/src/lib/slack-bot.ts +++ b/src/lib/slack-bot.ts @@ -1,13 +1,5 @@ -import { - createCloudAgentClient, - type InitiateSessionInput, -} from '@/lib/cloud-agent/cloud-agent-client'; -import { - getGitHubTokenForUser, - getGitHubTokenForOrganization, -} from '@/lib/cloud-agent/github-integration-helpers'; import type OpenAI from 'openai'; -import type { Owner } from '@/lib/integrations/core/types'; +import { getTool, type ToolExecutionContext, getToolsForOwner } from '@/lib/bot/tools'; import { getInstallationByTeamId, getOwnerFromInstallation, @@ -100,49 +92,6 @@ Your prompt to the agent should usually include: - Don’t fabricate links (including PR URLs). - If you can’t proceed (missing repo, missing details, permissions), say what’s missing and what you need next.`; -/** - * Tool definition for spawning Cloud Agent sessions - */ -const SPAWN_CLOUD_AGENT_TOOL: OpenAI.Chat.Completions.ChatCompletionTool = { - type: 'function', - function: { - name: 'spawn_cloud_agent', - description: - 'Spawn a Cloud Agent session to perform coding tasks on a GitHub repository. The agent can make code changes, fix bugs, implement features, and more.', - parameters: { - type: 'object', - properties: { - githubRepo: { - type: 'string', - description: 'The GitHub repository in owner/repo format (e.g., "facebook/react")', - pattern: '^[-a-zA-Z0-9_.]+/[-a-zA-Z0-9_.]+$', - }, - prompt: { - type: 'string', - description: - 'The task description for the Cloud Agent. Be specific about what changes or analysis you want.', - }, - mode: { - type: 'string', - enum: ['architect', 'code', 'ask', 'debug', 'orchestrator'], - description: - 'The agent mode: "code" for making changes, "architect" for design tasks, "ask" for questions, "debug" for troubleshooting, "orchestrator" for complex multi-step tasks', - default: 'code', - }, - }, - required: ['githubRepo', 'prompt'], - }, - }, -}; - -/** - * Result from spawning a Cloud Agent session - */ -type SpawnCloudAgentResult = { - response: string; - sessionId?: string; -}; - /** * Information about the Slack user who requested the PR */ @@ -151,23 +100,6 @@ type SlackRequesterInfo = { messagePermalink?: string; }; -/** - * Build the PR signature to append to the Cloud Agent prompt - */ -function buildPrSignature(requesterInfo: SlackRequesterInfo): string { - const requesterPart = requesterInfo.messagePermalink - ? `[${requesterInfo.displayName}](${requesterInfo.messagePermalink})` - : requesterInfo.displayName; - - return ` - ---- -**PR Signature to include in the PR description:** -When you create a pull request, include the following signature at the end of the PR description: - -Built for ${requesterPart} by [Kilo for Slack](https://kilo.ai/features/slack-integration)`; -} - /** * Fetch the requester info for PR signatures * Gets the user's display name and a permalink to the triggering message @@ -211,124 +143,6 @@ async function getSlackRequesterInfo( }; } -/** - * Spawn a Cloud Agent session and collect the results - */ -async function spawnCloudAgentSession( - args: { - githubRepo: string; - prompt: string; - mode?: string; - }, - owner: Owner, - model: string, - authToken: string, - requesterInfo?: SlackRequesterInfo -): Promise { - console.log('[SlackBot] spawnCloudAgentSession called with args:', JSON.stringify(args, null, 2)); - console.log('[SlackBot] Owner:', JSON.stringify(owner, null, 2)); - - let githubToken: string | undefined; - let kilocodeOrganizationId: string | undefined; - - // Handle organization-owned integrations - if (owner.type === 'org') { - // Get GitHub token for the organization - githubToken = await getGitHubTokenForOrganization(owner.id); - - // Set the organization ID for cloud agent usage attribution - kilocodeOrganizationId = owner.id; - } else { - // Get GitHub token for the user - githubToken = await getGitHubTokenForUser(owner.id); - } - - // Skip balance check for Slackbot users - Slack integration has its own billing model - const cloudAgentClient = createCloudAgentClient(authToken, { skipBalanceCheck: true }); - - // Append PR signature to the prompt if we have requester info - const promptWithSignature = requesterInfo - ? args.prompt + buildPrSignature(requesterInfo) - : args.prompt; - - const input: InitiateSessionInput = { - githubRepo: args.githubRepo, - prompt: promptWithSignature, - mode: (args.mode as InitiateSessionInput['mode']) || 'code', - model: model, - githubToken, - kilocodeOrganizationId, - createdOnPlatform: 'slack', - }; - - const statusMessages: string[] = []; - let completionResult: string | undefined; - let sessionId: string | undefined; - let hasError = false; - - try { - console.log('[SlackBot] Starting to stream events from Cloud Agent...'); - for await (const event of cloudAgentClient.initiateSessionStream(input)) { - if (event.sessionId) sessionId = event.sessionId; - - switch (event.streamEventType) { - case 'complete': - statusMessages.push( - `Session completed in ${event.metadata.executionTimeMs}ms with exit code ${event.exitCode}` - ); - break; - case 'error': - statusMessages.push(`Error: ${event.error}`); - hasError = true; - break; - case 'kilocode': { - const payload = event.payload; - if (payload.say === 'completion_result' && typeof payload.content === 'string') { - completionResult = payload.content; - } - break; - } - case 'output': - if (event.source === 'stderr') { - statusMessages.push(`[stderr] ${event.content}`); - hasError = true; - console.log('[SlackBot] Error flag set to true'); - } - break; - case 'interrupted': - statusMessages.push(`Session interrupted: ${event.reason}`); - hasError = true; - console.log('[SlackBot] Error flag set to true'); - break; - } - } - console.log( - `[SlackBot] Stream completed. Total status messages: ${statusMessages.length}, Has completion result: ${!!completionResult}` - ); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - console.error('[SlackBot] Error during stream:', errorMessage, error); - return { response: `Error spawning Cloud Agent: ${errorMessage}`, sessionId }; - } - - if (hasError) { - const errorResult = `Cloud Agent session ${sessionId || 'unknown'} encountered errors:\n${statusMessages.join('\n')}`; - console.log('[SlackBot] Returning error result:', errorResult); - return { response: errorResult, sessionId }; - } - - // Return the completion result if available, otherwise show status messages - if (completionResult) { - const successResult = `Cloud Agent session ${sessionId || 'unknown'} completed:\n\n${completionResult}`; - console.log('[SlackBot] Returning success result'); - return { response: successResult, sessionId }; - } - - const fallbackResult = `Cloud Agent session ${sessionId || 'unknown'} completed successfully.\n\nStatus:\n${statusMessages.slice(-5).join('\n')}`; - console.log('[SlackBot] Returning fallback result:', fallbackResult); - return { response: fallbackResult, sessionId }; -} - type ChatMessage = OpenAI.Chat.Completions.ChatCompletionMessageParam; type ChatCompletionResponse = OpenAI.Chat.Completions.ChatCompletion; @@ -440,6 +254,19 @@ export async function processKiloBotMessage( const systemPrompt = KILO_BOT_SYSTEM_PROMPT + slackContextForPrompt + formatGitHubRepositoriesForPrompt(repoContext); + // Get available tools for this owner (based on their integrations) + const availableTools = await getToolsForOwner(owner); + const availableToolDefinitions = availableTools.map(tool => tool.definition); + console.log('[SlackBot] Available tools:', JSON.stringify(availableTools.map(tool => tool.name))); + + // Build tool execution context for later use + const toolExecutionContext: ToolExecutionContext = { + owner, + authToken, + model: selectedModel, + requesterInfo: slackRequesterInfo, + }; + // Build initial messages array const messages: ChatMessage[] = [ { @@ -470,8 +297,8 @@ export async function processKiloBotMessage( body: { model: selectedModel, messages, - tools: [SPAWN_CLOUD_AGENT_TOOL], - tool_choice: 'auto', + tools: availableToolDefinitions.length > 0 ? availableToolDefinitions : undefined, + tool_choice: availableToolDefinitions.length > 0 ? 'auto' : undefined, }, organizationId: owner.type === 'org' ? owner.id : undefined, }); @@ -531,56 +358,57 @@ export async function processKiloBotMessage( continue; } - // Track the tool call - toolCallsMade.push(toolCall.function.name); - - if (toolCall.function.name === 'spawn_cloud_agent') { - console.log( - '[SlackBot] spawn_cloud_agent tool call - arguments:', - toolCall.function.arguments - ); - try { - const args = JSON.parse(toolCall.function.arguments); - console.log('[SlackBot] Parsed tool arguments:', JSON.stringify(args, null, 2)); - - console.log('[SlackBot] Calling spawnCloudAgentSession...'); - const toolResult = await spawnCloudAgentSession( - args, - owner, - selectedModel, - authToken, - slackRequesterInfo - ); - console.log('[SlackBot] Tool result received, length:', toolResult.response.length); - console.log('[SlackBot] Tool result preview:', toolResult.response.slice(0, 100)); - - // Track the cloud agent session ID - if (toolResult.sessionId) { - cloudAgentSessionId = toolResult.sessionId; - } - - // Add tool result to conversation history - messages.push({ - role: 'tool', - tool_call_id: toolCall.id, - content: toolResult.response, - }); - console.log( - '[SlackBot] Added tool result to history, total messages:', - messages.length - ); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - console.error('[SlackBot] Error executing tool:', errMsg, error); - errorMessage = errMsg; - messages.push({ - role: 'tool', - tool_call_id: toolCall.id, - content: `Error executing tool: ${errMsg}`, - }); + const toolName = toolCall.function.name; + toolCallsMade.push(toolName); + + // Look up the tool in the registry + const tool = getTool(toolName); + if (!tool) { + console.log('[SlackBot] Unknown tool:', toolName); + messages.push({ + role: 'tool', + tool_call_id: toolCall.id, + content: `Error: Unknown tool "${toolName}"`, + }); + continue; + } + + console.log( + '[SlackBot] Executing tool:', + toolName, + 'arguments:', + toolCall.function.arguments + ); + + try { + const args = JSON.parse(toolCall.function.arguments); + console.log('[SlackBot] Parsed tool arguments:', JSON.stringify(args, null, 2)); + + const toolResult = await tool.execute(args, toolExecutionContext); + console.log('[SlackBot] Tool result received, length:', toolResult.response.length); + console.log('[SlackBot] Tool result preview:', toolResult.response.slice(0, 100)); + + // Track cloud agent session ID if present in metadata + if (toolResult.metadata?.sessionId) { + cloudAgentSessionId = toolResult.metadata.sessionId as string; } - } else { - console.log('[SlackBot] Unknown tool:', toolCall.function.name); + + // Add tool result to conversation history + messages.push({ + role: 'tool', + tool_call_id: toolCall.id, + content: toolResult.response, + }); + console.log('[SlackBot] Added tool result to history, total messages:', messages.length); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + console.error('[SlackBot] Error executing tool:', errMsg, error); + errorMessage = errMsg; + messages.push({ + role: 'tool', + tool_call_id: toolCall.id, + content: `Error executing tool: ${errMsg}`, + }); } } } else {