agent-client.ts•7.58 kB
/**
* ByteBot Agent API HTTP Client
*
* Provides HTTP client for interacting with ByteBot Agent API (task management)
* Includes error handling, retry logic, and request/response validation
*/
import axios, { AxiosInstance } from 'axios';
import {
Task,
CreateTaskRequest,
CreateTaskResponse,
UpdateTaskRequest,
ListTasksQuery,
} from '../types/bytebot.js';
import { EnvironmentConfig, TaskCacheEntry } from '../types/mcp.js';
import {
handleAxiosError,
withRetry,
logError,
} from '../utils/error-handler.js';
import {
validateTaskId,
CreateTaskSchema,
UpdateTaskSchema,
ListTasksSchema,
} from '../utils/validators.js';
/**
* Agent API Client
*/
export class AgentClient {
private client: AxiosInstance;
private config: EnvironmentConfig;
private taskCache: Map<string, TaskCacheEntry> = new Map();
private readonly CACHE_TTL = 5000; // 5 seconds
constructor(config: EnvironmentConfig) {
this.config = config;
this.client = axios.create({
baseURL: config.agentUrl,
timeout: config.requestTimeout,
headers: {
'Content-Type': 'application/json',
},
});
// Log client initialization
console.log(`[ByteBot MCP] Agent API client initialized: ${config.agentUrl}`);
}
/**
* Create a new task
*/
async createTask(request: CreateTaskRequest): Promise<CreateTaskResponse> {
try {
// Validate request
const validatedRequest = CreateTaskSchema.parse(request);
// Make request with retry logic
const response = await withRetry(
async () => {
return await this.client.post<CreateTaskResponse>(
'/tasks',
validatedRequest
);
},
{
maxRetries: this.config.maxRetries,
delay: this.config.retryDelay,
backoffMultiplier: 2,
maxDelay: 10000,
}
);
console.log(
`[ByteBot MCP] Task created successfully: ${response.data.id}`
);
return response.data;
} catch (error) {
logError('createTask', error);
throw handleAxiosError(error);
}
}
/**
* List all tasks with optional filters
*/
async listTasks(query?: ListTasksQuery): Promise<Task[]> {
try {
// Validate query parameters
if (query) {
ListTasksSchema.parse(query);
}
const response = await withRetry(
async () => {
return await this.client.get<Task[]>('/tasks', {
params: query,
});
},
{
maxRetries: this.config.maxRetries,
delay: this.config.retryDelay,
backoffMultiplier: 2,
maxDelay: 10000,
}
);
console.log(`[ByteBot MCP] Retrieved ${response.data.length} tasks`);
return response.data;
} catch (error) {
logError('listTasks', error);
throw handleAxiosError(error);
}
}
/**
* Get a specific task by ID
*/
async getTask(taskId: string, useCache = true): Promise<Task> {
try {
validateTaskId(taskId);
// Check cache first
if (useCache) {
const cached = this.getCachedTask(taskId);
if (cached) {
console.log(`[ByteBot MCP] Returning cached task: ${taskId}`);
return cached;
}
}
const response = await withRetry(
async () => {
return await this.client.get<Task>(`/tasks/${taskId}`);
},
{
maxRetries: this.config.maxRetries,
delay: this.config.retryDelay,
backoffMultiplier: 2,
maxDelay: 10000,
}
);
// Cache the task
this.cacheTask(taskId, response.data);
console.log(
`[ByteBot MCP] Retrieved task ${taskId}: status=${response.data.status}`
);
return response.data;
} catch (error) {
logError('getTask', error);
throw handleAxiosError(error);
}
}
/**
* Get the currently in-progress task
*/
async getInProgressTask(): Promise<Task | null> {
try {
const response = await withRetry(
async () => {
return await this.client.get<Task>('/tasks/in-progress');
},
{
maxRetries: this.config.maxRetries,
delay: this.config.retryDelay,
backoffMultiplier: 2,
maxDelay: 10000,
}
);
if (response.data) {
console.log(
`[ByteBot MCP] Retrieved in-progress task: ${response.data.id}`
);
this.cacheTask(response.data.id, response.data);
return response.data;
}
console.log(`[ByteBot MCP] No task currently in progress`);
return null;
} catch (error) {
// 404 is expected when no task is in progress
const bytebotError = handleAxiosError(error);
if (bytebotError.statusCode === 404) {
return null;
}
logError('getInProgressTask', error);
throw bytebotError;
}
}
/**
* Update a task
*/
async updateTask(taskId: string, update: UpdateTaskRequest): Promise<Task> {
try {
validateTaskId(taskId);
// Validate update request
const validatedUpdate = UpdateTaskSchema.parse(update);
const response = await withRetry(
async () => {
return await this.client.patch<Task>(
`/tasks/${taskId}`,
validatedUpdate
);
},
{
maxRetries: this.config.maxRetries,
delay: this.config.retryDelay,
backoffMultiplier: 2,
maxDelay: 10000,
}
);
// Invalidate cache
this.invalidateCache(taskId);
console.log(
`[ByteBot MCP] Task ${taskId} updated: status=${response.data.status}`
);
return response.data;
} catch (error) {
logError('updateTask', error);
throw handleAxiosError(error);
}
}
/**
* Delete a task
*/
async deleteTask(taskId: string): Promise<void> {
try {
validateTaskId(taskId);
await withRetry(
async () => {
return await this.client.delete(`/tasks/${taskId}`);
},
{
maxRetries: this.config.maxRetries,
delay: this.config.retryDelay,
backoffMultiplier: 2,
maxDelay: 10000,
}
);
// Invalidate cache
this.invalidateCache(taskId);
console.log(`[ByteBot MCP] Task ${taskId} deleted successfully`);
} catch (error) {
logError('deleteTask', error);
throw handleAxiosError(error);
}
}
/**
* Health check
*/
async healthCheck(): Promise<boolean> {
try {
await this.client.get('/tasks', { timeout: 5000 });
return true;
} catch (error) {
console.error('[ByteBot MCP] Agent API health check failed:', error);
return false;
}
}
/**
* Cache management
*/
private getCachedTask(taskId: string): Task | null {
const entry = this.taskCache.get(taskId);
if (!entry) return null;
const now = Date.now();
if (now - entry.lastFetched > entry.ttl) {
// Cache expired
this.taskCache.delete(taskId);
return null;
}
return entry.task;
}
private cacheTask(taskId: string, task: Task): void {
this.taskCache.set(taskId, {
task,
lastFetched: Date.now(),
ttl: this.CACHE_TTL,
});
}
private invalidateCache(taskId: string): void {
this.taskCache.delete(taskId);
}
/**
* Clear all cache
*/
clearCache(): void {
this.taskCache.clear();
console.log('[ByteBot MCP] Task cache cleared');
}
}