From ae187de9d1f2ffed9f6a871f3f67cbe72356856d Mon Sep 17 00:00:00 2001
From: cyrus
Date: Tue, 10 Mar 2026 00:40:00 +0530
Subject: [PATCH 1/3] feat: introduce agent system types, execution panel, and
service tests
- Added new `AgentSystem` types to support enhanced agent execution, tool tracking, API compatibility, and integration with thread system.
- Developed `AgentExecutionPanel` React component for detailed real-time monitoring of agent execution status and history.
- Created comprehensive tests for `AgentLoopService` covering execution flow, tool integration, error handling, and timeout scenarios.
---
.gitignore | 2 +
src-tauri/src/commands/runtime.rs | 452 +++++++++++++++
src-tauri/src/lib.rs | 6 +
src/components/agent/AgentExecutionPanel.tsx | 236 ++++++++
src/components/agent/AgentStatusIndicator.tsx | 96 ++++
src/components/agent/AgentToggle.tsx | 162 ++++++
src/components/agent/index.ts | 13 +
src/pages/Conversations.tsx | 17 +
src/services/__tests__/agentLoop.test.ts | 541 ++++++++++++++++++
.../__tests__/agentToolRegistry.test.ts | 367 ++++++++++++
src/services/agentLoop.ts | 388 +++++++++++++
src/services/agentToolRegistry.ts | 256 +++++++++
src/store/__tests__/agentSlice.test.ts | 496 ++++++++++++++++
src/store/agentSlice.ts | 433 ++++++++++++++
src/store/index.ts | 10 +
src/store/threadSlice.ts | 47 +-
src/types/agent.ts | 379 ++++++++++++
17 files changed, 3894 insertions(+), 7 deletions(-)
create mode 100644 src/components/agent/AgentExecutionPanel.tsx
create mode 100644 src/components/agent/AgentStatusIndicator.tsx
create mode 100644 src/components/agent/AgentToggle.tsx
create mode 100644 src/components/agent/index.ts
create mode 100644 src/services/__tests__/agentLoop.test.ts
create mode 100644 src/services/__tests__/agentToolRegistry.test.ts
create mode 100644 src/services/agentLoop.ts
create mode 100644 src/services/agentToolRegistry.ts
create mode 100644 src/store/__tests__/agentSlice.test.ts
create mode 100644 src/store/agentSlice.ts
create mode 100644 src/types/agent.ts
diff --git a/.gitignore b/.gitignore
index 64a5d7f2d..e740f0efc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,6 +20,8 @@ dist-ssr
.env.local
.env.*.local
+my_docs/*
+
# CI secrets for local testing (contains real tokens)
scripts/ci-secrets.json
scripts/ci-secrets.local.json
diff --git a/src-tauri/src/commands/runtime.rs b/src-tauri/src/commands/runtime.rs
index 47c0b6ecb..e305d8140 100644
--- a/src-tauri/src/commands/runtime.rs
+++ b/src-tauri/src/commands/runtime.rs
@@ -10,7 +10,9 @@ use crate::models::socket::SocketState;
use crate::runtime::socket_manager::SocketManager;
use crate::utils::config::get_backend_url;
use std::sync::Arc;
+use std::collections::HashMap;
use tauri::State;
+use serde::{Deserialize, Serialize};
// Desktop-only imports
#[cfg(not(any(target_os = "android", target_os = "ios")))]
@@ -40,6 +42,32 @@ pub struct ToolResult {
pub is_error: bool,
}
+// =============================================================================
+// ZeroClaw Format Compatibility Types
+// =============================================================================
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct ZeroClawToolSchema {
+ #[serde(rename = "type")]
+ pub type_field: String,
+ pub function: ZeroClawFunction,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct ZeroClawFunction {
+ pub name: String,
+ pub description: String,
+ pub parameters: serde_json::Value,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct ZeroClawToolResult {
+ pub success: bool,
+ pub output: String,
+ pub error: Option,
+ pub execution_time: Option,
+}
+
// =============================================================================
// Desktop implementations (V8 available)
// =============================================================================
@@ -257,6 +285,164 @@ mod desktop {
.to_string_lossy()
.to_string())
}
+
+ // =============================================================================
+ // ZeroClaw Format Compatibility Commands
+ // =============================================================================
+
+ /// Generate ZeroClaw-compatible tool schemas from all available QuickJS tools.
+ /// This bridges the gap between QuickJS runtime and OpenAI function calling format.
+ #[tauri::command]
+ pub async fn runtime_get_tool_schemas(
+ engine: State<'_, Arc>,
+ ) -> Result, String> {
+ log::info!("Generating ZeroClaw-compatible tool schemas");
+
+ let tools = engine.all_tools();
+ let mut schemas = Vec::new();
+
+ for (skill_id, tool) in tools {
+ // Extract tool information from ToolDefinition struct
+ let description = if tool.description.is_empty() {
+ "No description available".to_string()
+ } else {
+ tool.description.clone()
+ };
+
+ // Convert input schema to OpenAI-compatible format
+ let openai_parameters = convert_to_openai_schema(tool.input_schema)?;
+
+ let schema = ZeroClawToolSchema {
+ type_field: "function".to_string(),
+ function: ZeroClawFunction {
+ name: format!("{}_{}", skill_id, tool.name),
+ description,
+ parameters: openai_parameters,
+ },
+ };
+
+ schemas.push(schema);
+ }
+
+ log::info!("Generated {} ZeroClaw tool schemas", schemas.len());
+ Ok(schemas)
+ }
+
+ /// Execute a specific tool based on agent decision with enhanced validation.
+ /// This wraps the existing runtime_call_tool with ZeroClaw format compatibility.
+ #[tauri::command]
+ pub async fn runtime_execute_tool(
+ engine: State<'_, Arc>,
+ tool_id: String,
+ args: serde_json::Value,
+ ) -> Result {
+ let start_time = std::time::Instant::now();
+
+ log::info!("Executing ZeroClaw tool: {} with args: {}", tool_id, args);
+
+ // Parse tool_id to get skill_id and tool_name (format: "skill_id_tool_name")
+ let (skill_id, tool_name) = match parse_tool_id(&tool_id) {
+ Ok((skill, tool)) => (skill, tool),
+ Err(e) => {
+ let execution_time = start_time.elapsed().as_millis() as u64;
+ return Ok(ZeroClawToolResult {
+ success: false,
+ output: String::new(),
+ error: Some(format!("Invalid tool ID format: {}", e)),
+ execution_time: Some(execution_time),
+ });
+ }
+ };
+
+ // Execute the tool using the existing command
+ match engine.call_tool(&skill_id, &tool_name, args).await {
+ Ok(result) => {
+ let execution_time = start_time.elapsed().as_millis() as u64;
+
+ if result.is_error {
+ let error_message = result.content
+ .iter()
+ .filter(|c| matches!(c, crate::runtime::types::ToolContent::Text { .. }))
+ .map(|c| match c {
+ crate::runtime::types::ToolContent::Text { text } => text.as_str(),
+ _ => "",
+ })
+ .collect::>()
+ .join("\n");
+
+ Ok(ZeroClawToolResult {
+ success: false,
+ output: String::new(),
+ error: Some(error_message),
+ execution_time: Some(execution_time),
+ })
+ } else {
+ let output = result.content
+ .iter()
+ .map(|c| match c {
+ crate::runtime::types::ToolContent::Text { text } => text.clone(),
+ crate::runtime::types::ToolContent::Json { data } => {
+ serde_json::to_string(data).unwrap_or_else(|_| "Invalid JSON".to_string())
+ }
+ })
+ .collect::>()
+ .join("\n");
+
+ log::info!("ZeroClaw tool execution completed in {}ms", execution_time);
+
+ Ok(ZeroClawToolResult {
+ success: true,
+ output,
+ error: None,
+ execution_time: Some(execution_time),
+ })
+ }
+ }
+ Err(e) => {
+ let execution_time = start_time.elapsed().as_millis() as u64;
+ log::error!("ZeroClaw tool execution failed: {}", e);
+
+ Ok(ZeroClawToolResult {
+ success: false,
+ output: String::new(),
+ error: Some(e),
+ execution_time: Some(execution_time),
+ })
+ }
+ }
+ }
+
+ // Helper function to parse tool_id format: "skill_id_tool_name"
+ pub fn parse_tool_id(tool_id: &str) -> Result<(String, String), String> {
+ // Find the first underscore to separate skill_id from tool_name
+ if let Some(underscore_pos) = tool_id.find('_') {
+ let skill_id = tool_id[..underscore_pos].to_string();
+ let tool_name = tool_id[underscore_pos + 1..].to_string();
+
+ if skill_id.is_empty() || tool_name.is_empty() {
+ return Err("Tool ID must be in format 'skill_id_tool_name'".to_string());
+ }
+
+ Ok((skill_id, tool_name))
+ } else {
+ Err("Tool ID must contain an underscore separator".to_string())
+ }
+ }
+
+ // Helper function to convert MCP schema to OpenAI function calling format
+ pub fn convert_to_openai_schema(mcp_schema: serde_json::Value) -> Result {
+ // If it's already in OpenAI format, return as-is
+ if mcp_schema.is_object() && mcp_schema.get("type").is_some() {
+ return Ok(mcp_schema);
+ }
+
+ // Convert basic MCP schema to OpenAI format
+ Ok(serde_json::json!({
+ "type": "object",
+ "properties": mcp_schema.get("properties").cloned().unwrap_or_else(|| serde_json::json!({})),
+ "required": mcp_schema.get("required").cloned().unwrap_or_else(|| serde_json::json!([]))
+ }))
+ }
}
// =============================================================================
@@ -379,6 +565,19 @@ mod mobile {
pub async fn runtime_skill_data_dir(_skill_id: String) -> Result {
Err(MOBILE_ERROR.to_string())
}
+
+ #[tauri::command]
+ pub async fn runtime_get_tool_schemas() -> Result, String> {
+ Ok(vec![])
+ }
+
+ #[tauri::command]
+ pub async fn runtime_execute_tool(
+ _tool_id: String,
+ _args: serde_json::Value,
+ ) -> Result {
+ Err(MOBILE_ERROR.to_string())
+ }
}
// =============================================================================
@@ -432,3 +631,256 @@ pub use desktop::*;
#[cfg(any(target_os = "android", target_os = "ios"))]
pub use mobile::*;
+
+// =============================================================================
+// Tests
+// =============================================================================
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::sync::Arc;
+
+ #[cfg(not(any(target_os = "android", target_os = "ios")))]
+ mod desktop_tests {
+ use super::*;
+ use crate::runtime::qjs_engine::RuntimeEngine;
+
+ #[tokio::test]
+ async fn test_runtime_get_tool_schemas_format() {
+ // Note: This test requires a properly initialized RuntimeEngine
+ // In a real test environment, you would mock the engine or use a test instance
+
+ // For now, we'll test the struct format and serialization
+ let schema = ZeroClawToolSchema {
+ type_field: "function".to_string(),
+ function: ZeroClawFunction {
+ name: "test_tool".to_string(),
+ description: "A test tool".to_string(),
+ parameters: serde_json::json!({
+ "type": "object",
+ "properties": {
+ "message": {
+ "type": "string",
+ "description": "Test message"
+ }
+ },
+ "required": ["message"]
+ })
+ }
+ };
+
+ // Test serialization
+ let json = serde_json::to_string(&schema).expect("Should serialize to JSON");
+ assert!(json.contains("function"));
+ assert!(json.contains("test_tool"));
+ assert!(json.contains("A test tool"));
+
+ // Test deserialization
+ let deserialized: ZeroClawToolSchema = serde_json::from_str(&json)
+ .expect("Should deserialize from JSON");
+ assert_eq!(deserialized.type_field, "function");
+ assert_eq!(deserialized.function.name, "test_tool");
+ }
+
+ #[tokio::test]
+ async fn test_zeroclaw_tool_result_format() {
+ let result = ZeroClawToolResult {
+ success: true,
+ output: "Test output".to_string(),
+ error: None,
+ execution_time: Some(1500)
+ };
+
+ // Test serialization
+ let json = serde_json::to_string(&result).expect("Should serialize to JSON");
+ assert!(json.contains("true"));
+ assert!(json.contains("Test output"));
+ assert!(json.contains("1500"));
+
+ // Test error case
+ let error_result = ZeroClawToolResult {
+ success: false,
+ output: String::new(),
+ error: Some("Tool not found".to_string()),
+ execution_time: Some(100)
+ };
+
+ let error_json = serde_json::to_string(&error_result).expect("Should serialize error");
+ assert!(json.contains("false") || error_json.contains("false"));
+ assert!(error_json.contains("Tool not found"));
+ }
+
+ #[test]
+ fn test_parse_tool_id_valid_formats() {
+ // Test valid tool ID formats
+ let (skill_id, tool_name) = desktop::parse_tool_id("github_list_issues")
+ .expect("Should parse valid tool ID");
+ assert_eq!(skill_id, "github");
+ assert_eq!(tool_name, "list_issues");
+
+ let (skill_id, tool_name) = desktop::parse_tool_id("notion_create_page")
+ .expect("Should parse valid tool ID");
+ assert_eq!(skill_id, "notion");
+ assert_eq!(tool_name, "create_page");
+
+ // Test complex skill names (first underscore separates skill_id from tool_name)
+ let (skill_id, tool_name) = desktop::parse_tool_id("complex_skill_name_tool_function")
+ .expect("Should parse complex tool ID");
+ assert_eq!(skill_id, "complex");
+ assert_eq!(tool_name, "skill_name_tool_function");
+ }
+
+ #[test]
+ fn test_parse_tool_id_invalid_formats() {
+ // Test invalid formats
+ assert!(desktop::parse_tool_id("nounderscore").is_err(), "Should fail for no underscore");
+ assert!(desktop::parse_tool_id("_empty_skill").is_err(), "Should fail for empty skill ID");
+ assert!(desktop::parse_tool_id("empty_tool_").is_err(), "Should fail for empty tool name");
+ assert!(desktop::parse_tool_id("").is_err(), "Should fail for empty string");
+ }
+
+ #[test]
+ fn test_convert_to_openai_schema() {
+ // Test MCP schema to OpenAI conversion
+ let mcp_schema = serde_json::json!({
+ "properties": {
+ "owner": {"type": "string"},
+ "repo": {"type": "string"}
+ },
+ "required": ["owner", "repo"]
+ });
+
+ let openai_schema = desktop::convert_to_openai_schema(mcp_schema)
+ .expect("Should convert MCP to OpenAI schema");
+
+ assert_eq!(openai_schema["type"], "object");
+ assert!(openai_schema["properties"].is_object());
+ assert!(openai_schema["required"].is_array());
+
+ // Test already OpenAI format (should pass through)
+ let existing_openai = serde_json::json!({
+ "type": "object",
+ "properties": {"test": {"type": "string"}},
+ "required": ["test"]
+ });
+
+ let result = desktop::convert_to_openai_schema(existing_openai.clone())
+ .expect("Should handle existing OpenAI format");
+ assert_eq!(result, existing_openai);
+ }
+
+ #[test]
+ fn test_zeroclaw_format_compliance() {
+ // Test that our ZeroClaw format matches expected OpenAI structure
+ let schema = ZeroClawToolSchema {
+ type_field: "function".to_string(),
+ function: ZeroClawFunction {
+ name: "github_list_issues".to_string(),
+ description: "List GitHub issues for a repository".to_string(),
+ parameters: serde_json::json!({
+ "type": "object",
+ "properties": {
+ "owner": {"type": "string", "description": "Repository owner"},
+ "repo": {"type": "string", "description": "Repository name"},
+ "state": {"type": "string", "enum": ["open", "closed", "all"], "default": "open"}
+ },
+ "required": ["owner", "repo"]
+ })
+ }
+ };
+
+ // Serialize and check format
+ let json = serde_json::to_value(&schema).expect("Should serialize");
+
+ // Check OpenAI compatibility
+ assert_eq!(json["type"], "function");
+ assert!(json["function"].is_object());
+ assert!(json["function"]["name"].is_string());
+ assert!(json["function"]["description"].is_string());
+ assert!(json["function"]["parameters"].is_object());
+
+ // Check parameter schema
+ let params = &json["function"]["parameters"];
+ assert_eq!(params["type"], "object");
+ assert!(params["properties"].is_object());
+ assert!(params["required"].is_array());
+ }
+ }
+
+ #[cfg(any(target_os = "android", target_os = "ios"))]
+ mod mobile_tests {
+ use super::*;
+
+ #[tokio::test]
+ async fn test_mobile_stub_runtime_get_tool_schemas() {
+ let result = mobile::runtime_get_tool_schemas().await;
+
+ // Mobile should return empty list with helpful error
+ assert!(result.is_err());
+ assert!(result.unwrap_err().contains("not available on mobile"));
+ }
+
+ #[tokio::test]
+ async fn test_mobile_stub_runtime_execute_tool() {
+ let result = mobile::runtime_execute_tool(
+ "test_tool".to_string(),
+ "{}".to_string()
+ ).await;
+
+ // Mobile should return error
+ assert!(result.is_err());
+ assert!(result.unwrap_err().contains("not available on mobile"));
+ }
+ }
+
+ #[test]
+ fn test_zeroclaw_struct_defaults() {
+ // Test that ZeroClaw structs can be created with serde_json
+ let tool_schema: ZeroClawToolSchema = serde_json::from_value(serde_json::json!({
+ "type": "function",
+ "function": {
+ "name": "test",
+ "description": "test",
+ "parameters": {}
+ }
+ })).expect("Should deserialize from JSON");
+
+ assert_eq!(tool_schema.type_field, "function");
+ assert_eq!(tool_schema.function.name, "test");
+
+ // Test tool result
+ let tool_result: ZeroClawToolResult = serde_json::from_value(serde_json::json!({
+ "success": true,
+ "output": "result",
+ "error": null,
+ "execution_time": 1000
+ })).expect("Should deserialize tool result");
+
+ assert!(tool_result.success);
+ assert_eq!(tool_result.output, "result");
+ assert_eq!(tool_result.execution_time, Some(1000));
+ }
+
+ #[test]
+ fn test_error_handling_structures() {
+ // Test that error scenarios can be properly serialized
+ let error_result = ZeroClawToolResult {
+ success: false,
+ output: String::new(),
+ error: Some("Connection timeout".to_string()),
+ execution_time: Some(30000) // 30 second timeout
+ };
+
+ let json = serde_json::to_string(&error_result).expect("Should serialize error");
+ assert!(json.contains("false"));
+ assert!(json.contains("Connection timeout"));
+ assert!(json.contains("30000"));
+
+ // Test deserialization back
+ let parsed: ZeroClawToolResult = serde_json::from_str(&json)
+ .expect("Should parse error result");
+ assert!(!parsed.success);
+ assert_eq!(parsed.error, Some("Connection timeout".to_string()));
+ }
+}
diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs
index 0944fe168..99697ca9d 100644
--- a/src-tauri/src/lib.rs
+++ b/src-tauri/src/lib.rs
@@ -142,6 +142,8 @@ macro_rules! common_handlers {
runtime_get_skill_state,
runtime_call_tool,
runtime_all_tools,
+ runtime_get_tool_schemas,
+ runtime_execute_tool,
runtime_broadcast_event,
// Runtime enable/disable + KV commands
runtime_enable_skill,
@@ -751,6 +753,8 @@ pub fn run() {
runtime_get_skill_state,
runtime_call_tool,
runtime_all_tools,
+ runtime_get_tool_schemas,
+ runtime_execute_tool,
runtime_broadcast_event,
// Runtime enable/disable + KV commands
runtime_enable_skill,
@@ -870,6 +874,8 @@ pub fn run() {
runtime_get_skill_state,
runtime_call_tool,
runtime_all_tools,
+ runtime_get_tool_schemas,
+ runtime_execute_tool,
runtime_broadcast_event,
// Runtime enable/disable + KV commands
runtime_enable_skill,
diff --git a/src/components/agent/AgentExecutionPanel.tsx b/src/components/agent/AgentExecutionPanel.tsx
new file mode 100644
index 000000000..54ada33a5
--- /dev/null
+++ b/src/components/agent/AgentExecutionPanel.tsx
@@ -0,0 +1,236 @@
+/**
+ * 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
new file mode 100644
index 000000000..87e4d9238
--- /dev/null
+++ b/src/components/agent/AgentStatusIndicator.tsx
@@ -0,0 +1,96 @@
+/**
+ * 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 (
+
+ );
+ }
+
+ 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 */}
+
+
+ {/* 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
new file mode 100644
index 000000000..7f648d62a
--- /dev/null
+++ b/src/components/agent/AgentToggle.tsx
@@ -0,0 +1,162 @@
+/**
+ * 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 */}
+
+
+
+ {/* Loading indicator */}
+ {isLoading && (
+
+ )}
+
+
+ {/* 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
new file mode 100644
index 000000000..3305c8fdb
--- /dev/null
+++ b/src/components/agent/index.ts
@@ -0,0 +1,13 @@
+/**
+ * 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 79a19f0fe..182642c75 100644
--- a/src/pages/Conversations.tsx
+++ b/src/pages/Conversations.tsx
@@ -12,6 +12,7 @@ import { useNavigate, useParams } from 'react-router-dom';
import { inferenceApi, type ModelInfo } from '../services/api/inferenceApi';
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,
@@ -595,6 +596,9 @@ const Conversations = () => {
Created {formatRelativeTime(selectedThread.createdAt)}
+
{/* Messages */}
@@ -739,6 +743,19 @@ const Conversations = () => {
)}
+ {/* Agent Status and Execution Panel */}
+
+
{/* Message Input */}
{/* Model selector */}
diff --git a/src/services/__tests__/agentLoop.test.ts b/src/services/__tests__/agentLoop.test.ts
new file mode 100644
index 000000000..663fee8d1
--- /dev/null
+++ b/src/services/__tests__/agentLoop.test.ts
@@ -0,0 +1,541 @@
+import { describe, test, expect, beforeEach, vi, type Mock } from 'vitest';
+import { AgentLoopService } from '../agentLoop';
+import { AgentToolRegistry } from '../agentToolRegistry';
+import { apiClient } from '../apiClient';
+import type {
+ AgentToolSchema,
+ AgentExecutionResult,
+ AgentToolExecution,
+ AgentExecutionOptions
+} from '../../types/agent';
+
+// Mock dependencies
+vi.mock('../agentToolRegistry');
+vi.mock('../apiClient');
+
+describe('AgentLoopService', () => {
+ let service: AgentLoopService;
+ const mockToolRegistry = AgentToolRegistry as vi.MockedClass
;
+ const mockApiClient = apiClient as { post: Mock };
+
+ const mockToolSchemas: AgentToolSchema[] = [
+ {
+ type: "function",
+ function: {
+ name: "github_list_issues",
+ description: "List GitHub issues for a repository",
+ parameters: {
+ type: "object",
+ properties: {
+ owner: { type: "string", description: "Repository owner" },
+ repo: { type: "string", description: "Repository name" }
+ },
+ required: ["owner", "repo"]
+ }
+ }
+ },
+ {
+ type: "function",
+ function: {
+ name: "notion_create_page",
+ description: "Create a new Notion page",
+ parameters: {
+ type: "object",
+ properties: {
+ title: { type: "string", description: "Page title" },
+ content: { type: "string", description: "Page content" }
+ },
+ required: ["title"]
+ }
+ }
+ }
+ ];
+
+ beforeEach(() => {
+ service = AgentLoopService.getInstance();
+ vi.clearAllMocks();
+
+ // Setup default mock implementations
+ const mockRegistryInstance = {
+ loadToolSchemas: vi.fn(),
+ executeTool: vi.fn()
+ };
+
+ mockToolRegistry.getInstance.mockReturnValue(mockRegistryInstance as any);
+ mockRegistryInstance.loadToolSchemas.mockResolvedValue(mockToolSchemas);
+ });
+
+ describe('executeTask', () => {
+ test('should execute simple task without tool calls', async () => {
+ const mockResponse = {
+ choices: [{
+ message: {
+ role: 'assistant' as const,
+ content: 'Hello! How can I help you today?',
+ tool_calls: undefined
+ },
+ finish_reason: 'stop' as const
+ }],
+ usage: {
+ prompt_tokens: 20,
+ completion_tokens: 10,
+ total_tokens: 30
+ }
+ };
+
+ mockApiClient.post.mockResolvedValue({ data: mockResponse });
+
+ const result = await service.executeTask(
+ 'Hello',
+ 'conv_123',
+ { maxIterations: 5, timeoutMs: 30000 }
+ );
+
+ expect(result.status).toBe('completed');
+ expect(result.finalResponse).toBe('Hello! How can I help you today?');
+ expect(result.iterations).toBe(1);
+ expect(result.toolExecutions).toHaveLength(0);
+ expect(result.executionTime).toBeGreaterThan(0);
+
+ // Verify API call format
+ expect(mockApiClient.post).toHaveBeenCalledWith(
+ '/api/v1/conversations/conv_123/messages',
+ expect.objectContaining({
+ model: expect.any(String),
+ messages: expect.arrayContaining([
+ expect.objectContaining({
+ role: 'user',
+ content: 'Hello'
+ })
+ ]),
+ tools: mockToolSchemas,
+ tool_choice: 'auto'
+ })
+ );
+ });
+
+ test('should execute task with single tool call', async () => {
+ const mockToolCallResponse = {
+ choices: [{
+ message: {
+ role: 'assistant' as const,
+ content: null,
+ tool_calls: [{
+ id: 'call_123',
+ type: 'function' as const,
+ function: {
+ name: 'github_list_issues',
+ arguments: '{"owner":"user","repo":"test"}'
+ }
+ }]
+ },
+ finish_reason: 'tool_calls' as const
+ }]
+ };
+
+ const mockFinalResponse = {
+ choices: [{
+ message: {
+ role: 'assistant' as const,
+ content: 'I found 3 open issues in your repository.',
+ tool_calls: undefined
+ },
+ finish_reason: 'stop' as const
+ }],
+ usage: {
+ prompt_tokens: 50,
+ completion_tokens: 20,
+ total_tokens: 70
+ }
+ };
+
+ const mockToolExecution: AgentToolExecution = {
+ id: 'exec_123',
+ toolName: 'list_issues',
+ skillId: 'github',
+ arguments: '{"owner":"user","repo":"test"}',
+ status: 'success',
+ startTime: Date.now() - 1500,
+ endTime: Date.now(),
+ executionTimeMs: 1500,
+ result: '{"issues":[{"title":"Bug fix","number":1}]}'
+ };
+
+ // Setup mocks
+ const mockRegistryInstance = mockToolRegistry.getInstance();
+ mockRegistryInstance.executeTool.mockResolvedValue(mockToolExecution);
+
+ mockApiClient.post
+ .mockResolvedValueOnce({ data: mockToolCallResponse })
+ .mockResolvedValueOnce({ data: mockFinalResponse });
+
+ const result = await service.executeTask(
+ 'Show me GitHub issues',
+ 'conv_123'
+ );
+
+ expect(result.status).toBe('completed');
+ expect(result.finalResponse).toBe('I found 3 open issues in your repository.');
+ expect(result.iterations).toBe(2);
+ expect(result.toolExecutions).toHaveLength(1);
+ expect(result.toolExecutions[0].toolName).toBe('list_issues');
+ expect(result.toolExecutions[0].status).toBe('success');
+
+ // Verify tool execution was called with correct parameters
+ expect(mockRegistryInstance.executeTool).toHaveBeenCalledWith(
+ 'github',
+ 'list_issues',
+ '{"owner":"user","repo":"test"}'
+ );
+ });
+
+ test('should handle multiple tool calls in sequence', async () => {
+ const mockFirstToolCallResponse = {
+ choices: [{
+ message: {
+ role: 'assistant' as const,
+ content: null,
+ tool_calls: [{
+ id: 'call_1',
+ type: 'function' as const,
+ function: {
+ name: 'github_list_issues',
+ arguments: '{"owner":"user","repo":"test"}'
+ }
+ }]
+ },
+ finish_reason: 'tool_calls' as const
+ }]
+ };
+
+ const mockSecondToolCallResponse = {
+ choices: [{
+ message: {
+ role: 'assistant' as const,
+ content: null,
+ tool_calls: [{
+ id: 'call_2',
+ type: 'function' as const,
+ function: {
+ name: 'notion_create_page',
+ arguments: '{"title":"Issues Summary"}'
+ }
+ }]
+ },
+ finish_reason: 'tool_calls' as const
+ }]
+ };
+
+ const mockFinalResponse = {
+ choices: [{
+ message: {
+ role: 'assistant' as const,
+ content: 'I created a summary page with your GitHub issues.',
+ tool_calls: undefined
+ },
+ finish_reason: 'stop' as const
+ }]
+ };
+
+ const mockToolExecution1: AgentToolExecution = {
+ id: 'exec_1',
+ toolName: 'list_issues',
+ skillId: 'github',
+ arguments: '{"owner":"user","repo":"test"}',
+ status: 'success',
+ startTime: Date.now() - 2000,
+ endTime: Date.now() - 1000,
+ executionTimeMs: 1000,
+ result: '{"issues":[{"title":"Bug fix","number":1}]}'
+ };
+
+ const mockToolExecution2: AgentToolExecution = {
+ id: 'exec_2',
+ toolName: 'create_page',
+ skillId: 'notion',
+ arguments: '{"title":"Issues Summary"}',
+ status: 'success',
+ startTime: Date.now() - 800,
+ endTime: Date.now(),
+ executionTimeMs: 800,
+ result: '{"page_id":"page_123"}'
+ };
+
+ // Setup mocks
+ const mockRegistryInstance = mockToolRegistry.getInstance();
+ mockRegistryInstance.executeTool
+ .mockResolvedValueOnce(mockToolExecution1)
+ .mockResolvedValueOnce(mockToolExecution2);
+
+ mockApiClient.post
+ .mockResolvedValueOnce({ data: mockFirstToolCallResponse })
+ .mockResolvedValueOnce({ data: mockSecondToolCallResponse })
+ .mockResolvedValueOnce({ data: mockFinalResponse });
+
+ const result = await service.executeTask(
+ 'Get GitHub issues and create a summary page',
+ 'conv_123',
+ { maxIterations: 5 }
+ );
+
+ expect(result.status).toBe('completed');
+ expect(result.iterations).toBe(3);
+ expect(result.toolExecutions).toHaveLength(2);
+ expect(result.toolExecutions[0].skillId).toBe('github');
+ expect(result.toolExecutions[1].skillId).toBe('notion');
+ });
+
+ test('should handle tool execution timeout', async () => {
+ const result = await service.executeTask(
+ 'Test timeout',
+ 'conv_123',
+ { maxIterations: 1, timeoutMs: 100 } // Very short timeout
+ );
+
+ // The timeout logic depends on how it's implemented in the actual service
+ // This test may need adjustment based on the actual implementation
+ expect(result.status).toBe('timeout');
+ expect(result.error).toContain('timeout');
+ });
+
+ test('should respect maximum iterations limit', async () => {
+ const mockToolCallResponse = {
+ choices: [{
+ message: {
+ role: 'assistant' as const,
+ tool_calls: [{
+ id: 'call_1',
+ type: 'function' as const,
+ function: { name: 'github_list_issues', arguments: '{}' }
+ }]
+ },
+ finish_reason: 'tool_calls' as const
+ }]
+ };
+
+ // Mock to always return tool calls (infinite loop scenario)
+ mockApiClient.post.mockResolvedValue({ data: mockToolCallResponse });
+
+ const mockToolExecution: AgentToolExecution = {
+ id: 'exec_1',
+ toolName: 'list_issues',
+ skillId: 'github',
+ arguments: '{}',
+ status: 'success',
+ startTime: Date.now() - 100,
+ endTime: Date.now(),
+ executionTimeMs: 100,
+ result: '{}'
+ };
+
+ const mockRegistryInstance = mockToolRegistry.getInstance();
+ mockRegistryInstance.executeTool.mockResolvedValue(mockToolExecution);
+
+ const result = await service.executeTask(
+ 'Infinite loop test',
+ 'conv_123',
+ { maxIterations: 2, timeoutMs: 10000 }
+ );
+
+ expect(result.status).toBe('max_iterations');
+ expect(result.iterations).toBe(2);
+ expect(result.error).toContain('maximum iterations');
+ });
+
+ test('should handle tool execution error gracefully', async () => {
+ const mockToolCallResponse = {
+ choices: [{
+ message: {
+ role: 'assistant' as const,
+ tool_calls: [{
+ id: 'call_1',
+ type: 'function' as const,
+ function: {
+ name: 'invalid_tool',
+ arguments: '{}'
+ }
+ }]
+ },
+ finish_reason: 'tool_calls' as const
+ }]
+ };
+
+ const mockErrorResponse = {
+ choices: [{
+ message: {
+ role: 'assistant' as const,
+ content: 'I encountered an error while executing the tool.',
+ tool_calls: undefined
+ },
+ finish_reason: 'stop' as const
+ }]
+ };
+
+ const mockToolExecution: AgentToolExecution = {
+ id: 'exec_1',
+ toolName: 'invalid_tool',
+ skillId: 'unknown',
+ arguments: '{}',
+ status: 'error',
+ startTime: Date.now() - 100,
+ endTime: Date.now(),
+ executionTimeMs: 100,
+ errorMessage: 'Tool not found'
+ };
+
+ const mockRegistryInstance = mockToolRegistry.getInstance();
+ mockRegistryInstance.executeTool.mockResolvedValue(mockToolExecution);
+
+ mockApiClient.post
+ .mockResolvedValueOnce({ data: mockToolCallResponse })
+ .mockResolvedValueOnce({ data: mockErrorResponse });
+
+ const result = await service.executeTask(
+ 'Test error handling',
+ 'conv_123'
+ );
+
+ expect(result.status).toBe('completed');
+ expect(result.toolExecutions).toHaveLength(1);
+ expect(result.toolExecutions[0].status).toBe('error');
+ expect(result.toolExecutions[0].errorMessage).toBe('Tool not found');
+ });
+
+ test('should handle API client errors', async () => {
+ mockApiClient.post.mockRejectedValue(new Error('Network error'));
+
+ const result = await service.executeTask(
+ 'Test API error',
+ 'conv_123'
+ );
+
+ expect(result.status).toBe('error');
+ expect(result.error).toContain('Network error');
+ expect(result.iterations).toBe(0);
+ expect(result.toolExecutions).toHaveLength(0);
+ });
+
+ test('should parse tool name from function name correctly', async () => {
+ const mockToolCallResponse = {
+ choices: [{
+ message: {
+ role: 'assistant' as const,
+ tool_calls: [{
+ id: 'call_1',
+ type: 'function' as const,
+ function: {
+ name: 'github_list_issues', // Should parse to skillId=github, toolName=list_issues
+ arguments: '{"owner":"user","repo":"test"}'
+ }
+ }]
+ },
+ finish_reason: 'tool_calls' as const
+ }]
+ };
+
+ const mockFinalResponse = {
+ choices: [{
+ message: {
+ role: 'assistant' as const,
+ content: 'Done',
+ tool_calls: undefined
+ },
+ finish_reason: 'stop' as const
+ }]
+ };
+
+ const mockToolExecution: AgentToolExecution = {
+ id: 'exec_1',
+ toolName: 'list_issues',
+ skillId: 'github',
+ arguments: '{"owner":"user","repo":"test"}',
+ status: 'success',
+ startTime: Date.now() - 100,
+ endTime: Date.now(),
+ executionTimeMs: 100,
+ result: '{}'
+ };
+
+ const mockRegistryInstance = mockToolRegistry.getInstance();
+ mockRegistryInstance.executeTool.mockResolvedValue(mockToolExecution);
+
+ mockApiClient.post
+ .mockResolvedValueOnce({ data: mockToolCallResponse })
+ .mockResolvedValueOnce({ data: mockFinalResponse });
+
+ await service.executeTask('Test tool parsing', 'conv_123');
+
+ // Verify correct parsing of skill ID and tool name
+ expect(mockRegistryInstance.executeTool).toHaveBeenCalledWith(
+ 'github',
+ 'list_issues',
+ '{"owner":"user","repo":"test"}'
+ );
+ });
+ });
+
+ describe('singleton behavior', () => {
+ test('should return the same instance', () => {
+ const instance1 = AgentLoopService.getInstance();
+ const instance2 = AgentLoopService.getInstance();
+
+ expect(instance1).toBe(instance2);
+ });
+ });
+
+ describe('task execution options', () => {
+ test('should use default options when none provided', async () => {
+ const mockResponse = {
+ choices: [{
+ message: {
+ role: 'assistant' as const,
+ content: 'Test response'
+ },
+ finish_reason: 'stop' as const
+ }]
+ };
+
+ mockApiClient.post.mockResolvedValue({ data: mockResponse });
+
+ const result = await service.executeTask('Test', 'conv_123');
+
+ // Should complete successfully with defaults
+ expect(result.status).toBe('completed');
+ expect(result.executionTime).toBeGreaterThan(0);
+ });
+
+ test('should respect custom execution options', async () => {
+ const mockResponse = {
+ choices: [{
+ message: {
+ role: 'assistant' as const,
+ content: 'Test response'
+ },
+ finish_reason: 'stop' as const
+ }]
+ };
+
+ mockApiClient.post.mockResolvedValue({ data: mockResponse });
+
+ const customOptions: AgentExecutionOptions = {
+ maxIterations: 3,
+ timeoutMs: 5000,
+ model: 'gpt-3.5-turbo',
+ temperature: 0.7
+ };
+
+ const result = await service.executeTask('Test', 'conv_123', customOptions);
+
+ expect(result.status).toBe('completed');
+
+ // Verify custom options were passed to API
+ expect(mockApiClient.post).toHaveBeenCalledWith(
+ '/api/v1/conversations/conv_123/messages',
+ expect.objectContaining({
+ model: 'gpt-3.5-turbo',
+ temperature: 0.7
+ })
+ );
+ });
+ });
+});
\ No newline at end of file
diff --git a/src/services/__tests__/agentToolRegistry.test.ts b/src/services/__tests__/agentToolRegistry.test.ts
new file mode 100644
index 000000000..3d3251a4e
--- /dev/null
+++ b/src/services/__tests__/agentToolRegistry.test.ts
@@ -0,0 +1,367 @@
+import { describe, test, expect, beforeEach, vi, type Mock } from 'vitest';
+import { AgentToolRegistry } from '../agentToolRegistry';
+import { invoke } from '@tauri-apps/api/core';
+
+// Mock Tauri invoke
+vi.mock('@tauri-apps/api/core');
+
+describe('AgentToolRegistry', () => {
+ let service: AgentToolRegistry;
+ const mockInvoke = invoke as Mock;
+
+ beforeEach(() => {
+ service = AgentToolRegistry.getInstance();
+ vi.clearAllMocks();
+ service.clearCache(); // Clear cache between tests
+ });
+
+ describe('loadToolSchemas', () => {
+ test('should load tool schemas from Tauri using ZeroClaw format', async () => {
+ const mockSchemas = [
+ {
+ type: "function",
+ function: {
+ name: "github_list_issues",
+ description: "List GitHub issues for a repository",
+ parameters: {
+ type: "object",
+ properties: {
+ owner: { type: "string", description: "Repository owner" },
+ repo: { type: "string", description: "Repository name" }
+ },
+ required: ["owner", "repo"]
+ }
+ }
+ },
+ {
+ type: "function",
+ function: {
+ name: "notion_create_page",
+ description: "Create a new Notion page",
+ parameters: {
+ type: "object",
+ properties: {
+ title: { type: "string", description: "Page title" },
+ content: { type: "string", description: "Page content" }
+ },
+ required: ["title"]
+ }
+ }
+ }
+ ];
+
+ mockInvoke.mockResolvedValue(mockSchemas);
+
+ const schemas = await service.loadToolSchemas();
+
+ expect(schemas).toHaveLength(2);
+ expect(schemas[0].function.name).toBe("github_list_issues");
+ expect(schemas[1].function.name).toBe("notion_create_page");
+ expect(mockInvoke).toHaveBeenCalledWith('runtime_get_tool_schemas');
+ });
+
+ test('should cache tool schemas to avoid repeated calls', async () => {
+ const mockSchemas = [
+ {
+ type: "function",
+ function: {
+ name: "test_tool",
+ description: "Test tool",
+ parameters: { type: "object", properties: {} }
+ }
+ }
+ ];
+
+ mockInvoke.mockResolvedValue(mockSchemas);
+
+ // First call
+ const schemas1 = await service.loadToolSchemas();
+ // Second call
+ const schemas2 = await service.loadToolSchemas();
+
+ expect(schemas1).toEqual(schemas2);
+ // Should only invoke Tauri once due to caching (TTL = 5 minutes)
+ expect(mockInvoke).toHaveBeenCalledTimes(1);
+ });
+
+ test('should force reload when requested', async () => {
+ const mockSchemas = [
+ {
+ type: "function",
+ function: {
+ name: "test_tool",
+ description: "Test tool",
+ parameters: { type: "object", properties: {} }
+ }
+ }
+ ];
+
+ mockInvoke.mockResolvedValue(mockSchemas);
+
+ // First call
+ await service.loadToolSchemas();
+ // Force reload
+ await service.loadToolSchemas(true);
+
+ // Should invoke Tauri twice
+ expect(mockInvoke).toHaveBeenCalledTimes(2);
+ });
+
+ test('should handle empty tool schema response', async () => {
+ mockInvoke.mockResolvedValue([]);
+
+ const schemas = await service.loadToolSchemas();
+
+ expect(schemas).toHaveLength(0);
+ expect(mockInvoke).toHaveBeenCalledWith('runtime_get_tool_schemas');
+ });
+
+ test('should throw error when Tauri command fails', async () => {
+ const errorMessage = 'Failed to load tool schemas';
+ mockInvoke.mockRejectedValue(new Error(errorMessage));
+
+ await expect(service.loadToolSchemas()).rejects.toThrow(`Failed to load tool schemas: Error: ${errorMessage}`);
+ });
+ });
+
+ describe('executeTool', () => {
+ test('should execute tool using ZeroClaw format with success', async () => {
+ const mockResult = {
+ success: true,
+ output: '{"issues": [{"title": "Bug fix", "number": 1}]}',
+ error: null,
+ execution_time: 1500
+ };
+
+ mockInvoke.mockResolvedValue(mockResult);
+
+ const result = await service.executeTool(
+ 'github',
+ 'list_issues',
+ '{"owner":"user","repo":"test"}'
+ );
+
+ expect(result.status).toBe('success');
+ expect(result.result).toBe(mockResult.output);
+ expect(result.executionTimeMs).toBe(1500);
+ expect(result.toolName).toBe('list_issues');
+ expect(result.skillId).toBe('github');
+
+ // Verify correct tool_id format and arguments
+ expect(mockInvoke).toHaveBeenCalledWith('runtime_execute_tool', {
+ toolId: 'github_list_issues',
+ arguments: '{"owner":"user","repo":"test"}'
+ });
+ });
+
+ test('should handle tool execution failure', async () => {
+ const mockResult = {
+ success: false,
+ output: '',
+ error: 'Tool not found: invalid_tool',
+ execution_time: 100
+ };
+
+ mockInvoke.mockResolvedValue(mockResult);
+
+ const result = await service.executeTool(
+ 'invalid',
+ 'tool',
+ '{}'
+ );
+
+ expect(result.status).toBe('error');
+ expect(result.errorMessage).toBe('Tool not found: invalid_tool');
+ expect(result.result).toBe('Tool not found: invalid_tool');
+ expect(result.executionTimeMs).toBe(100);
+ });
+
+ test('should handle tool execution without execution_time', async () => {
+ const mockResult = {
+ success: true,
+ output: 'Success',
+ error: null
+ // No execution_time provided
+ };
+
+ mockInvoke.mockResolvedValue(mockResult);
+
+ const startTime = Date.now();
+ const result = await service.executeTool('test', 'tool', '{}');
+ const endTime = Date.now();
+
+ expect(result.status).toBe('success');
+ expect(result.executionTimeMs).toBeGreaterThan(0);
+ expect(result.executionTimeMs).toBeLessThanOrEqual(endTime - startTime + 10); // Allow small margin
+ });
+
+ test('should handle Tauri invoke exception', async () => {
+ const errorMessage = 'Network error';
+ mockInvoke.mockRejectedValue(new Error(errorMessage));
+
+ const result = await service.executeTool('test', 'tool', '{}');
+
+ expect(result.status).toBe('error');
+ expect(result.errorMessage).toBe(errorMessage);
+ expect(result.result).toBe(errorMessage);
+ expect(result.executionTimeMs).toBeGreaterThan(0);
+ });
+
+ test('should generate unique execution IDs', async () => {
+ const mockResult = {
+ success: true,
+ output: 'test',
+ error: null,
+ execution_time: 100
+ };
+
+ mockInvoke.mockResolvedValue(mockResult);
+
+ const result1 = await service.executeTool('test', 'tool1', '{}');
+ const result2 = await service.executeTool('test', 'tool2', '{}');
+
+ expect(result1.id).not.toBe(result2.id);
+ expect(result1.id).toMatch(/^exec_\d+_[a-z0-9]+$/);
+ expect(result2.id).toMatch(/^exec_\d+_[a-z0-9]+$/);
+ });
+ });
+
+ describe('tool management methods', () => {
+ beforeEach(async () => {
+ const mockSchemas = [
+ {
+ type: "function",
+ function: {
+ name: "github_list_issues",
+ description: "List GitHub issues",
+ parameters: { type: "object", properties: {} }
+ }
+ },
+ {
+ type: "function",
+ function: {
+ name: "github_create_issue",
+ description: "Create GitHub issue",
+ parameters: { type: "object", properties: {} }
+ }
+ },
+ {
+ type: "function",
+ function: {
+ name: "notion_create_page",
+ description: "Create Notion page",
+ parameters: { type: "object", properties: {} }
+ }
+ }
+ ];
+
+ mockInvoke.mockResolvedValue(mockSchemas);
+ await service.loadToolSchemas();
+ });
+
+ test('getToolByName should find tool by name', () => {
+ const tool = service.getToolByName('github_list_issues');
+
+ expect(tool).toBeDefined();
+ expect(tool?.function.name).toBe('github_list_issues');
+ expect(tool?.function.description).toBe('List GitHub issues');
+ });
+
+ test('getToolByName should return undefined for non-existent tool', () => {
+ const tool = service.getToolByName('non_existent_tool');
+
+ expect(tool).toBeUndefined();
+ });
+
+ test('getAllTools should return all loaded tools', () => {
+ const tools = service.getAllTools();
+
+ expect(tools).toHaveLength(3);
+ expect(tools.map(t => t.function.name)).toEqual([
+ 'github_list_issues',
+ 'github_create_issue',
+ 'notion_create_page'
+ ]);
+ });
+
+ test('getToolsBySkill should organize tools by skill ID', () => {
+ const toolsBySkill = service.getToolsBySkill();
+
+ expect(toolsBySkill).toHaveProperty('github');
+ expect(toolsBySkill).toHaveProperty('notion');
+ expect(toolsBySkill.github).toHaveLength(2);
+ expect(toolsBySkill.notion).toHaveLength(1);
+
+ expect(toolsBySkill.github.map(t => t.function.name)).toEqual([
+ 'github_list_issues',
+ 'github_create_issue'
+ ]);
+ expect(toolsBySkill.notion[0].function.name).toBe('notion_create_page');
+ });
+
+ test('getToolStats should return accurate statistics', () => {
+ const stats = service.getToolStats();
+
+ expect(stats.totalTools).toBe(3);
+ expect(stats.skillCount).toBe(2);
+ expect(stats.categories).toHaveProperty('GitHub', 2);
+ expect(stats.categories).toHaveProperty('Notion', 1);
+ });
+ });
+
+ describe('helper methods', () => {
+ test('extractSkillIdFromToolName should parse skill ID correctly', () => {
+ // Use reflection to access private method
+ const extractMethod = (service as any).extractSkillIdFromToolName.bind(service);
+
+ expect(extractMethod('github_list_issues')).toBe('github');
+ expect(extractMethod('notion_create_page')).toBe('notion');
+ expect(extractMethod('complex_skill_name_tool_name')).toBe('complex_skill_name_tool');
+ expect(extractMethod('invalid_format')).toBe('invalid');
+ expect(extractMethod('no_underscore')).toBeNull();
+ });
+
+ test('extractCategoryFromSkillId should categorize skills correctly', () => {
+ // Use reflection to access private method
+ const extractMethod = (service as any).extractCategoryFromSkillId.bind(service);
+
+ expect(extractMethod('github')).toBe('GitHub');
+ expect(extractMethod('github_enterprise')).toBe('GitHub');
+ expect(extractMethod('notion')).toBe('Notion');
+ expect(extractMethod('telegram')).toBe('Telegram');
+ expect(extractMethod('gmail')).toBe('Email');
+ expect(extractMethod('calendar')).toBe('Calendar');
+ expect(extractMethod('slack')).toBe('Slack');
+ expect(extractMethod('crypto_wallet')).toBe('Crypto');
+ expect(extractMethod('unknown_skill')).toBe('Other');
+ });
+ });
+
+ describe('clearCache', () => {
+ test('should clear cached tool schemas', async () => {
+ const mockSchemas = [
+ {
+ type: "function",
+ function: {
+ name: "test_tool",
+ description: "Test tool",
+ parameters: { type: "object", properties: {} }
+ }
+ }
+ ];
+
+ mockInvoke.mockResolvedValue(mockSchemas);
+
+ // Load schemas
+ await service.loadToolSchemas();
+ expect(mockInvoke).toHaveBeenCalledTimes(1);
+
+ // Clear cache
+ service.clearCache();
+
+ // Load again - should call Tauri again
+ await service.loadToolSchemas();
+ expect(mockInvoke).toHaveBeenCalledTimes(2);
+ });
+ });
+});
\ No newline at end of file
diff --git a/src/services/agentLoop.ts b/src/services/agentLoop.ts
new file mode 100644
index 000000000..231f344a8
--- /dev/null
+++ b/src/services/agentLoop.ts
@@ -0,0 +1,388 @@
+/**
+ * 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/agentToolRegistry.ts b/src/services/agentToolRegistry.ts
new file mode 100644
index 000000000..2de9d47bb
--- /dev/null
+++ b/src/services/agentToolRegistry.ts
@@ -0,0 +1,256 @@
+/**
+ * Agent Tool Registry Service
+ *
+ * Builds on top of the existing skill system to provide agent-compatible
+ * tool discovery and execution. Uses ZeroClaw format compatibility commands:
+ * - runtime_get_tool_schemas: Get all tools in OpenAI-compatible format
+ * - runtime_execute_tool: Execute a tool with enhanced validation and timing
+ */
+
+import { invoke } from '@tauri-apps/api/core';
+import type {
+ AgentToolSchema,
+ AgentToolExecution,
+ IAgentToolRegistry
+} from '../types/agent';
+
+// ZeroClaw format types from Rust
+interface ZeroClawToolSchema {
+ type: string;
+ function: {
+ name: string;
+ description: string;
+ parameters: any;
+ };
+}
+
+interface ZeroClawToolResult {
+ success: boolean;
+ output: string;
+ error?: string;
+ execution_time?: number;
+}
+
+export class AgentToolRegistry implements IAgentToolRegistry {
+ private static instance: AgentToolRegistry;
+ private toolSchemas: AgentToolSchema[] = [];
+ private lastLoadTime = 0;
+ private readonly CACHE_TTL = 5 * 60 * 1000; // 5 minutes
+
+ static getInstance(): AgentToolRegistry {
+ if (!this.instance) {
+ this.instance = new AgentToolRegistry();
+ }
+ return this.instance;
+ }
+
+ /**
+ * Load tool schemas from the skill system using ZeroClaw format
+ */
+ async loadToolSchemas(forceReload = false): Promise {
+ const now = Date.now();
+
+ // Return cached tools if still fresh
+ if (!forceReload && this.toolSchemas.length > 0 && (now - this.lastLoadTime) < this.CACHE_TTL) {
+ return this.toolSchemas;
+ }
+
+ try {
+ console.log('🔧 Loading tool schemas from skill system (ZeroClaw format)...');
+
+ // Call ZeroClaw format command to get tools in OpenAI-compatible format
+ const zeroClawTools = await invoke('runtime_get_tool_schemas');
+
+ console.log(`🔧 Loaded ${zeroClawTools.length} tools in ZeroClaw format`);
+
+ // Tools are already in OpenAI format, just map to our interface
+ this.toolSchemas = zeroClawTools.map(tool => ({
+ type: tool.type,
+ function: {
+ name: tool.function.name,
+ description: tool.function.description,
+ parameters: tool.function.parameters
+ }
+ }));
+
+ this.lastLoadTime = now;
+
+ console.log(`✅ Tool registry updated: ${this.toolSchemas.length} tools available`);
+
+ return this.toolSchemas;
+ } catch (error) {
+ console.error('❌ Failed to load tool schemas:', error);
+ throw new Error(`Failed to load tool schemas: ${error}`);
+ }
+ }
+
+ /**
+ * Execute a tool using ZeroClaw format with enhanced validation
+ */
+ async executeTool(
+ skillId: string,
+ toolName: string,
+ toolArguments: string
+ ): Promise {
+ const startTime = Date.now();
+ const executionId = `exec_${startTime}_${Math.random().toString(36).substr(2, 9)}`;
+
+ // Create tool ID in format expected by runtime_execute_tool
+ const toolId = `${skillId}_${toolName}`;
+
+ console.log(`🚀 Executing tool: ${toolId}`);
+ console.log(`📝 Arguments:`, toolArguments);
+
+ const execution: AgentToolExecution = {
+ id: executionId,
+ toolName,
+ skillId,
+ arguments: toolArguments,
+ status: 'running',
+ startTime
+ };
+
+ try {
+ // Call ZeroClaw format command with enhanced validation and timing
+ const result = await invoke('runtime_execute_tool', {
+ toolId,
+ arguments: toolArguments
+ });
+
+ execution.endTime = Date.now();
+ // Use execution time from Rust if available, otherwise calculate locally
+ execution.executionTimeMs = result.execution_time || (execution.endTime - execution.startTime);
+
+ if (!result.success) {
+ execution.status = 'error';
+ execution.errorMessage = result.error || 'Unknown error occurred';
+ execution.result = execution.errorMessage;
+
+ console.log(`❌ Tool execution failed: ${toolName} (${execution.executionTimeMs}ms)`);
+ console.log(`❌ Error:`, execution.errorMessage);
+ } else {
+ execution.status = 'success';
+ execution.result = result.output;
+
+ console.log(`✅ Tool execution completed: ${toolName} (${execution.executionTimeMs}ms)`);
+ }
+
+ return execution;
+
+ } catch (error) {
+ execution.endTime = Date.now();
+ execution.executionTimeMs = execution.endTime - execution.startTime;
+ execution.status = 'error';
+ execution.errorMessage = error instanceof Error ? error.message : String(error);
+ execution.result = execution.errorMessage;
+
+ console.error(`❌ Tool execution error: ${toolName}`, error);
+
+ return execution;
+ }
+ }
+
+ /**
+ * Get a specific tool by name
+ */
+ getToolByName(toolName: string): AgentToolSchema | undefined {
+ return this.toolSchemas.find(tool => tool.function.name === toolName);
+ }
+
+ /**
+ * Get all available tools
+ */
+ getAllTools(): AgentToolSchema[] {
+ return [...this.toolSchemas];
+ }
+
+ /**
+ * Get tools organized by skill
+ */
+ getToolsBySkill(): Record {
+ const toolsBySkill: Record = {};
+
+ for (const tool of this.toolSchemas) {
+ // Extract skill ID from tool name (format: skillId_toolName)
+ const skillId = this.extractSkillIdFromToolName(tool.function.name) || 'unknown';
+
+ if (!toolsBySkill[skillId]) {
+ toolsBySkill[skillId] = [];
+ }
+ toolsBySkill[skillId].push(tool);
+ }
+
+ return toolsBySkill;
+ }
+
+ /**
+ * Get tool execution statistics
+ */
+ getToolStats(): {
+ totalTools: number;
+ skillCount: number;
+ categories: Record;
+ } {
+ const categories: Record = {};
+ const skills = new Set();
+
+ for (const tool of this.toolSchemas) {
+ const skillId = this.extractSkillIdFromToolName(tool.function.name) || 'unknown';
+ skills.add(skillId);
+
+ // Categorize by skill name
+ const category = this.extractCategoryFromSkillId(skillId);
+ categories[category] = (categories[category] || 0) + 1;
+ }
+
+ return {
+ totalTools: this.toolSchemas.length,
+ skillCount: skills.size,
+ categories
+ };
+ }
+
+ /**
+ * Clear the tool registry cache
+ */
+ clearCache(): void {
+ this.toolSchemas = [];
+ this.lastLoadTime = 0;
+ console.log('🔧 Tool registry cache cleared');
+ }
+
+ // =============================================================================
+ // Private Helper Methods
+ // =============================================================================
+
+ /**
+ * Extract skill ID from tool name (format: skillId_toolName)
+ */
+ private extractSkillIdFromToolName(toolName: string): string | null {
+ const underscoreIndex = toolName.lastIndexOf('_');
+ if (underscoreIndex === -1) {
+ return null;
+ }
+ return toolName.substring(0, underscoreIndex);
+ }
+
+ /**
+ * Extract category name from skill ID for organization
+ */
+ private extractCategoryFromSkillId(skillId: string): string {
+ // Common skill naming patterns
+ if (skillId.includes('github') || skillId.includes('git')) return 'GitHub';
+ if (skillId.includes('notion')) return 'Notion';
+ if (skillId.includes('telegram') || skillId.includes('tg')) return 'Telegram';
+ if (skillId.includes('email') || skillId.includes('gmail')) return 'Email';
+ if (skillId.includes('calendar')) return 'Calendar';
+ if (skillId.includes('slack')) return 'Slack';
+ if (skillId.includes('discord')) return 'Discord';
+ if (skillId.includes('twitter') || skillId.includes('x')) return 'Social';
+ if (skillId.includes('file') || skillId.includes('fs')) return 'File System';
+ if (skillId.includes('crypto') || skillId.includes('blockchain')) return 'Crypto';
+ if (skillId.includes('ai') || skillId.includes('ml')) return 'AI/ML';
+
+ return 'Other';
+ }
+}
\ No newline at end of file
diff --git a/src/store/__tests__/agentSlice.test.ts b/src/store/__tests__/agentSlice.test.ts
new file mode 100644
index 000000000..0d4197422
--- /dev/null
+++ b/src/store/__tests__/agentSlice.test.ts
@@ -0,0 +1,496 @@
+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
new file mode 100644
index 000000000..775faa5e8
--- /dev/null
+++ b/src/store/agentSlice.ts
@@ -0,0 +1,433 @@
+/**
+ * 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 3d642173a..3d203447c 100644
--- a/src/store/index.ts
+++ b/src/store/index.ts
@@ -15,6 +15,7 @@ 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';
@@ -53,10 +54,18 @@ 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
@@ -102,6 +111,7 @@ 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 d736e62f9..425bd0b7c 100644
--- a/src/store/threadSlice.ts
+++ b/src/store/threadSlice.ts
@@ -4,6 +4,8 @@ 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 {
// Existing local data (will be persisted)
@@ -122,13 +124,39 @@ export const sendMessage = createAsyncThunk(
// Continue with original message
}
- // 3. Send to API with processed message (disable injection in threadApi to avoid double injection)
- const data = await threadApi.sendMessage(processedMessage, threadId, { injectSoul: false });
+ // 3. Check if agent mode is enabled for this thread
+ const state = getState() as RootState;
+ const agentMode = selectAgentModeForThread(state, threadId);
- // 3. For now, we'll handle AI response via the existing inference API
- // The AI response will be added separately via addInferenceResponse
+ if (agentMode) {
+ // Execute agent task instead of sending to inference API
+ console.log('🤖 Agent mode enabled - executing agent task');
- return data;
+ 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;
+ }
} catch (error) {
// Remove optimistic user message on failure
const state = (getState() as { thread: ThreadState }).thread;
@@ -200,12 +228,17 @@ const threadSlice = createSlice({
createdAt: new Date().toISOString(),
});
},
- addInferenceResponse: (state, action: { payload: { content: string } }) => {
+ addInferenceResponse: (state, action: { payload: { content: string; agentExecutionId?: string; toolExecutions?: any[] } }) => {
const aiMessage: ThreadMessage = {
id: `inference-${Date.now()}`,
content: action.payload.content,
type: 'text',
- extraMetadata: {},
+ extraMetadata: {
+ ...(action.payload.agentExecutionId && {
+ agentExecutionId: action.payload.agentExecutionId,
+ toolExecutions: action.payload.toolExecutions || []
+ })
+ },
sender: 'agent',
createdAt: new Date().toISOString(),
};
diff --git a/src/types/agent.ts b/src/types/agent.ts
new file mode 100644
index 000000000..af3848355
--- /dev/null
+++ b/src/types/agent.ts
@@ -0,0 +1,379 @@
+/**
+ * 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
From cae9f7335c686843cbb4c86a98d2174ca98acdb6 Mon Sep 17 00:00:00 2001
From: cyrus
Date: Tue, 10 Mar 2026 01:12:31 +0530
Subject: [PATCH 2/3] 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.
---
src/components/agent/AgentExecutionPanel.tsx | 236 ---------
src/components/agent/AgentStatusIndicator.tsx | 96 ----
src/components/agent/AgentToggle.tsx | 162 ------
src/components/agent/index.ts | 13 -
src/pages/Conversations.tsx | 124 ++++-
src/services/agentLoop.ts | 388 --------------
src/services/api/inferenceApi.ts | 28 +-
src/store/__tests__/agentSlice.test.ts | 496 ------------------
src/store/agentSlice.ts | 433 ---------------
src/store/index.ts | 9 -
src/store/threadSlice.ts | 46 +-
src/types/agent.ts | 379 -------------
12 files changed, 132 insertions(+), 2278 deletions(-)
delete mode 100644 src/components/agent/AgentExecutionPanel.tsx
delete mode 100644 src/components/agent/AgentStatusIndicator.tsx
delete mode 100644 src/components/agent/AgentToggle.tsx
delete mode 100644 src/components/agent/index.ts
delete mode 100644 src/services/agentLoop.ts
delete mode 100644 src/store/__tests__/agentSlice.test.ts
delete mode 100644 src/store/agentSlice.ts
delete mode 100644 src/types/agent.ts
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 (
-
- );
- }
-
- 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 */}
-
-
- {/* 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 */}
-
-
-
- {/* Loading indicator */}
- {isLoading && (
-
- )}
-
-
- {/* 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
From e0af9e923d3abdab0f352a60e49ed06ecd6b50d0 Mon Sep 17 00:00:00 2001
From: cyrus
Date: Tue, 10 Mar 2026 02:00:58 +0530
Subject: [PATCH 3/3] log: enhance runtime logging and tool filtering for
debugging
- Added detailed logging for tool schema generation, tool execution, and runtime state checks to improve traceability.
- Enhanced Gmail/Notion tool filtering with additional
---
src-tauri/src/commands/runtime.rs | 49 ++++++++++++++++---
src/pages/Conversations.tsx | 75 ++++++++++++++++++++++++++---
src/services/agentToolRegistry.ts | 27 +++++++++--
src/services/daemonHealthService.ts | 6 +--
4 files changed, 138 insertions(+), 19 deletions(-)
diff --git a/src-tauri/src/commands/runtime.rs b/src-tauri/src/commands/runtime.rs
index e305d8140..b3d4cdf24 100644
--- a/src-tauri/src/commands/runtime.rs
+++ b/src-tauri/src/commands/runtime.rs
@@ -296,9 +296,11 @@ mod desktop {
pub async fn runtime_get_tool_schemas(
engine: State<'_, Arc>,
) -> Result, String> {
- log::info!("Generating ZeroClaw-compatible tool schemas");
+ log::info!("🔧 [RUNTIME] Generating ZeroClaw-compatible tool schemas");
let tools = engine.all_tools();
+ log::info!("🔧 [RUNTIME] Found {} tools from engine", tools.len());
+
let mut schemas = Vec::new();
for (skill_id, tool) in tools {
@@ -309,13 +311,16 @@ mod desktop {
tool.description.clone()
};
+ let tool_name = format!("{}_{}", skill_id, tool.name);
+ log::info!("🔧 [RUNTIME] Processing tool: {}", tool_name);
+
// Convert input schema to OpenAI-compatible format
let openai_parameters = convert_to_openai_schema(tool.input_schema)?;
let schema = ZeroClawToolSchema {
type_field: "function".to_string(),
function: ZeroClawFunction {
- name: format!("{}_{}", skill_id, tool.name),
+ name: tool_name,
description,
parameters: openai_parameters,
},
@@ -324,7 +329,15 @@ mod desktop {
schemas.push(schema);
}
- log::info!("Generated {} ZeroClaw tool schemas", schemas.len());
+ log::info!("🔧 [RUNTIME] Generated {} ZeroClaw tool schemas", schemas.len());
+
+ // Log tools that contain 'notion' or 'gmail' for debugging
+ let gmail_notion_tools: Vec = schemas.iter()
+ .map(|s| &s.function.name)
+ .filter(|name| name.to_lowercase().contains("gmail") || name.to_lowercase().contains("notion"))
+ .cloned()
+ .collect();
+ log::info!("🔧 [RUNTIME] Gmail/Notion tools found: {:?}", gmail_notion_tools);
Ok(schemas)
}
@@ -338,12 +351,16 @@ mod desktop {
) -> Result {
let start_time = std::time::Instant::now();
- log::info!("Executing ZeroClaw tool: {} with args: {}", tool_id, args);
+ log::info!("🔧 [RUNTIME] Executing ZeroClaw tool: {} with args: {}", tool_id, args);
// Parse tool_id to get skill_id and tool_name (format: "skill_id_tool_name")
let (skill_id, tool_name) = match parse_tool_id(&tool_id) {
- Ok((skill, tool)) => (skill, tool),
+ Ok((skill, tool)) => {
+ log::info!("🔧 [RUNTIME] Parsed tool_id: skill_id='{}', tool_name='{}'", skill, tool);
+ (skill, tool)
+ }
Err(e) => {
+ log::error!("🔧 [RUNTIME] Failed to parse tool_id '{}': {}", tool_id, e);
let execution_time = start_time.elapsed().as_millis() as u64;
return Ok(ZeroClawToolResult {
success: false,
@@ -354,10 +371,28 @@ mod desktop {
}
};
+ // Log runtime state before execution
+ log::info!("🔧 [RUNTIME] Attempting to call tool '{}' on skill '{}'", tool_name, skill_id);
+
+ // Get available skills for debugging
+ let skills = engine.list_skills();
+ log::info!("🔧 [RUNTIME] Available skills: {:?}", skills.iter().map(|s| &s.skill_id).collect::>());
+
+ // Check if the specific skill exists
+ if let Some(skill) = skills.iter().find(|s| s.skill_id == skill_id) {
+ log::info!("🔧 [RUNTIME] Found skill '{}' with state: {:?}, tools: {:?}",
+ skill_id, skill.state, skill.tools.iter().map(|t| &t.name).collect::>());
+ } else {
+ log::error!("🔧 [RUNTIME] Skill '{}' not found in runtime!", skill_id);
+ }
+
// Execute the tool using the existing command
+ log::info!("🔧 [RUNTIME] Calling engine.call_tool('{}', '{}', {})", skill_id, tool_name, args);
+
match engine.call_tool(&skill_id, &tool_name, args).await {
Ok(result) => {
let execution_time = start_time.elapsed().as_millis() as u64;
+ log::info!("🔧 [RUNTIME] Tool execution completed in {}ms, is_error: {}", execution_time, result.is_error);
if result.is_error {
let error_message = result.content
@@ -370,6 +405,8 @@ mod desktop {
.collect::>()
.join("\n");
+ log::error!("🔧 [RUNTIME] Tool execution failed with error: {}", error_message);
+
Ok(ZeroClawToolResult {
success: false,
output: String::new(),
@@ -400,7 +437,7 @@ mod desktop {
}
Err(e) => {
let execution_time = start_time.elapsed().as_millis() as u64;
- log::error!("ZeroClaw tool execution failed: {}", e);
+ log::error!("🔧 [RUNTIME] Engine call_tool failed: {}", e);
Ok(ZeroClawToolResult {
success: false,
diff --git a/src/pages/Conversations.tsx b/src/pages/Conversations.tsx
index d9f4eb2d6..4f20deb4b 100644
--- a/src/pages/Conversations.tsx
+++ b/src/pages/Conversations.tsx
@@ -284,7 +284,7 @@ const Conversations = () => {
try {
const toolSchemas = await toolRegistry.loadToolSchemas();
- availableTools = toolSchemas.map(schema => ({
+ const allTools = toolSchemas.map(schema => ({
type: 'function' as const,
function: {
name: schema.function.name,
@@ -292,7 +292,21 @@ const Conversations = () => {
parameters: schema.function.parameters
}
}));
- console.log(`🔧 Loaded ${availableTools.length} tools for transparent execution`);
+
+ // Filter to only Gmail and Notion tools
+ console.log(`🔧 All tools available:`, allTools.map(t => t.function.name));
+
+ availableTools = allTools.filter(tool => {
+ const toolName = tool.function.name.toLowerCase();
+ const isMatch = toolName.includes('gmail') || toolName.includes('notion');
+ if (isMatch) {
+ console.log(`✅ Including tool: ${tool.function.name}`);
+ }
+ return isMatch;
+ });
+
+ console.log(`🔧 Loaded ${availableTools.length} Gmail and Notion tools (from ${allTools.length} total) for transparent execution`);
+ console.log(`🔧 Filtered tools:`, availableTools.map(t => t.function.name));
} catch (error) {
console.warn('⚠️ Failed to load tools, continuing without tool support:', error);
}
@@ -315,13 +329,37 @@ const Conversations = () => {
iterations++;
console.log(`🔄 Tool calling iteration ${iterations}`);
- const response = await inferenceApi.createChatCompletion({
+ // Log the full request being sent to inference API
+ const requestPayload = {
model: selectedModel,
messages: currentMessages,
tools: availableTools.length > 0 ? availableTools : undefined,
tool_choice: availableTools.length > 0 ? 'auto' : undefined,
+ };
+
+ console.log(`📤 [INFERENCE REQUEST] Iteration ${iterations}:`, {
+ model: requestPayload.model,
+ messagesCount: requestPayload.messages.length,
+ toolsCount: requestPayload.tools?.length || 0,
+ toolChoice: requestPayload.tool_choice,
+ lastMessage: requestPayload.messages[requestPayload.messages.length - 1],
});
+ console.log(`📋 [INFERENCE REQUEST FULL]`, JSON.stringify(requestPayload, null, 2));
+
+ const response = await inferenceApi.createChatCompletion(requestPayload);
+
+ console.log(`📥 [INFERENCE RESPONSE] Iteration ${iterations}:`, {
+ id: response.id,
+ model: response.model,
+ choicesCount: response.choices.length,
+ usage: response.usage,
+ assistantMessage: response.choices[0]?.message,
+ toolCallsCount: response.choices[0]?.message?.tool_calls?.length || 0,
+ });
+
+ console.log(`📋 [INFERENCE RESPONSE FULL]`, JSON.stringify(response, null, 2));
+
const assistantMessage = response.choices[0]?.message;
if (!assistantMessage) {
throw new Error('No assistant message in response');
@@ -351,10 +389,25 @@ const Conversations = () => {
const underscoreIndex = toolName.lastIndexOf('_');
const skillId = underscoreIndex > -1 ? toolName.substring(0, underscoreIndex) : 'unknown';
- console.log(`⚡ Executing tool: ${toolName} with args: ${toolArgs}`);
+ console.log(`⚡ [TOOL CALL] Processing tool call:`, {
+ id: toolCall.id,
+ name: toolName,
+ skillId,
+ arguments: toolArgs,
+ argumentsType: typeof toolArgs,
+ argumentsLength: toolArgs?.length
+ });
const execution = await toolRegistry.executeTool(skillId, toolName, toolArgs);
+ console.log(`⚡ [TOOL RESULT] Tool execution completed:`, {
+ toolName,
+ status: execution.status,
+ executionTimeMs: execution.executionTimeMs,
+ resultLength: execution.result?.length,
+ hasError: !!execution.errorMessage
+ });
+
// Add tool result to conversation
currentMessages.push({
role: 'tool',
@@ -364,12 +417,22 @@ const Conversations = () => {
console.log(`✅ Tool ${toolName} completed: ${execution.status}`);
} catch (error) {
- console.error(`❌ Tool execution failed for ${toolCall.function.name}:`, error);
+ console.error(`❌ [TOOL ERROR] Tool execution failed for ${toolCall.function.name}:`, {
+ error: error,
+ errorMessage: error instanceof Error ? error.message : String(error),
+ errorStack: error instanceof Error ? error.stack : undefined,
+ toolCall: {
+ id: toolCall.id,
+ name: toolCall.function.name,
+ arguments: toolCall.function.arguments
+ }
+ });
// Add error result to conversation
+ const errorMessage = error instanceof Error ? error.message : String(error);
currentMessages.push({
role: 'tool',
- content: `Tool execution failed: ${error}`,
+ content: `Tool execution failed: ${errorMessage}`,
tool_call_id: toolCall.id,
});
}
diff --git a/src/services/agentToolRegistry.ts b/src/services/agentToolRegistry.ts
index 2de9d47bb..edd803c1e 100644
--- a/src/services/agentToolRegistry.ts
+++ b/src/services/agentToolRegistry.ts
@@ -98,8 +98,20 @@ export class AgentToolRegistry implements IAgentToolRegistry {
// Create tool ID in format expected by runtime_execute_tool
const toolId = `${skillId}_${toolName}`;
- console.log(`🚀 Executing tool: ${toolId}`);
- console.log(`📝 Arguments:`, toolArguments);
+ console.log(`🚀 [TOOL EXECUTION START] Executing tool: ${toolId}`);
+ console.log(`📝 [ARGUMENTS] Raw arguments:`, {
+ arguments: toolArguments,
+ type: typeof toolArguments,
+ length: toolArguments?.length,
+ isString: typeof toolArguments === 'string',
+ parsed: (() => {
+ try {
+ return typeof toolArguments === 'string' ? JSON.parse(toolArguments) : toolArguments;
+ } catch (e) {
+ return 'Failed to parse: ' + e;
+ }
+ })()
+ });
const execution: AgentToolExecution = {
id: executionId,
@@ -112,11 +124,18 @@ export class AgentToolRegistry implements IAgentToolRegistry {
try {
// Call ZeroClaw format command with enhanced validation and timing
+ console.log(`🔧 [BEFORE INVOKE] Calling runtime_execute_tool with:`);
+ console.log(` toolId: "${toolId}"`);
+ console.log(` args: ${toolArguments}`);
+ console.log(` args type: ${typeof toolArguments}`);
+
const result = await invoke('runtime_execute_tool', {
- toolId,
- arguments: toolArguments
+ toolId: toolId, // Use camelCase as expected by current Rust version
+ args: toolArguments // Use "args" instead of "arguments"
});
+ console.log(`🔧 [AFTER INVOKE] Tool execution result:`, result);
+
execution.endTime = Date.now();
// Use execution time from Rust if available, otherwise calculate locally
execution.executionTimeMs = result.execution_time || (execution.endTime - execution.startTime);
diff --git a/src/services/daemonHealthService.ts b/src/services/daemonHealthService.ts
index e619c0d26..b83fff08d 100644
--- a/src/services/daemonHealthService.ts
+++ b/src/services/daemonHealthService.ts
@@ -31,7 +31,7 @@ export class DaemonHealthService {
console.log('[DaemonHealth] Setting up alphahuman:health event listener');
this.healthEventListener = await listen('alphahuman:health', event => {
- console.log('[DaemonHealth] Received health event:', event.payload);
+ // console.log('[DaemonHealth] Received health event:', event.payload);
const healthSnapshot = this.parseHealthSnapshot(event.payload);
if (healthSnapshot) {
@@ -147,7 +147,7 @@ export class DaemonHealthService {
// Update the health snapshot in Redux
store.dispatch(updateHealthSnapshot({ userId, healthSnapshot: snapshot }));
- console.log('[DaemonHealth] Updated health snapshot for user:', userId, snapshot);
+ // console.log('[DaemonHealth] Updated health snapshot for user:', userId, snapshot);
} catch (error) {
console.error('[DaemonHealth] Error updating Redux from health:', error);
}
@@ -204,4 +204,4 @@ export class DaemonHealthService {
}
// Export singleton instance
-export const daemonHealthService = new DaemonHealthService();
\ No newline at end of file
+export const daemonHealthService = new DaemonHealthService();