diff --git a/src/components/agent/AgentExecutionPanel.tsx b/src/components/agent/AgentExecutionPanel.tsx deleted file mode 100644 index 54ada33a5..000000000 --- a/src/components/agent/AgentExecutionPanel.tsx +++ /dev/null @@ -1,236 +0,0 @@ -/** - * Agent Execution Panel Component - * - * Detailed view of agent execution progress, tool executions, and results. - * Expandable panel that shows real-time execution details. - */ - -import { memo, useMemo } from 'react'; -import { useAppSelector } from '../../store/hooks'; -import { - selectActiveExecutionForThread, - selectExecutionHistoryForThread, - selectAgentModeForThread -} from '../../store/agentSlice'; -import type { AgentToolExecution } from '../../types/agent'; - -interface AgentExecutionPanelProps { - threadId: string; - className?: string; - maxHeight?: string; -} - -const formatDuration = (ms: number): string => { - if (ms < 1000) return `${ms}ms`; - if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`; - return `${Math.floor(ms / 60000)}m ${Math.floor((ms % 60000) / 1000)}s`; -}; - -const getStatusIcon = (status: string) => { - switch (status) { - case 'pending': - return ( - - - - ); - case 'running': - return ( -
- ); - case 'success': - return ( - - - - ); - case 'error': - return ( - - - - ); - default: - return ( -
- ); - } -}; - -const ToolExecutionItem = memo<{ toolExecution: AgentToolExecution }>(({ toolExecution }) => { - const duration = toolExecution.executionTimeMs || (toolExecution.endTime ? toolExecution.endTime - toolExecution.startTime : null); - - return ( -
-
- {getStatusIcon(toolExecution.status)} -
- -
-
- {toolExecution.toolName} - - {toolExecution.skillId} - - {duration && ( - - {formatDuration(duration)} - - )} -
- - {/* Arguments */} - {toolExecution.arguments && ( -
- Arguments: -
-              {JSON.stringify(JSON.parse(toolExecution.arguments), null, 2)}
-            
-
- )} - - {/* Result */} - {toolExecution.result && ( -
- Result: -
- {toolExecution.result} -
-
- )} - - {/* Error */} - {toolExecution.errorMessage && ( -
- Error: -
- {toolExecution.errorMessage} -
-
- )} -
-
- ); -}); - -ToolExecutionItem.displayName = 'ToolExecutionItem'; - -const AgentExecutionPanel = memo(({ - threadId, - className = '', - maxHeight = '400px' -}) => { - const agentMode = useAppSelector(state => selectAgentModeForThread(state, threadId)); - const activeExecution = useAppSelector(state => selectActiveExecutionForThread(state, threadId)); - const executionHistory = useAppSelector(state => selectExecutionHistoryForThread(state, threadId)); - - const sortedToolExecutions = useMemo(() => { - if (!activeExecution) return []; - return [...activeExecution.toolExecutions].sort((a, b) => a.startTime - b.startTime); - }, [activeExecution]); - - const recentHistory = useMemo(() => { - return executionHistory.slice(0, 3); // Show last 3 completed executions - }, [executionHistory]); - - if (!agentMode) { - return null; - } - - return ( -
-
-

Agent Execution Details

-
- -
- {/* Active Execution */} - {activeExecution && ( -
-
-

Current Execution

- - Running for {formatDuration(Date.now() - activeExecution.startTime)} - -
- -
-
Progress:
-
-
-
-
- - {activeExecution.currentIteration}/{activeExecution.maxIterations} - -
-
- - {/* Tool Executions */} - {sortedToolExecutions.length > 0 && ( -
-
- Tool Executions ({sortedToolExecutions.length}): -
- {sortedToolExecutions.map(toolExecution => ( - - ))} -
- )} -
- )} - - {/* Execution History */} - {recentHistory.length > 0 && ( -
-

Recent Executions

-
- {recentHistory.map(entry => ( -
-
-
- - {entry.result.status} - - - {entry.result.toolExecutions.length} tools - -
-
- {formatDuration(entry.duration)} -
-
- ))} -
-
- )} - - {/* Empty State */} - {!activeExecution && recentHistory.length === 0 && ( -
- - - -

No agent executions yet

-

Send a message to start an agent task

-
- )} -
-
- ); -}); - -AgentExecutionPanel.displayName = 'AgentExecutionPanel'; - -export default AgentExecutionPanel; \ No newline at end of file diff --git a/src/components/agent/AgentStatusIndicator.tsx b/src/components/agent/AgentStatusIndicator.tsx deleted file mode 100644 index 87e4d9238..000000000 --- a/src/components/agent/AgentStatusIndicator.tsx +++ /dev/null @@ -1,96 +0,0 @@ -/** - * Agent Status Indicator Component - * - * Shows the current status of agent execution within thread UI. - * Displays real-time agent activity, tool executions, and completion status. - */ - -import { memo } from 'react'; -import { useAppSelector } from '../../store/hooks'; -import { selectActiveExecutionForThread, selectAgentModeForThread } from '../../store/agentSlice'; - -interface AgentStatusIndicatorProps { - threadId: string; - className?: string; -} - -const AgentStatusIndicator = memo(({ threadId, className = '' }) => { - const agentMode = useAppSelector(state => selectAgentModeForThread(state, threadId)); - const activeExecution = useAppSelector(state => selectActiveExecutionForThread(state, threadId)); - - // Don't render if agent mode is disabled - if (!agentMode) { - return null; - } - - // No active execution - if (!activeExecution) { - return ( -
-
- Agent Ready -
- ); - } - - const getStatusColor = () => { - switch (activeExecution.status) { - case 'initializing': - return 'bg-amber-500'; - case 'running': - return 'bg-primary-500 animate-pulse'; - case 'completing': - return 'bg-sage-500'; - default: - return 'bg-canvas-400'; - } - }; - - const getStatusText = () => { - switch (activeExecution.status) { - case 'initializing': - return 'Starting...'; - case 'running': - return `Iteration ${activeExecution.currentIteration}/${activeExecution.maxIterations}`; - case 'completing': - return 'Finishing...'; - default: - return 'Agent Active'; - } - }; - - const toolCount = activeExecution.toolExecutions.length; - const runningTools = activeExecution.toolExecutions.filter(t => t.status === 'running').length; - - return ( -
- {/* Status indicator */} -
-
- {getStatusText()} -
- - {/* Tool execution info */} - {toolCount > 0 && ( -
- - - - {toolCount} tools - {runningTools > 0 && ( - • {runningTools} running - )} -
- )} - - {/* Execution time */} -
- {Math.floor((Date.now() - activeExecution.startTime) / 1000)}s -
-
- ); -}); - -AgentStatusIndicator.displayName = 'AgentStatusIndicator'; - -export default AgentStatusIndicator; \ No newline at end of file diff --git a/src/components/agent/AgentToggle.tsx b/src/components/agent/AgentToggle.tsx deleted file mode 100644 index 7f648d62a..000000000 --- a/src/components/agent/AgentToggle.tsx +++ /dev/null @@ -1,162 +0,0 @@ -/** - * Agent Toggle Component - * - * Toggle switch to enable/disable agent mode for a thread. - * Shows agent status and allows configuration when enabled. - */ - -import { memo, useCallback, useState } from 'react'; -import { useAppDispatch, useAppSelector } from '../../store/hooks'; -import { - selectAgentModeForThread, - selectAgentConfigForThread, - selectActiveExecutionForThread, - setAgentModeForThread, - loadAgentTools -} from '../../store/agentSlice'; - -interface AgentToggleProps { - threadId: string; - className?: string; - size?: 'sm' | 'md' | 'lg'; -} - -const AgentToggle = memo(({ - threadId, - className = '', - size = 'md' -}) => { - const dispatch = useAppDispatch(); - const agentMode = useAppSelector(state => selectAgentModeForThread(state, threadId)); - const agentConfig = useAppSelector(state => selectAgentConfigForThread(state, threadId)); - const activeExecution = useAppSelector(state => selectActiveExecutionForThread(state, threadId)); - const [isLoading, setIsLoading] = useState(false); - - const handleToggle = useCallback(async () => { - if (activeExecution) { - // Can't disable while agent is running - return; - } - - setIsLoading(true); - - try { - const newMode = !agentMode; - - // Enable agent mode - if (newMode) { - // Load tools when enabling agent mode - await dispatch(loadAgentTools()).unwrap(); - } - - dispatch(setAgentModeForThread({ - threadId, - enabled: newMode - })); - } catch (error) { - console.error('Failed to toggle agent mode:', error); - } finally { - setIsLoading(false); - } - }, [dispatch, threadId, agentMode, activeExecution]); - - const getSizeClasses = () => { - switch (size) { - case 'sm': - return { - container: 'w-8 h-5', - toggle: 'w-3 h-3', - translate: 'translate-x-3' - }; - case 'lg': - return { - container: 'w-12 h-7', - toggle: 'w-5 h-5', - translate: 'translate-x-5' - }; - default: // md - return { - container: 'w-10 h-6', - toggle: 'w-4 h-4', - translate: 'translate-x-4' - }; - } - }; - - const sizeClasses = getSizeClasses(); - const isDisabled = isLoading || Boolean(activeExecution); - - return ( -
- {/* Toggle Switch */} - - - {/* Label and Status */} -
-
- - Agent Mode - - - {agentMode && ( - - Active - - )} -
- - {/* Configuration hint */} - {agentMode && !activeExecution && ( -
- {agentConfig.maxIterations ? `Max ${agentConfig.maxIterations} iterations` : 'Default settings'} - {agentConfig.allowedSkills && agentConfig.allowedSkills.length > 0 && - ` • ${agentConfig.allowedSkills.length} skills allowed` - } -
- )} - - {/* Active execution status */} - {activeExecution && ( -
- Running iteration {activeExecution.currentIteration}/{activeExecution.maxIterations} -
- )} -
-
- ); -}); - -AgentToggle.displayName = 'AgentToggle'; - -export default AgentToggle; \ No newline at end of file diff --git a/src/components/agent/index.ts b/src/components/agent/index.ts deleted file mode 100644 index 3305c8fdb..000000000 --- a/src/components/agent/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Agent Components Export Index - * - * Centralized exports for all agent-related UI components. - */ - -export { default as AgentStatusIndicator } from './AgentStatusIndicator'; -export { default as AgentToggle } from './AgentToggle'; -export { default as AgentExecutionPanel } from './AgentExecutionPanel'; - -export type { default as AgentStatusIndicatorProps } from './AgentStatusIndicator'; -export type { default as AgentToggleProps } from './AgentToggle'; -export type { default as AgentExecutionPanelProps } from './AgentExecutionPanel'; \ No newline at end of file diff --git a/src/pages/Conversations.tsx b/src/pages/Conversations.tsx index 182642c75..d9f4eb2d6 100644 --- a/src/pages/Conversations.tsx +++ b/src/pages/Conversations.tsx @@ -9,10 +9,10 @@ import { import Markdown from 'react-markdown'; import { useNavigate, useParams } from 'react-router-dom'; -import { inferenceApi, type ModelInfo } from '../services/api/inferenceApi'; +import { inferenceApi, type ModelInfo, type Tool, type ChatMessage } from '../services/api/inferenceApi'; +import { AgentToolRegistry } from '../services/agentToolRegistry'; import { injectAll } from '../lib/ai/injector'; import type { Message } from '../lib/ai/providers/interface'; -import { AgentToggle, AgentStatusIndicator, AgentExecutionPanel } from '../components/agent'; import { useAppDispatch, useAppSelector } from '../store/hooks'; import { addInferenceResponse, @@ -278,21 +278,110 @@ const Conversations = () => { // Continue with original message } - const chatMessages = [ + // Load available tools for transparent tool calling + const toolRegistry = AgentToolRegistry.getInstance(); + let availableTools: Tool[] = []; + + try { + const toolSchemas = await toolRegistry.loadToolSchemas(); + availableTools = toolSchemas.map(schema => ({ + type: 'function' as const, + function: { + name: schema.function.name, + description: schema.function.description, + parameters: schema.function.parameters + } + })); + console.log(`🔧 Loaded ${availableTools.length} tools for transparent execution`); + } catch (error) { + console.warn('⚠️ Failed to load tools, continuing without tool support:', error); + } + + const chatMessages: ChatMessage[] = [ ...historySnapshot.map(m => ({ - role: (m.sender === 'user' ? 'user' : 'assistant') as 'user' | 'assistant', + role: (m.sender === 'user' ? 'user' : 'assistant') as ChatMessage['role'], content: m.content, })), { role: 'user' as const, content: processedUserContent }, ]; - const response = await inferenceApi.createChatCompletion({ - model: selectedModel, - messages: chatMessages, - }); + // Tool calling loop - continue until no more tool calls needed + let currentMessages = [...chatMessages]; + let finalResponse = ''; + let iterations = 0; + const maxIterations = 10; - const content = response.choices[0]?.message?.content ?? ''; - dispatch(addInferenceResponse({ content })); + while (iterations < maxIterations) { + iterations++; + console.log(`🔄 Tool calling iteration ${iterations}`); + + const response = await inferenceApi.createChatCompletion({ + model: selectedModel, + messages: currentMessages, + tools: availableTools.length > 0 ? availableTools : undefined, + tool_choice: availableTools.length > 0 ? 'auto' : undefined, + }); + + const assistantMessage = response.choices[0]?.message; + if (!assistantMessage) { + throw new Error('No assistant message in response'); + } + + // Add assistant message to conversation + currentMessages.push({ + role: 'assistant', + content: assistantMessage.content, + tool_calls: assistantMessage.tool_calls, + }); + + // If no tool calls, we're done + if (!assistantMessage.tool_calls || assistantMessage.tool_calls.length === 0) { + finalResponse = assistantMessage.content || ''; + break; + } + + console.log(`🛠️ Executing ${assistantMessage.tool_calls.length} tool calls`); + + // Execute each tool call + for (const toolCall of assistantMessage.tool_calls) { + try { + const { function: { name: toolName, arguments: toolArgs } } = toolCall; + + // Extract skill ID from tool name (format: skillId_toolName) + const underscoreIndex = toolName.lastIndexOf('_'); + const skillId = underscoreIndex > -1 ? toolName.substring(0, underscoreIndex) : 'unknown'; + + console.log(`⚡ Executing tool: ${toolName} with args: ${toolArgs}`); + + const execution = await toolRegistry.executeTool(skillId, toolName, toolArgs); + + // Add tool result to conversation + currentMessages.push({ + role: 'tool', + content: execution.result || execution.errorMessage || 'Tool executed', + tool_call_id: toolCall.id, + }); + + console.log(`✅ Tool ${toolName} completed: ${execution.status}`); + } catch (error) { + console.error(`❌ Tool execution failed for ${toolCall.function.name}:`, error); + + // Add error result to conversation + currentMessages.push({ + role: 'tool', + content: `Tool execution failed: ${error}`, + tool_call_id: toolCall.id, + }); + } + } + } + + if (iterations >= maxIterations) { + console.warn(`⚠️ Tool calling loop exceeded maximum iterations (${maxIterations})`); + finalResponse = finalResponse || 'Task completed with maximum iterations reached.'; + } + + dispatch(addInferenceResponse({ content: finalResponse })); } catch (err) { dispatch(removeOptimisticMessages()); const msg = @@ -596,9 +685,6 @@ const Conversations = () => { Created {formatRelativeTime(selectedThread.createdAt)}

-
- -
{/* Messages */} @@ -743,18 +829,6 @@ const Conversations = () => {
)} - {/* Agent Status and Execution Panel */} -
- - -
{/* Message Input */}
diff --git a/src/services/agentLoop.ts b/src/services/agentLoop.ts deleted file mode 100644 index 231f344a8..000000000 --- a/src/services/agentLoop.ts +++ /dev/null @@ -1,388 +0,0 @@ -/** - * Agent Loop Service - * - * Orchestrates autonomous agent task execution by: - * 1. Loading tools from the existing skill system - * 2. Sending requests to the backend (which proxies to AI providers) - * 3. Executing tool calls using the skill system - * 4. Managing conversation state and iteration - */ - -import { AgentToolRegistry } from './agentToolRegistry'; -import { apiClient } from './apiClient'; -import type { - AgentExecutionOptions, - AgentExecutionResult, - AgentToolExecution, - AgentChatRequest, - AgentChatResponse, - OpenAIMessage, - OpenAITool, - IAgentLoop -} from '../types/agent'; - -export class AgentLoop implements IAgentLoop { - private static instance: AgentLoop; - private toolRegistry: AgentToolRegistry; - private activeExecutions = new Map(); - - constructor() { - this.toolRegistry = AgentToolRegistry.getInstance(); - } - - static getInstance(): AgentLoop { - if (!this.instance) { - this.instance = new AgentLoop(); - } - return this.instance; - } - - /** - * Execute an agent task autonomously - */ - async executeTask( - userMessage: string, - threadId: string, - options: AgentExecutionOptions = {} - ): Promise { - const { - maxIterations = 10, - timeoutMs = 300000, // 5 minutes - requireApproval = false, - allowedSkills, - blockedTools = [], - retryFailedTools = false - } = options; - - const executionId = `agent_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; - const abortController = new AbortController(); - this.activeExecutions.set(executionId, abortController); - - const startTime = Date.now(); - const toolExecutions: AgentToolExecution[] = []; - let iterations = 0; - - try { - console.log(`🤖 Starting agent task execution (${executionId})`); - console.log(`📝 User message: "${userMessage}"`); - console.log(`⚙️ Options:`, { maxIterations, timeoutMs, allowedSkills, blockedTools }); - - // Set up timeout - const timeoutId = setTimeout(() => { - console.log(`⏰ Agent execution timeout (${timeoutMs}ms)`); - abortController.abort(); - }, timeoutMs); - - try { - // Load available tools from skill system - console.log('🔧 Loading available tools from skills...'); - const toolSchemas = await this.toolRegistry.loadToolSchemas(); - - // Filter tools based on configuration - const availableTools = this.filterTools(toolSchemas, allowedSkills, blockedTools); - console.log(`🛠️ Agent has access to ${availableTools.length} tools from ${toolSchemas.length} total`); - - // Convert to OpenAI format for backend compatibility - const tools = availableTools.map(this.convertToOpenAITool); - - // Initialize conversation with user message - const messages: OpenAIMessage[] = [ - { - role: 'user', - content: userMessage - } - ]; - - let finalResponse: string | undefined; - - // Agent iteration loop - while (iterations < maxIterations && !abortController.signal.aborted) { - iterations++; - console.log(`🔄 Agent iteration ${iterations}/${maxIterations}`); - - try { - // Send request to backend (which proxies to AI provider) - const request: AgentChatRequest = { - model: 'gpt-4', // Backend will handle the actual model - messages: [...messages], - tools, - tool_choice: 'auto', - temperature: 0.7, - max_tokens: 4096 - }; - - console.log('📤 Sending request to backend proxy...'); - const response = await apiClient.post( - `/api/v1/conversations/${threadId}/messages`, - request, - { - signal: abortController.signal - } - ); - - const assistantMessage = response.data.choices[0]?.message; - if (!assistantMessage) { - throw new Error('No response from AI provider'); - } - - console.log(`📥 Received response: ${assistantMessage.tool_calls?.length || 0} tool calls`); - - // Add assistant message to conversation - messages.push(assistantMessage); - - // Check if AI wants to call tools - if (assistantMessage.tool_calls && assistantMessage.tool_calls.length > 0) { - console.log(`🛠️ Executing ${assistantMessage.tool_calls.length} tool calls...`); - - // Execute each tool call - for (const toolCall of assistantMessage.tool_calls) { - if (abortController.signal.aborted) { - break; - } - - const execution = await this.executeSingleTool( - toolCall, - availableTools, - requireApproval, - abortController.signal - ); - - toolExecutions.push(execution); - - // Add tool result to conversation - messages.push({ - role: 'tool', - content: execution.result || execution.errorMessage || 'No result', - tool_call_id: toolCall.id - }); - - console.log(`✅ Tool result added to conversation: ${execution.status}`); - } - - // Continue to next iteration to let AI process tool results - continue; - } else { - // AI provided final response - finalResponse = assistantMessage.content || ''; - console.log('✅ Agent task completed with final response'); - break; - } - - } catch (error) { - console.error(`❌ Error in agent iteration ${iterations}:`, error); - - if (abortController.signal.aborted) { - clearTimeout(timeoutId); - return { - status: 'timeout', - executionId, - iterations, - toolExecutions, - executionTime: Date.now() - startTime, - error: 'Execution timed out' - }; - } - - clearTimeout(timeoutId); - return { - status: 'error', - executionId, - iterations, - toolExecutions, - executionTime: Date.now() - startTime, - error: error instanceof Error ? error.message : String(error) - }; - } - } - - clearTimeout(timeoutId); - - // Check if we hit max iterations - if (iterations >= maxIterations && !finalResponse) { - console.log('⚠️ Agent reached maximum iterations without completion'); - return { - status: 'max_iterations', - executionId, - iterations, - toolExecutions, - executionTime: Date.now() - startTime, - error: 'Maximum iterations reached without completion' - }; - } - - const executionTime = Date.now() - startTime; - console.log(`🎉 Agent execution completed successfully in ${executionTime}ms`); - console.log(`📊 Stats: ${iterations} iterations, ${toolExecutions.length} tool executions`); - - return { - status: 'completed', - executionId, - finalResponse, - iterations, - toolExecutions, - executionTime, - metadata: { - toolsAvailable: availableTools.length, - skillsInvolved: [...new Set(toolExecutions.map(te => te.skillId))] - } - }; - - } finally { - clearTimeout(timeoutId); - } - - } catch (error) { - console.error('❌ Agent execution failed:', error); - - return { - status: 'error', - executionId, - iterations, - toolExecutions, - executionTime: Date.now() - startTime, - error: error instanceof Error ? error.message : String(error) - }; - } finally { - this.activeExecutions.delete(executionId); - } - } - - /** - * Cancel an active agent execution - */ - cancelExecution(executionId: string): boolean { - const controller = this.activeExecutions.get(executionId); - if (controller) { - controller.abort(); - this.activeExecutions.delete(executionId); - console.log(`🛑 Cancelled agent execution: ${executionId}`); - return true; - } - return false; - } - - /** - * Get list of active execution IDs - */ - getActiveExecutions(): string[] { - return Array.from(this.activeExecutions.keys()); - } - - /** - * Get execution status (placeholder - would need Redux integration) - */ - getExecutionStatus(executionId: string): null { - // This would typically integrate with Redux state - // For now, just return null - return null; - } - - // ============================================================================= - // Private Helper Methods - // ============================================================================= - - /** - * Execute a single tool call - */ - private async executeSingleTool( - toolCall: any, - availableTools: any[], - requireApproval: boolean, - signal: AbortSignal - ): Promise { - const startTime = Date.now(); - - try { - // Find the tool and its associated skill - const toolSchema = availableTools.find(t => t.function.name === toolCall.function.name); - if (!toolSchema) { - return { - id: toolCall.id, - toolName: toolCall.function.name, - skillId: 'unknown', - arguments: toolCall.function.arguments, - status: 'error', - startTime, - endTime: Date.now(), - errorMessage: `Tool not found: ${toolCall.function.name}` - }; - } - - const skillId = (toolSchema.function as any).skillId; - - console.log(`🔧 Executing tool: ${skillId}.${toolCall.function.name}`); - - // TODO: Implement approval workflow if requireApproval is true - - // Execute the tool using the existing skill system - const result = await this.toolRegistry.executeTool( - skillId, - toolCall.function.name, - toolCall.function.arguments - ); - - console.log(`✅ Tool execution ${result.status}: ${toolCall.function.name}`); - - return { - ...result, - id: toolCall.id // Use the tool call ID from the AI - }; - - } catch (error) { - const endTime = Date.now(); - console.error(`❌ Tool execution error: ${toolCall.function.name}`, error); - - return { - id: toolCall.id, - toolName: toolCall.function.name, - skillId: 'unknown', - arguments: toolCall.function.arguments, - status: 'error', - startTime, - endTime, - executionTimeMs: endTime - startTime, - errorMessage: error instanceof Error ? error.message : String(error) - }; - } - } - - /** - * Filter tools based on allowed skills and blocked tools - */ - private filterTools( - toolSchemas: any[], - allowedSkills?: string[], - blockedTools: string[] = [] - ): any[] { - return toolSchemas.filter(tool => { - const skillId = (tool.function as any).skillId; - const toolName = tool.function.name; - - // Check if tool is blocked - if (blockedTools.includes(toolName)) { - return false; - } - - // Check if skill is allowed (if allowedSkills is specified) - if (allowedSkills && allowedSkills.length > 0) { - return allowedSkills.includes(skillId); - } - - return true; - }); - } - - /** - * Convert agent tool schema to OpenAI tool format - */ - private convertToOpenAITool(toolSchema: any): OpenAITool { - return { - type: 'function', - function: { - name: toolSchema.function.name, - description: toolSchema.function.description, - parameters: toolSchema.function.parameters - } - }; - } -} \ No newline at end of file diff --git a/src/services/api/inferenceApi.ts b/src/services/api/inferenceApi.ts index 2c59841cc..6dbf7e466 100644 --- a/src/services/api/inferenceApi.ts +++ b/src/services/api/inferenceApi.ts @@ -2,16 +2,40 @@ import { apiClient } from '../apiClient'; // ── Request types ──────────────────────────────────────────────────────────── -export type ChatRole = 'system' | 'user' | 'assistant'; +export type ChatRole = 'system' | 'user' | 'assistant' | 'tool'; + +export interface ToolCall { + id: string; + type: 'function'; + function: { + name: string; + arguments: string; + }; +} export interface ChatMessage { role: ChatRole; - content: string; + content: string | null; + tool_calls?: ToolCall[]; + tool_call_id?: string; +} + +export interface ToolFunction { + name: string; + description: string; + parameters: any; +} + +export interface Tool { + type: 'function'; + function: ToolFunction; } export interface ChatCompletionRequest { model: string; messages: ChatMessage[]; + tools?: Tool[]; + tool_choice?: 'auto' | 'none' | { type: 'function'; function: { name: string } }; stream?: boolean; temperature?: number; max_tokens?: number; diff --git a/src/store/__tests__/agentSlice.test.ts b/src/store/__tests__/agentSlice.test.ts deleted file mode 100644 index 0d4197422..000000000 --- a/src/store/__tests__/agentSlice.test.ts +++ /dev/null @@ -1,496 +0,0 @@ -import { describe, test, expect, beforeEach, vi } from 'vitest'; -import { configureStore } from '@reduxjs/toolkit'; -import agentReducer, { - setAgentMode, - startAgentExecution, - updateExecutionProgress, - completeAgentExecution, - cancelAgentExecution, - setToolRegistry, - clearExecutionHistory, - executeAgentTask, - loadAgentTools, - cancelAgentExecutionThunk, - type AgentState -} from '../agentSlice'; -import type { - AgentExecutionResult, - AgentToolExecution, - AgentToolSchema -} from '../../types/agent'; - -// Mock dependencies -vi.mock('../../services/agentLoop'); -vi.mock('../../services/agentToolRegistry'); - -describe('agentSlice', () => { - let store: ReturnType; - - beforeEach(() => { - store = configureStore({ - reducer: { - agent: agentReducer - } - }); - }); - - describe('synchronous actions', () => { - test('setAgentMode should toggle agent mode', () => { - expect(store.getState().agent.isAgentMode).toBe(false); - - store.dispatch(setAgentMode(true)); - expect(store.getState().agent.isAgentMode).toBe(true); - - store.dispatch(setAgentMode(false)); - expect(store.getState().agent.isAgentMode).toBe(false); - }); - - test('startAgentExecution should initialize execution state', () => { - const executionId = 'exec_123'; - const threadId = 'thread_456'; - const userMessage = 'Help me with GitHub issues'; - - store.dispatch(startAgentExecution({ executionId, threadId, userMessage })); - - const state = store.getState().agent; - expect(state.currentExecution).toEqual({ - id: executionId, - threadId, - userMessage, - status: 'running', - iterations: 0, - toolExecutions: [], - startTime: expect.any(Number), - executionTime: 0 - }); - expect(state.executionHistory).toHaveLength(1); - expect(state.executionHistory[0].id).toBe(executionId); - }); - - test('updateExecutionProgress should update current execution', () => { - const executionId = 'exec_123'; - const threadId = 'thread_456'; - - // Start execution first - store.dispatch(startAgentExecution({ - executionId, - threadId, - userMessage: 'Test' - })); - - const toolExecution: AgentToolExecution = { - id: 'tool_exec_1', - toolName: 'list_issues', - skillId: 'github', - arguments: '{"owner":"user","repo":"test"}', - status: 'running', - startTime: Date.now() - }; - - store.dispatch(updateExecutionProgress({ - executionId, - iteration: 1, - toolExecution - })); - - const state = store.getState().agent; - expect(state.currentExecution?.iterations).toBe(1); - expect(state.currentExecution?.toolExecutions).toHaveLength(1); - expect(state.currentExecution?.toolExecutions[0]).toEqual(toolExecution); - }); - - test('updateExecutionProgress should update existing tool execution', () => { - const executionId = 'exec_123'; - const threadId = 'thread_456'; - - // Start execution - store.dispatch(startAgentExecution({ - executionId, - threadId, - userMessage: 'Test' - })); - - const toolExecution: AgentToolExecution = { - id: 'tool_exec_1', - toolName: 'list_issues', - skillId: 'github', - arguments: '{}', - status: 'running', - startTime: Date.now() - }; - - // Add tool execution - store.dispatch(updateExecutionProgress({ - executionId, - iteration: 1, - toolExecution - })); - - // Update the same tool execution with completion - const updatedToolExecution: AgentToolExecution = { - ...toolExecution, - status: 'success', - endTime: Date.now(), - executionTimeMs: 1500, - result: '{"issues":[]}' - }; - - store.dispatch(updateExecutionProgress({ - executionId, - iteration: 1, - toolExecution: updatedToolExecution - })); - - const state = store.getState().agent; - expect(state.currentExecution?.toolExecutions).toHaveLength(1); - expect(state.currentExecution?.toolExecutions[0].status).toBe('success'); - expect(state.currentExecution?.toolExecutions[0].result).toBe('{"issues":[]}'); - }); - - test('completeAgentExecution should finalize execution', () => { - const executionId = 'exec_123'; - const threadId = 'thread_456'; - - // Start execution first - store.dispatch(startAgentExecution({ - executionId, - threadId, - userMessage: 'Test' - })); - - const completionData = { - executionId, - status: 'completed' as const, - finalResponse: 'Task completed successfully', - totalExecutionTime: 5000 - }; - - store.dispatch(completeAgentExecution(completionData)); - - const state = store.getState().agent; - expect(state.currentExecution?.status).toBe('completed'); - expect(state.currentExecution?.finalResponse).toBe('Task completed successfully'); - expect(state.currentExecution?.executionTime).toBe(5000); - expect(state.lastExecutionId).toBe(executionId); - - // Execution should be updated in history - const historyItem = state.executionHistory.find(item => item.id === executionId); - expect(historyItem?.status).toBe('completed'); - expect(historyItem?.finalResponse).toBe('Task completed successfully'); - }); - - test('cancelAgentExecution should cancel current execution', () => { - const executionId = 'exec_123'; - const threadId = 'thread_456'; - - // Start execution first - store.dispatch(startAgentExecution({ - executionId, - threadId, - userMessage: 'Test' - })); - - store.dispatch(cancelAgentExecution({ - executionId, - reason: 'User cancelled' - })); - - const state = store.getState().agent; - expect(state.currentExecution?.status).toBe('cancelled'); - expect(state.currentExecution?.error).toBe('User cancelled'); - }); - - test('setToolRegistry should update available tools', () => { - const mockTools: AgentToolSchema[] = [ - { - type: "function", - function: { - name: "github_list_issues", - description: "List GitHub issues", - parameters: { - type: "object", - properties: { - owner: { type: "string" }, - repo: { type: "string" } - }, - required: ["owner", "repo"] - } - } - } - ]; - - store.dispatch(setToolRegistry({ - tools: mockTools, - lastUpdated: Date.now() - })); - - const state = store.getState().agent; - expect(state.toolRegistry.tools).toEqual(mockTools); - expect(state.toolRegistry.lastUpdated).toBeDefined(); - }); - - test('clearExecutionHistory should reset history', () => { - const executionId = 'exec_123'; - const threadId = 'thread_456'; - - // Add some history first - store.dispatch(startAgentExecution({ - executionId, - threadId, - userMessage: 'Test' - })); - - expect(store.getState().agent.executionHistory).toHaveLength(1); - - store.dispatch(clearExecutionHistory()); - - expect(store.getState().agent.executionHistory).toHaveLength(0); - }); - }); - - describe('async thunks', () => { - test('executeAgentTask.pending should set loading state', () => { - const action = { type: executeAgentTask.pending.type }; - const state = agentReducer(undefined, action); - - expect(state.isLoading).toBe(true); - expect(state.error).toBeNull(); - }); - - test('executeAgentTask.fulfilled should handle successful execution', () => { - const mockResult: AgentExecutionResult = { - status: 'completed', - executionId: 'exec_123', - finalResponse: 'Task completed', - iterations: 2, - toolExecutions: [], - executionTime: 3000 - }; - - const action = { - type: executeAgentTask.fulfilled.type, - payload: mockResult - }; - - const state = agentReducer(undefined, action); - - expect(state.isLoading).toBe(false); - expect(state.error).toBeNull(); - expect(state.lastExecutionId).toBe('exec_123'); - }); - - test('executeAgentTask.rejected should handle execution error', () => { - const action = { - type: executeAgentTask.rejected.type, - error: { message: 'Execution failed' } - }; - - const state = agentReducer(undefined, action); - - expect(state.isLoading).toBe(false); - expect(state.error).toBe('Execution failed'); - }); - - test('loadAgentTools.fulfilled should update tool registry', () => { - const mockTools: AgentToolSchema[] = [ - { - type: "function", - function: { - name: "test_tool", - description: "Test tool", - parameters: { type: "object", properties: {} } - } - } - ]; - - const action = { - type: loadAgentTools.fulfilled.type, - payload: mockTools - }; - - const state = agentReducer(undefined, action); - - expect(state.toolRegistry.tools).toEqual(mockTools); - expect(state.toolRegistry.isLoaded).toBe(true); - expect(state.toolRegistry.lastUpdated).toBeDefined(); - }); - - test('loadAgentTools.rejected should handle tool loading error', () => { - const action = { - type: loadAgentTools.rejected.type, - error: { message: 'Failed to load tools' } - }; - - const state = agentReducer(undefined, action); - - expect(state.toolRegistry.isLoaded).toBe(false); - expect(state.toolRegistry.error).toBe('Failed to load tools'); - }); - - test('cancelAgentExecutionThunk.fulfilled should cancel execution', () => { - // First set up an execution - const initialState: AgentState = { - isAgentMode: false, - isLoading: false, - error: null, - currentExecution: { - id: 'exec_123', - threadId: 'thread_456', - userMessage: 'Test', - status: 'running', - iterations: 1, - toolExecutions: [], - startTime: Date.now(), - executionTime: 0 - }, - executionHistory: [{ - id: 'exec_123', - threadId: 'thread_456', - userMessage: 'Test', - status: 'running', - iterations: 1, - toolExecutions: [], - startTime: Date.now(), - executionTime: 0 - }], - lastExecutionId: null, - toolRegistry: { - tools: [], - isLoaded: false, - lastUpdated: null, - error: null - }, - configByThreadId: {} - }; - - const action = { - type: cancelAgentExecutionThunk.fulfilled.type, - payload: { executionId: 'exec_123' } - }; - - const state = agentReducer(initialState, action); - - expect(state.currentExecution?.status).toBe('cancelled'); - expect(state.executionHistory[0].status).toBe('cancelled'); - }); - }); - - describe('selectors and derived state', () => { - test('should maintain execution history chronologically', () => { - const execution1 = { - executionId: 'exec_1', - threadId: 'thread_1', - userMessage: 'First task' - }; - - const execution2 = { - executionId: 'exec_2', - threadId: 'thread_1', - userMessage: 'Second task' - }; - - store.dispatch(startAgentExecution(execution1)); - store.dispatch(startAgentExecution(execution2)); - - const state = store.getState().agent; - expect(state.executionHistory).toHaveLength(2); - expect(state.executionHistory[0].id).toBe('exec_1'); - expect(state.executionHistory[1].id).toBe('exec_2'); - }); - - test('should track tool execution statistics', () => { - const executionId = 'exec_123'; - - store.dispatch(startAgentExecution({ - executionId, - threadId: 'thread_1', - userMessage: 'Test' - })); - - // Add multiple tool executions - const toolExecution1: AgentToolExecution = { - id: 'tool_1', - toolName: 'list_issues', - skillId: 'github', - arguments: '{}', - status: 'success', - startTime: Date.now() - 2000, - endTime: Date.now() - 1000, - executionTimeMs: 1000 - }; - - const toolExecution2: AgentToolExecution = { - id: 'tool_2', - toolName: 'create_page', - skillId: 'notion', - arguments: '{}', - status: 'success', - startTime: Date.now() - 1000, - endTime: Date.now(), - executionTimeMs: 1000 - }; - - store.dispatch(updateExecutionProgress({ - executionId, - iteration: 1, - toolExecution: toolExecution1 - })); - - store.dispatch(updateExecutionProgress({ - executionId, - iteration: 2, - toolExecution: toolExecution2 - })); - - const state = store.getState().agent; - expect(state.currentExecution?.toolExecutions).toHaveLength(2); - expect(state.currentExecution?.iterations).toBe(2); - }); - }); - - describe('error handling', () => { - test('should handle invalid execution updates gracefully', () => { - // Try to update non-existent execution - store.dispatch(updateExecutionProgress({ - executionId: 'non_existent', - iteration: 1, - toolExecution: { - id: 'tool_1', - toolName: 'test', - skillId: 'test', - arguments: '{}', - status: 'running', - startTime: Date.now() - } - })); - - // Should not crash and current execution should remain null - const state = store.getState().agent; - expect(state.currentExecution).toBeNull(); - }); - - test('should handle completion of non-existent execution gracefully', () => { - store.dispatch(completeAgentExecution({ - executionId: 'non_existent', - status: 'completed', - finalResponse: 'Done', - totalExecutionTime: 1000 - })); - - // Should not crash - const state = store.getState().agent; - expect(state.currentExecution).toBeNull(); - }); - }); - - describe('thread-specific configuration', () => { - test('should store and retrieve thread-specific config', () => { - const initialState = store.getState().agent; - expect(initialState.configByThreadId).toEqual({}); - - // Test that the structure exists for future configuration - // Note: actual config setting would require a specific action - expect(typeof initialState.configByThreadId).toBe('object'); - }); - }); -}); \ No newline at end of file diff --git a/src/store/agentSlice.ts b/src/store/agentSlice.ts deleted file mode 100644 index 775faa5e8..000000000 --- a/src/store/agentSlice.ts +++ /dev/null @@ -1,433 +0,0 @@ -/** - * Agent Redux slice for managing agent execution state - * - * Extends AlphaHuman's existing Redux pattern to handle agent task execution, - * tool executions, and agent configuration per thread. - */ - -import { createSlice, createAsyncThunk, type PayloadAction } from '@reduxjs/toolkit'; - -import { AgentLoop } from '../services/agentLoop'; -import { AgentToolRegistry } from '../services/agentToolRegistry'; -import type { - AgentState, - AgentExecution, - AgentExecutionResult, - AgentExecutionOptions, - AgentExecutionHistoryEntry, - AgentToolExecution, - AgentToolSchema -} from '../types/agent'; - -// ============================================================================= -// Async Thunks -// ============================================================================= - -/** - * Execute an agent task autonomously - */ -export const executeAgentTask = createAsyncThunk( - 'agent/executeTask', - async ( - params: { - userMessage: string; - threadId: string; - options?: AgentExecutionOptions; - }, - { getState, rejectWithValue } - ) => { - try { - const agentLoop = AgentLoop.getInstance(); - const result = await agentLoop.executeTask( - params.userMessage, - params.threadId, - params.options - ); - - return { - threadId: params.threadId, - userMessage: params.userMessage, - result, - timestamp: Date.now() - }; - } catch (error) { - return rejectWithValue( - error instanceof Error ? error.message : String(error) - ); - } - } -); - -/** - * Load available tools from the skill system - */ -export const loadAgentTools = createAsyncThunk( - 'agent/loadTools', - async (forceReload = false, { rejectWithValue }) => { - try { - const registry = AgentToolRegistry.getInstance(); - const tools = await registry.loadToolSchemas(forceReload); - return tools; - } catch (error) { - return rejectWithValue( - error instanceof Error ? error.message : String(error) - ); - } - } -); - -/** - * Cancel an active agent execution - */ -export const cancelAgentExecution = createAsyncThunk( - 'agent/cancelExecution', - async (executionId: string, { rejectWithValue }) => { - try { - const agentLoop = AgentLoop.getInstance(); - const cancelled = agentLoop.cancelExecution(executionId); - - if (!cancelled) { - throw new Error('Execution not found or already completed'); - } - - return executionId; - } catch (error) { - return rejectWithValue( - error instanceof Error ? error.message : String(error) - ); - } - } -); - -// ============================================================================= -// Initial State -// ============================================================================= - -const initialState: AgentState = { - agentModeByThreadId: {}, - activeExecutions: {}, - executionHistory: [], - configByThreadId: {}, - toolRegistry: { - tools: [], - lastUpdated: 0, - loading: false - }, - ui: { - showExecutionDetails: {}, - selectedExecution: undefined - } -}; - -// ============================================================================= -// Slice Definition -// ============================================================================= - -const agentSlice = createSlice({ - name: 'agent', - initialState, - reducers: { - // Agent mode management - setAgentModeForThread: ( - state, - action: PayloadAction<{ threadId: string; enabled: boolean }> - ) => { - const { threadId, enabled } = action.payload; - state.agentModeByThreadId[threadId] = enabled; - }, - - // Agent configuration management - setAgentConfigForThread: ( - state, - action: PayloadAction<{ threadId: string; config: AgentExecutionOptions }> - ) => { - const { threadId, config } = action.payload; - state.configByThreadId[threadId] = config; - }, - - // UI state management - toggleExecutionDetails: ( - state, - action: PayloadAction<{ executionId: string }> - ) => { - const { executionId } = action.payload; - state.ui.showExecutionDetails[executionId] = !state.ui.showExecutionDetails[executionId]; - }, - - setSelectedExecution: ( - state, - action: PayloadAction - ) => { - state.ui.selectedExecution = action.payload; - }, - - // Tool registry cache management - clearToolRegistry: (state) => { - state.toolRegistry = { - tools: [], - lastUpdated: 0, - loading: false - }; - }, - - // Execution tracking (for real-time updates) - addActiveExecution: ( - state, - action: PayloadAction - ) => { - const execution = action.payload; - state.activeExecutions[execution.id] = execution; - }, - - updateActiveExecution: ( - state, - action: PayloadAction & { id: string }> - ) => { - const { id, ...updates } = action.payload; - if (state.activeExecutions[id]) { - Object.assign(state.activeExecutions[id], updates); - } - }, - - removeActiveExecution: ( - state, - action: PayloadAction - ) => { - const executionId = action.payload; - delete state.activeExecutions[executionId]; - }, - - // Tool execution updates - addToolExecution: ( - state, - action: PayloadAction<{ executionId: string; toolExecution: AgentToolExecution }> - ) => { - const { executionId, toolExecution } = action.payload; - if (state.activeExecutions[executionId]) { - state.activeExecutions[executionId].toolExecutions.push(toolExecution); - state.activeExecutions[executionId].lastUpdate = Date.now(); - } - }, - - updateToolExecution: ( - state, - action: PayloadAction<{ - executionId: string; - toolExecutionId: string; - updates: Partial; - }> - ) => { - const { executionId, toolExecutionId, updates } = action.payload; - const execution = state.activeExecutions[executionId]; - - if (execution) { - const toolExecution = execution.toolExecutions.find(te => te.id === toolExecutionId); - if (toolExecution) { - Object.assign(toolExecution, updates); - execution.lastUpdate = Date.now(); - } - } - }, - - // Execution history management - addExecutionToHistory: ( - state, - action: PayloadAction - ) => { - state.executionHistory.unshift(action.payload); - - // Keep only last 100 executions - if (state.executionHistory.length > 100) { - state.executionHistory = state.executionHistory.slice(0, 100); - } - }, - - clearExecutionHistory: (state) => { - state.executionHistory = []; - } - }, - - extraReducers: (builder) => { - // Execute agent task - builder - .addCase(executeAgentTask.pending, (state, action) => { - const { userMessage, threadId } = action.meta.arg; - const executionId = `agent_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; - - const execution: AgentExecution = { - id: executionId, - threadId, - userMessage, - status: 'initializing', - currentIteration: 0, - maxIterations: action.meta.arg.options?.maxIterations || 10, - toolExecutions: [], - startTime: Date.now(), - lastUpdate: Date.now() - }; - - state.activeExecutions[executionId] = execution; - }) - .addCase(executeAgentTask.fulfilled, (state, action) => { - const { threadId, userMessage, result, timestamp } = action.payload; - - // Find the execution by thread and message - const execution = Object.values(state.activeExecutions).find( - exec => exec.threadId === threadId && exec.userMessage === userMessage - ); - - if (execution) { - // Move from active to history - const historyEntry: AgentExecutionHistoryEntry = { - executionId: execution.id, - threadId, - userMessage, - result, - timestamp, - duration: Date.now() - execution.startTime - }; - - state.executionHistory.unshift(historyEntry); - delete state.activeExecutions[execution.id]; - - // Keep only last 100 executions - if (state.executionHistory.length > 100) { - state.executionHistory = state.executionHistory.slice(0, 100); - } - } - }) - .addCase(executeAgentTask.rejected, (state, action) => { - // Remove failed execution from active list - const rejectedExecution = Object.values(state.activeExecutions).find( - exec => exec.userMessage === action.meta.arg.userMessage - ); - - if (rejectedExecution) { - delete state.activeExecutions[rejectedExecution.id]; - } - }); - - // Load agent tools - builder - .addCase(loadAgentTools.pending, (state) => { - state.toolRegistry.loading = true; - }) - .addCase(loadAgentTools.fulfilled, (state, action) => { - state.toolRegistry.tools = action.payload; - state.toolRegistry.lastUpdated = Date.now(); - state.toolRegistry.loading = false; - state.toolRegistry.error = undefined; - }) - .addCase(loadAgentTools.rejected, (state, action) => { - state.toolRegistry.loading = false; - state.toolRegistry.error = action.payload as string; - }); - - // Cancel agent execution - builder - .addCase(cancelAgentExecution.fulfilled, (state, action) => { - const executionId = action.payload; - if (state.activeExecutions[executionId]) { - state.activeExecutions[executionId].status = 'completing'; - } - }); - } -}); - -// ============================================================================= -// Actions Export -// ============================================================================= - -export const { - setAgentModeForThread, - setAgentConfigForThread, - toggleExecutionDetails, - setSelectedExecution, - clearToolRegistry, - addActiveExecution, - updateActiveExecution, - removeActiveExecution, - addToolExecution, - updateToolExecution, - addExecutionToHistory, - clearExecutionHistory -} = agentSlice.actions; - -// ============================================================================= -// Selectors -// ============================================================================= - -export const selectAgentModeForThread = (state: { agent: AgentState }, threadId: string) => - state.agent.agentModeByThreadId[threadId] || false; - -export const selectAgentConfigForThread = (state: { agent: AgentState }, threadId: string) => - state.agent.configByThreadId[threadId] || {}; - -export const selectActiveExecutions = (state: { agent: AgentState }) => - Object.values(state.agent.activeExecutions); - -export const selectActiveExecutionForThread = (state: { agent: AgentState }, threadId: string) => - Object.values(state.agent.activeExecutions).find(exec => exec.threadId === threadId); - -export const selectExecutionHistory = (state: { agent: AgentState }) => - state.agent.executionHistory; - -export const selectExecutionHistoryForThread = (state: { agent: AgentState }, threadId: string) => - state.agent.executionHistory.filter(entry => entry.threadId === threadId); - -export const selectToolRegistry = (state: { agent: AgentState }) => - state.agent.toolRegistry; - -export const selectAvailableTools = (state: { agent: AgentState }) => - state.agent.toolRegistry.tools; - -export const selectToolsByCategory = (state: { agent: AgentState }) => { - const toolsBySkill: Record = {}; - - for (const tool of state.agent.toolRegistry.tools) { - const skillId = (tool.function as any).skillId || 'unknown'; - if (!toolsBySkill[skillId]) { - toolsBySkill[skillId] = []; - } - toolsBySkill[skillId].push(tool); - } - - return toolsBySkill; -}; - -export const selectToolStats = (state: { agent: AgentState }) => { - const tools = state.agent.toolRegistry.tools; - const skillIds = new Set(); - const categories: Record = {}; - - for (const tool of tools) { - const skillId = (tool.function as any).skillId || 'unknown'; - skillIds.add(skillId); - - // Categorize by skill type - let category = 'Other'; - if (skillId.includes('github') || skillId.includes('git')) category = 'GitHub'; - else if (skillId.includes('notion')) category = 'Notion'; - else if (skillId.includes('telegram') || skillId.includes('tg')) category = 'Telegram'; - else if (skillId.includes('email') || skillId.includes('gmail')) category = 'Email'; - else if (skillId.includes('calendar')) category = 'Calendar'; - else if (skillId.includes('slack')) category = 'Slack'; - - categories[category] = (categories[category] || 0) + 1; - } - - return { - totalTools: tools.length, - skillCount: skillIds.size, - categories - }; -}; - -export const selectAgentUIState = (state: { agent: AgentState }) => - state.agent.ui; - -// ============================================================================= -// Reducer Export -// ============================================================================= - -export default agentSlice.reducer; \ No newline at end of file diff --git a/src/store/index.ts b/src/store/index.ts index 3d203447c..6e659e00b 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -15,7 +15,6 @@ import storage from 'redux-persist/lib/storage'; import { setStoreForApiClient } from '../services/apiClient'; import { IS_DEV } from '../utils/config'; import { storeSession } from '../utils/tauriCommands'; -import agentReducer from './agentSlice'; import aiReducer from './aiSlice'; import authReducer, { setOnboardedForUser, setToken } from './authSlice'; import daemonReducer from './daemonSlice'; @@ -54,18 +53,11 @@ const threadPersistConfig = { whitelist: ['panelWidth', 'lastViewedAt', 'threads', 'messagesByThreadId', 'selectedThreadId'], }; -// Persist config for agent state (execution history, config, and agent mode) -const agentPersistConfig = { - key: 'agent', - storage, - whitelist: ['agentModeByThreadId', 'executionHistory', 'configByThreadId'], -}; const persistedAuthReducer = persistReducer(authPersistConfig, authReducer); const persistedAiReducer = persistReducer(aiPersistConfig, aiReducer); const persistedSkillsReducer = persistReducer(skillsPersistConfig, skillsReducer); const persistedThreadReducer = persistReducer(threadPersistConfig, threadReducer); -const persistedAgentReducer = persistReducer(agentPersistConfig, agentReducer); /** * Middleware that syncs the JWT token to the Rust SESSION_SERVICE whenever @@ -111,7 +103,6 @@ export const store = configureStore({ thread: persistedThreadReducer, invite: inviteReducer, notion: notionReducer, - agent: persistedAgentReducer, }, middleware: getDefaultMiddleware => { const middleware = getDefaultMiddleware({ diff --git a/src/store/threadSlice.ts b/src/store/threadSlice.ts index 425bd0b7c..85d91acef 100644 --- a/src/store/threadSlice.ts +++ b/src/store/threadSlice.ts @@ -4,7 +4,6 @@ import { threadApi } from '../services/api/threadApi'; import type { Thread, ThreadMessage } from '../types/thread'; import { injectAll } from '../lib/ai/injector'; import type { Message } from '../lib/ai/providers/interface'; -import { executeAgentTask, selectAgentModeForThread } from './agentSlice'; import type { RootState } from './index'; interface ThreadState { @@ -124,39 +123,13 @@ export const sendMessage = createAsyncThunk( // Continue with original message } - // 3. Check if agent mode is enabled for this thread - const state = getState() as RootState; - const agentMode = selectAgentModeForThread(state, threadId); + // 3. Send to API with processed message (disable injection in threadApi to avoid double injection) + const data = await threadApi.sendMessage(processedMessage, threadId, { injectSoul: false }); - if (agentMode) { - // Execute agent task instead of sending to inference API - console.log('🤖 Agent mode enabled - executing agent task'); + // 4. For now, we'll handle AI response via the existing inference API + // The AI response will be added separately via addInferenceResponse - const agentResult = await dispatch(executeAgentTask({ - userMessage: message, - threadId, - options: state.agent.configByThreadId[threadId] || {} - })).unwrap(); - - // Add the agent's final response as an AI message with execution metadata - if (agentResult.result.finalResponse) { - dispatch(addInferenceResponse({ - content: agentResult.result.finalResponse, - agentExecutionId: agentResult.result.executionId, - toolExecutions: agentResult.result.toolExecutions - })); - } - - return agentResult; - } else { - // 4. Send to API with processed message (disable injection in threadApi to avoid double injection) - const data = await threadApi.sendMessage(processedMessage, threadId, { injectSoul: false }); - - // 5. For now, we'll handle AI response via the existing inference API - // The AI response will be added separately via addInferenceResponse - - return data; - } + return data; } catch (error) { // Remove optimistic user message on failure const state = (getState() as { thread: ThreadState }).thread; @@ -228,17 +201,12 @@ const threadSlice = createSlice({ createdAt: new Date().toISOString(), }); }, - addInferenceResponse: (state, action: { payload: { content: string; agentExecutionId?: string; toolExecutions?: any[] } }) => { + addInferenceResponse: (state, action: { payload: { content: string } }) => { const aiMessage: ThreadMessage = { id: `inference-${Date.now()}`, content: action.payload.content, type: 'text', - extraMetadata: { - ...(action.payload.agentExecutionId && { - agentExecutionId: action.payload.agentExecutionId, - toolExecutions: action.payload.toolExecutions || [] - }) - }, + extraMetadata: {}, sender: 'agent', createdAt: new Date().toISOString(), }; diff --git a/src/types/agent.ts b/src/types/agent.ts deleted file mode 100644 index af3848355..000000000 --- a/src/types/agent.ts +++ /dev/null @@ -1,379 +0,0 @@ -/** - * Agent system types for AlphaHuman. - * Built on top of the existing skill system infrastructure. - */ - -import type { SkillToolDefinition } from '../lib/skills/types'; -import type { ThreadMessage, Thread } from './thread'; - -// ============================================================================= -// Agent Tool Types (extends skill tools) -// ============================================================================= - -/** - * Agent tool schema compatible with OpenAI function calling format - * and the existing skill tool system - */ -export interface AgentToolSchema { - type: 'function'; - function: { - name: string; - description: string; - parameters: { - type: 'object'; - properties: Record; - required?: string[]; - }; - }; -} - -export interface AgentToolParameter { - type: 'string' | 'number' | 'boolean' | 'array' | 'object'; - description?: string; - enum?: string[]; - items?: AgentToolParameter; - properties?: Record; - required?: string[]; - default?: any; - minimum?: number; - maximum?: number; - pattern?: string; -} - -/** - * Tool execution tracking for agent conversations - */ -export interface AgentToolExecution { - id: string; - toolName: string; - skillId: string; // Which skill provides this tool - arguments: string; // JSON string - result?: string; - status: AgentToolExecutionStatus; - startTime: number; - endTime?: number; - executionTimeMs?: number; - errorMessage?: string; - metadata?: { - retryCount?: number; - approvalRequired?: boolean; - approvalGranted?: boolean; - }; -} - -export type AgentToolExecutionStatus = - | 'pending' - | 'running' - | 'success' - | 'error' - | 'cancelled' - | 'timeout'; - -// ============================================================================= -// Agent Execution Types -// ============================================================================= - -/** - * Configuration options for agent task execution - */ -export interface AgentExecutionOptions { - maxIterations?: number; - timeoutMs?: number; - requireApproval?: boolean; - allowedSkills?: string[]; // Skill IDs that are allowed to execute - blockedTools?: string[]; // Specific tools that are blocked - retryFailedTools?: boolean; -} - -/** - * Result of an agent task execution - */ -export interface AgentExecutionResult { - status: AgentExecutionStatus; - executionId: string; - finalResponse?: string; - iterations: number; - toolExecutions: AgentToolExecution[]; - executionTime: number; - error?: string; - metadata?: { - tokensUsed?: number; - apiCalls?: number; - toolsAvailable?: number; - skillsInvolved?: string[]; - }; -} - -export type AgentExecutionStatus = - | 'completed' - | 'timeout' - | 'error' - | 'max_iterations' - | 'cancelled' - | 'blocked'; - -/** - * Active agent execution tracking - */ -export interface AgentExecution { - id: string; - threadId: string; - userMessage: string; - status: 'initializing' | 'running' | 'completing'; - currentIteration: number; - maxIterations: number; - toolExecutions: AgentToolExecution[]; - startTime: number; - lastUpdate: number; - abortController?: AbortController; -} - -// ============================================================================= -// OpenAI API Compatibility Types -// ============================================================================= - -/** - * OpenAI-compatible message format for backend communication - */ -export interface OpenAIMessage { - role: 'system' | 'user' | 'assistant' | 'tool'; - content: string | null; - name?: string; - tool_calls?: OpenAIToolCall[]; - tool_call_id?: string; -} - -export interface OpenAIToolCall { - id: string; - type: 'function'; - function: { - name: string; - arguments: string; // JSON string - }; -} - -export interface OpenAITool { - type: 'function'; - function: { - name: string; - description: string; - parameters: any; // JSON Schema - }; -} - -/** - * Chat completion request sent to backend - */ -export interface AgentChatRequest { - model: string; - messages: OpenAIMessage[]; - tools?: OpenAITool[]; - tool_choice?: 'auto' | 'none' | 'required'; - temperature?: number; - max_tokens?: number; -} - -/** - * Chat completion response from backend - */ -export interface AgentChatResponse { - id: string; - object: 'chat.completion'; - created: number; - model: string; - choices: AgentChatChoice[]; - usage?: { - prompt_tokens: number; - completion_tokens: number; - total_tokens: number; - }; -} - -export interface AgentChatChoice { - index: number; - message: OpenAIMessage; - finish_reason: 'stop' | 'length' | 'tool_calls' | 'content_filter'; -} - -// ============================================================================= -// Thread System Integration -// ============================================================================= - -/** - * Enhanced thread message with agent execution metadata - */ -export interface AgentThreadMessage extends ThreadMessage { - // Existing ThreadMessage fields remain the same - // Enhanced extraMetadata for agent tracking - extraMetadata: ThreadMessage['extraMetadata'] & { - agentExecutionId?: string; - toolExecutions?: AgentToolExecution[]; - iterationNumber?: number; - agentStatus?: AgentExecutionStatus; - }; -} - -/** - * Enhanced thread with agent mode capability - */ -export interface AgentThread extends Thread { - // Existing Thread fields remain the same - // Additional agent-specific metadata - agentMode?: boolean; - lastAgentExecution?: string; - agentConfig?: AgentExecutionOptions; -} - -// ============================================================================= -// Redux State Types -// ============================================================================= - -/** - * Agent Redux state that integrates with existing skill system - */ -export interface AgentState { - // Agent mode enabled per thread - agentModeByThreadId: Record; - - // Active agent executions - activeExecutions: Record; - - // Agent execution history (persisted) - executionHistory: AgentExecutionHistoryEntry[]; - - // Agent configuration per thread (persisted) - configByThreadId: Record; - - // Tool registry cache (derived from skills) - toolRegistry: { - tools: AgentToolSchema[]; - lastUpdated: number; - loading: boolean; - error?: string; - }; - - // UI state (not persisted) - ui: { - showExecutionDetails: Record; - selectedExecution?: string; - }; -} - -export interface AgentExecutionHistoryEntry { - executionId: string; - threadId: string; - userMessage: string; - result: AgentExecutionResult; - timestamp: number; - duration: number; -} - -// ============================================================================= -// Service Interface Types -// ============================================================================= - -/** - * Tool registry service interface - */ -export interface IAgentToolRegistry { - loadToolSchemas(forceReload?: boolean): Promise; - executeTool(skillId: string, toolName: string, toolArguments: string): Promise; - getToolByName(toolName: string): AgentToolSchema | undefined; - getAllTools(): AgentToolSchema[]; - getToolsBySkill(): Record; -} - -/** - * Agent loop service interface - */ -export interface IAgentLoop { - executeTask( - userMessage: string, - threadId: string, - options?: AgentExecutionOptions - ): Promise; - - cancelExecution(executionId: string): boolean; - getActiveExecutions(): string[]; - getExecutionStatus(executionId: string): AgentExecution | null; -} - -// ============================================================================= -// Event Types -// ============================================================================= - -/** - * Agent execution events for real-time UI updates - */ -export type AgentEvent = - | AgentExecutionStartedEvent - | AgentIterationStartedEvent - | AgentToolExecutionStartedEvent - | AgentToolExecutionCompletedEvent - | AgentExecutionCompletedEvent - | AgentExecutionErrorEvent; - -export interface AgentExecutionStartedEvent { - type: 'AGENT_EXECUTION_STARTED'; - executionId: string; - threadId: string; - userMessage: string; - timestamp: number; -} - -export interface AgentIterationStartedEvent { - type: 'AGENT_ITERATION_STARTED'; - executionId: string; - iteration: number; - timestamp: number; -} - -export interface AgentToolExecutionStartedEvent { - type: 'AGENT_TOOL_EXECUTION_STARTED'; - executionId: string; - toolExecution: AgentToolExecution; - timestamp: number; -} - -export interface AgentToolExecutionCompletedEvent { - type: 'AGENT_TOOL_EXECUTION_COMPLETED'; - executionId: string; - toolExecution: AgentToolExecution; - timestamp: number; -} - -export interface AgentExecutionCompletedEvent { - type: 'AGENT_EXECUTION_COMPLETED'; - executionId: string; - result: AgentExecutionResult; - timestamp: number; -} - -export interface AgentExecutionErrorEvent { - type: 'AGENT_EXECUTION_ERROR'; - executionId: string; - error: AgentError; - timestamp: number; -} - -// ============================================================================= -// Error Types -// ============================================================================= - -export interface AgentError extends Error { - type: AgentErrorType; - code?: string; - details?: Record; - retryable?: boolean; -} - -export type AgentErrorType = - | 'TOOL_EXECUTION_ERROR' - | 'TOOL_NOT_FOUND' - | 'SKILL_NOT_AVAILABLE' - | 'AGENT_TIMEOUT' - | 'MAX_ITERATIONS_EXCEEDED' - | 'API_ERROR' - | 'VALIDATION_ERROR' - | 'NETWORK_ERROR' - | 'UNKNOWN_ERROR'; \ No newline at end of file