remove: deprecated agent system files and types

- Removed unused `agent.ts` system types, agent loop service (`agentLoop.ts`), corresponding UI components (`AgentExecutionPanel.tsx`), and tests (`agentSlice.test.ts`).
- Cleaned up related imports and ensured no functionality depends on the deprecated files.
This commit is contained in:
cyrus
2026-03-10 01:12:31 +05:30
parent ae187de9d1
commit cae9f7335c
12 changed files with 132 additions and 2278 deletions
@@ -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 (
<svg className="w-4 h-4 text-canvas-400" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
);
case 'running':
return (
<div className="w-4 h-4 border-2 border-primary-500 border-t-transparent rounded-full animate-spin" />
);
case 'success':
return (
<svg className="w-4 h-4 text-sage-500" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
);
case 'error':
return (
<svg className="w-4 h-4 text-coral-500" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
</svg>
);
default:
return (
<div className="w-4 h-4 bg-canvas-300 rounded-full" />
);
}
};
const ToolExecutionItem = memo<{ toolExecution: AgentToolExecution }>(({ toolExecution }) => {
const duration = toolExecution.executionTimeMs || (toolExecution.endTime ? toolExecution.endTime - toolExecution.startTime : null);
return (
<div className="flex items-start gap-3 p-3 bg-canvas-50 rounded-lg border border-canvas-200">
<div className="flex-shrink-0 mt-0.5">
{getStatusIcon(toolExecution.status)}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<span className="font-medium text-canvas-900">{toolExecution.toolName}</span>
<span className="text-xs text-canvas-500 bg-canvas-200 px-2 py-0.5 rounded">
{toolExecution.skillId}
</span>
{duration && (
<span className="text-xs text-canvas-500">
{formatDuration(duration)}
</span>
)}
</div>
{/* Arguments */}
{toolExecution.arguments && (
<div className="mb-2">
<span className="text-xs font-medium text-canvas-600 block mb-1">Arguments:</span>
<pre className="text-xs text-canvas-700 bg-canvas-100 p-2 rounded border overflow-x-auto">
{JSON.stringify(JSON.parse(toolExecution.arguments), null, 2)}
</pre>
</div>
)}
{/* Result */}
{toolExecution.result && (
<div className="mb-2">
<span className="text-xs font-medium text-canvas-600 block mb-1">Result:</span>
<div className="text-sm text-canvas-800 bg-white p-2 rounded border">
{toolExecution.result}
</div>
</div>
)}
{/* Error */}
{toolExecution.errorMessage && (
<div className="mb-2">
<span className="text-xs font-medium text-coral-600 block mb-1">Error:</span>
<div className="text-sm text-coral-700 bg-coral-50 p-2 rounded border border-coral-200">
{toolExecution.errorMessage}
</div>
</div>
)}
</div>
</div>
);
});
ToolExecutionItem.displayName = 'ToolExecutionItem';
const AgentExecutionPanel = memo<AgentExecutionPanelProps>(({
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 (
<div className={`border border-canvas-200 rounded-lg bg-white ${className}`}>
<div className="p-4 border-b border-canvas-200">
<h3 className="text-sm font-semibold text-canvas-900">Agent Execution Details</h3>
</div>
<div className={`overflow-y-auto`} style={{ maxHeight }}>
{/* Active Execution */}
{activeExecution && (
<div className="p-4 border-b border-canvas-200">
<div className="flex items-center justify-between mb-3">
<h4 className="text-sm font-medium text-canvas-800">Current Execution</h4>
<span className="text-xs text-canvas-500">
Running for {formatDuration(Date.now() - activeExecution.startTime)}
</span>
</div>
<div className="mb-3">
<div className="text-xs text-canvas-600 mb-1">Progress:</div>
<div className="flex items-center gap-2">
<div className="flex-1 bg-canvas-200 rounded-full h-2">
<div
className="bg-primary-500 h-2 rounded-full transition-all duration-300"
style={{
width: `${(activeExecution.currentIteration / activeExecution.maxIterations) * 100}%`
}}
/>
</div>
<span className="text-xs text-canvas-600 font-medium">
{activeExecution.currentIteration}/{activeExecution.maxIterations}
</span>
</div>
</div>
{/* Tool Executions */}
{sortedToolExecutions.length > 0 && (
<div className="space-y-2">
<div className="text-xs font-medium text-canvas-600 mb-2">
Tool Executions ({sortedToolExecutions.length}):
</div>
{sortedToolExecutions.map(toolExecution => (
<ToolExecutionItem key={toolExecution.id} toolExecution={toolExecution} />
))}
</div>
)}
</div>
)}
{/* Execution History */}
{recentHistory.length > 0 && (
<div className="p-4">
<h4 className="text-sm font-medium text-canvas-800 mb-3">Recent Executions</h4>
<div className="space-y-2">
{recentHistory.map(entry => (
<div
key={entry.executionId}
className="flex items-center justify-between p-2 bg-canvas-50 rounded border"
>
<div className="flex items-center gap-2">
<div className={`w-2 h-2 rounded-full ${
entry.result.status === 'completed' ? 'bg-sage-500' :
entry.result.status === 'error' ? 'bg-coral-500' : 'bg-amber-500'
}`} />
<span className="text-xs text-canvas-700 font-medium">
{entry.result.status}
</span>
<span className="text-xs text-canvas-500">
{entry.result.toolExecutions.length} tools
</span>
</div>
<div className="text-xs text-canvas-500">
{formatDuration(entry.duration)}
</div>
</div>
))}
</div>
</div>
)}
{/* Empty State */}
{!activeExecution && recentHistory.length === 0 && (
<div className="p-6 text-center text-canvas-500">
<svg className="w-8 h-8 mx-auto mb-2 text-canvas-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
<p className="text-sm">No agent executions yet</p>
<p className="text-xs mt-1">Send a message to start an agent task</p>
</div>
)}
</div>
</div>
);
});
AgentExecutionPanel.displayName = 'AgentExecutionPanel';
export default AgentExecutionPanel;
@@ -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<AgentStatusIndicatorProps>(({ 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 (
<div className={`flex items-center gap-2 text-canvas-600 ${className}`}>
<div className="w-2 h-2 bg-sage-500 rounded-full"></div>
<span className="text-xs font-medium">Agent Ready</span>
</div>
);
}
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 (
<div className={`flex items-center gap-3 ${className}`}>
{/* Status indicator */}
<div className="flex items-center gap-2">
<div className={`w-2 h-2 rounded-full ${getStatusColor()}`}></div>
<span className="text-xs font-medium text-canvas-700">{getStatusText()}</span>
</div>
{/* Tool execution info */}
{toolCount > 0 && (
<div className="flex items-center gap-1.5 text-xs text-canvas-600">
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
</svg>
<span>{toolCount} tools</span>
{runningTools > 0 && (
<span className="text-primary-600"> {runningTools} running</span>
)}
</div>
)}
{/* Execution time */}
<div className="text-xs text-canvas-500">
{Math.floor((Date.now() - activeExecution.startTime) / 1000)}s
</div>
</div>
);
});
AgentStatusIndicator.displayName = 'AgentStatusIndicator';
export default AgentStatusIndicator;
-162
View File
@@ -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<AgentToggleProps>(({
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 (
<div className={`flex items-center gap-3 ${className}`}>
{/* Toggle Switch */}
<button
onClick={handleToggle}
disabled={isDisabled}
className={`
relative inline-flex items-center ${sizeClasses.container} rounded-full
transition-colors duration-200 ease-in-out
${agentMode
? 'bg-primary-500 hover:bg-primary-600'
: 'bg-canvas-300 hover:bg-canvas-400'
}
${isDisabled
? 'opacity-50 cursor-not-allowed'
: 'cursor-pointer focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2'
}
`}
aria-label={`${agentMode ? 'Disable' : 'Enable'} agent mode`}
>
<span
className={`
${sizeClasses.toggle} inline-block rounded-full bg-white shadow-sm
transform transition-transform duration-200 ease-in-out
${agentMode ? sizeClasses.translate : 'translate-x-0.5'}
`}
/>
{/* Loading indicator */}
{isLoading && (
<div className="absolute inset-0 flex items-center justify-center">
<div className="w-3 h-3 border-2 border-white border-t-transparent rounded-full animate-spin" />
</div>
)}
</button>
{/* Label and Status */}
<div className="flex flex-col">
<div className="flex items-center gap-2">
<span className={`text-sm font-medium ${agentMode ? 'text-canvas-900' : 'text-canvas-600'}`}>
Agent Mode
</span>
{agentMode && (
<span className="px-2 py-0.5 text-xs font-medium bg-primary-100 text-primary-700 rounded-full">
Active
</span>
)}
</div>
{/* Configuration hint */}
{agentMode && !activeExecution && (
<div className="text-xs text-canvas-500 mt-0.5">
{agentConfig.maxIterations ? `Max ${agentConfig.maxIterations} iterations` : 'Default settings'}
{agentConfig.allowedSkills && agentConfig.allowedSkills.length > 0 &&
`${agentConfig.allowedSkills.length} skills allowed`
}
</div>
)}
{/* Active execution status */}
{activeExecution && (
<div className="text-xs text-primary-600 mt-0.5 font-medium">
Running iteration {activeExecution.currentIteration}/{activeExecution.maxIterations}
</div>
)}
</div>
</div>
);
});
AgentToggle.displayName = 'AgentToggle';
export default AgentToggle;
-13
View File
@@ -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';
+99 -25
View File
@@ -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)}
</p>
</div>
<div className="flex-shrink-0">
<AgentToggle threadId={selectedThread.id} size="sm" />
</div>
</div>
{/* Messages */}
@@ -743,18 +829,6 @@ const Conversations = () => {
</div>
)}
{/* Agent Status and Execution Panel */}
<div className="flex-shrink-0">
<AgentStatusIndicator
threadId={selectedThread.id}
className="px-4 py-2 border-t border-white/10"
/>
<AgentExecutionPanel
threadId={selectedThread.id}
className="border-t border-white/10"
maxHeight="200px"
/>
</div>
{/* Message Input */}
<div className="flex-shrink-0 border-t border-white/10 px-4 py-3">
-388
View File
@@ -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<string, AbortController>();
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<AgentExecutionResult> {
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<AgentChatResponse>(
`/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<AgentToolExecution> {
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
}
};
}
}
+26 -2
View File
@@ -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;
-496
View File
@@ -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<typeof configureStore>;
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');
});
});
});
-433
View File
@@ -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<string | undefined>
) => {
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<AgentExecution>
) => {
const execution = action.payload;
state.activeExecutions[execution.id] = execution;
},
updateActiveExecution: (
state,
action: PayloadAction<Partial<AgentExecution> & { id: string }>
) => {
const { id, ...updates } = action.payload;
if (state.activeExecutions[id]) {
Object.assign(state.activeExecutions[id], updates);
}
},
removeActiveExecution: (
state,
action: PayloadAction<string>
) => {
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<AgentToolExecution>;
}>
) => {
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<AgentExecutionHistoryEntry>
) => {
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<string, AgentToolSchema[]> = {};
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<string>();
const categories: Record<string, number> = {};
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;
-9
View File
@@ -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({
+7 -39
View File
@@ -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(),
};
-379
View File
@@ -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<string, AgentToolParameter>;
required?: string[];
};
};
}
export interface AgentToolParameter {
type: 'string' | 'number' | 'boolean' | 'array' | 'object';
description?: string;
enum?: string[];
items?: AgentToolParameter;
properties?: Record<string, AgentToolParameter>;
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<string, boolean>;
// Active agent executions
activeExecutions: Record<string, AgentExecution>;
// Agent execution history (persisted)
executionHistory: AgentExecutionHistoryEntry[];
// Agent configuration per thread (persisted)
configByThreadId: Record<string, AgentExecutionOptions>;
// Tool registry cache (derived from skills)
toolRegistry: {
tools: AgentToolSchema[];
lastUpdated: number;
loading: boolean;
error?: string;
};
// UI state (not persisted)
ui: {
showExecutionDetails: Record<string, boolean>;
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<AgentToolSchema[]>;
executeTool(skillId: string, toolName: string, toolArguments: string): Promise<AgentToolExecution>;
getToolByName(toolName: string): AgentToolSchema | undefined;
getAllTools(): AgentToolSchema[];
getToolsBySkill(): Record<string, AgentToolSchema[]>;
}
/**
* Agent loop service interface
*/
export interface IAgentLoop {
executeTask(
userMessage: string,
threadId: string,
options?: AgentExecutionOptions
): Promise<AgentExecutionResult>;
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<string, any>;
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';