/// Agent execution endpoints for CQRS API library; import 'dart:convert'; import 'dart:io'; import 'dart:async'; import 'package:http/http.dart' as http; import '../client.dart'; import '../types.dart'; // ============================================================================= // Enums // ============================================================================= /// Represents the current status of an agent execution enum ExecutionStatus { pending('Pending'), running('Running'), completed('Completed'), failed('Failed'), cancelled('Cancelled'); const ExecutionStatus(this.value); final String value; static ExecutionStatus fromString(String value) { return ExecutionStatus.values.firstWhere( (status) => status.value == value, orElse: () => ExecutionStatus.pending, ); } static ExecutionStatus fromInt(int value) { if (value >= 0 && value < ExecutionStatus.values.length) { return ExecutionStatus.values[value]; } return ExecutionStatus.pending; } } // ============================================================================= // Commands // ============================================================================= /// Command to start an agent execution class StartAgentExecutionCommand implements Serializable { final String agentId; final String? conversationId; final String userPrompt; const StartAgentExecutionCommand({ required this.agentId, this.conversationId, required this.userPrompt, }); @override Map toJson() => { 'agentId': agentId, if (conversationId != null) 'conversationId': conversationId, 'userPrompt': userPrompt, }; } /// Command to complete an agent execution with results class CompleteAgentExecutionCommand implements Serializable { final String executionId; final ExecutionStatus status; final String? response; final int? inputTokens; final int? outputTokens; final double? estimatedCost; final String? errorMessage; const CompleteAgentExecutionCommand({ required this.executionId, required this.status, this.response, this.inputTokens, this.outputTokens, this.estimatedCost, this.errorMessage, }); @override Map toJson() => { 'executionId': executionId, 'status': ExecutionStatus.values.indexOf(status), if (response != null) 'response': response, if (inputTokens != null) 'inputTokens': inputTokens, if (outputTokens != null) 'outputTokens': outputTokens, if (estimatedCost != null) 'estimatedCost': estimatedCost, if (errorMessage != null) 'errorMessage': errorMessage, }; } // ============================================================================= // Queries // ============================================================================= /// Query to get a single execution by ID class GetAgentExecutionQuery implements Serializable { final String id; const GetAgentExecutionQuery({required this.id}); @override Map toJson() => {'id': id}; } // ============================================================================= // DTOs // ============================================================================= /// Response when starting an execution (returns only ID) class StartExecutionResult { final String id; const StartExecutionResult({required this.id}); factory StartExecutionResult.fromJson(Map json) { return StartExecutionResult( id: json['id'] as String, ); } Map toJson() => {'id': id}; } /// Full agent execution details class AgentExecutionDto { final String id; final String agentId; final String? conversationId; final String userPrompt; final String? response; final ExecutionStatus status; final DateTime startedAt; final DateTime? completedAt; final int? inputTokens; final int? outputTokens; final double? estimatedCost; final int messageCount; final String? errorMessage; const AgentExecutionDto({ required this.id, required this.agentId, this.conversationId, required this.userPrompt, this.response, required this.status, required this.startedAt, this.completedAt, this.inputTokens, this.outputTokens, this.estimatedCost, required this.messageCount, this.errorMessage, }); factory AgentExecutionDto.fromJson(Map json) { // Handle status as either int or string ExecutionStatus status; final Object? statusValue = json['status']; if (statusValue is int) { status = ExecutionStatus.fromInt(statusValue); } else if (statusValue is String) { status = ExecutionStatus.fromString(statusValue); } else { status = ExecutionStatus.pending; } return AgentExecutionDto( id: json['id'] as String, agentId: json['agentId'] as String, conversationId: json['conversationId'] as String?, userPrompt: json['userPrompt'] as String, response: json['response'] as String?, status: status, startedAt: DateTime.parse(json['startedAt'] as String), completedAt: json['completedAt'] != null ? DateTime.parse(json['completedAt'] as String) : null, inputTokens: json['inputTokens'] as int?, outputTokens: json['outputTokens'] as int?, estimatedCost: json['estimatedCost'] != null ? (json['estimatedCost'] as num).toDouble() : null, messageCount: json['messageCount'] as int, errorMessage: json['errorMessage'] as String?, ); } Map toJson() => { 'id': id, 'agentId': agentId, 'conversationId': conversationId, 'userPrompt': userPrompt, 'response': response, 'status': status.value, 'startedAt': startedAt.toIso8601String(), 'completedAt': completedAt?.toIso8601String(), 'inputTokens': inputTokens, 'outputTokens': outputTokens, 'estimatedCost': estimatedCost, 'messageCount': messageCount, 'errorMessage': errorMessage, }; } /// Execution list item (lightweight version for lists) class ExecutionListItemDto { final String id; final String agentId; final String agentName; final String? conversationId; final String userPrompt; final ExecutionStatus status; final DateTime startedAt; final DateTime? completedAt; final int? inputTokens; final int? outputTokens; final double? estimatedCost; final int messageCount; final String? errorMessage; const ExecutionListItemDto({ required this.id, required this.agentId, required this.agentName, this.conversationId, required this.userPrompt, required this.status, required this.startedAt, this.completedAt, this.inputTokens, this.outputTokens, this.estimatedCost, required this.messageCount, this.errorMessage, }); factory ExecutionListItemDto.fromJson(Map json) { // Handle status as either int or string ExecutionStatus status; final Object? statusValue = json['status']; if (statusValue is int) { status = ExecutionStatus.fromInt(statusValue); } else if (statusValue is String) { status = ExecutionStatus.fromString(statusValue); } else { status = ExecutionStatus.pending; } return ExecutionListItemDto( id: json['id'] as String, agentId: json['agentId'] as String, agentName: json['agentName'] as String, conversationId: json['conversationId'] as String?, userPrompt: json['userPrompt'] as String, status: status, startedAt: DateTime.parse(json['startedAt'] as String), completedAt: json['completedAt'] != null ? DateTime.parse(json['completedAt'] as String) : null, inputTokens: json['inputTokens'] as int?, outputTokens: json['outputTokens'] as int?, estimatedCost: json['estimatedCost'] != null ? (json['estimatedCost'] as num).toDouble() : null, messageCount: json['messageCount'] as int, errorMessage: json['errorMessage'] as String?, ); } Map toJson() => { 'id': id, 'agentId': agentId, 'agentName': agentName, 'conversationId': conversationId, 'userPrompt': userPrompt, 'status': status.value, 'startedAt': startedAt.toIso8601String(), 'completedAt': completedAt?.toIso8601String(), 'inputTokens': inputTokens, 'outputTokens': outputTokens, 'estimatedCost': estimatedCost, 'messageCount': messageCount, 'errorMessage': errorMessage, }; } // ============================================================================= // Extension Methods // ============================================================================= /// Agent execution management endpoints extension ExecutionEndpoint on CqrsApiClient { /// Start an agent execution /// /// Returns the ID of the newly created execution. /// /// Example: /// ```dart /// final result = await client.startAgentExecution( /// StartAgentExecutionCommand( /// agentId: 'agent-uuid', /// conversationId: 'conversation-uuid', // Optional /// userPrompt: 'Generate a function to calculate factorial', /// ), /// ); /// /// result.when( /// success: (started) => print('Execution ID: ${started.id}'), /// error: (error) => print('Error: ${error.message}'), /// ); /// ``` Future> startAgentExecution( StartAgentExecutionCommand command, ) async { try { final Uri url = Uri.parse('${config.baseUrl}/api/command/startAgentExecution'); final String body = jsonEncode(command.toJson()); final http.Response response = await http .post( url, headers: config.defaultHeaders, body: body, ) .timeout(config.timeout); if (response.statusCode >= 200 && response.statusCode < 300) { try { final Object? json = jsonDecode(response.body); final StartExecutionResult result = StartExecutionResult.fromJson(json as Map); return ApiSuccess(result); } catch (e) { return ApiError( ApiErrorInfo( message: 'Failed to parse start execution response', statusCode: response.statusCode, type: ApiErrorType.serialization, details: e.toString(), ), ); } } else { return ApiError( ApiErrorInfo( message: 'Start execution failed', statusCode: response.statusCode, type: ApiErrorType.http, ), ); } } on TimeoutException catch (e) { return ApiError( ApiErrorInfo( message: 'Request timeout: ${e.message ?? "Operation took too long"}', type: ApiErrorType.timeout, ), ); } on SocketException catch (e) { return ApiError( ApiErrorInfo( message: 'Network error: ${e.message}', type: ApiErrorType.network, details: e.osError?.message, ), ); } catch (e) { return ApiError( ApiErrorInfo( message: 'Unexpected error: $e', type: ApiErrorType.unknown, ), ); } } /// Complete an agent execution with results /// /// Example: /// ```dart /// final result = await client.completeAgentExecution( /// CompleteAgentExecutionCommand( /// executionId: 'execution-uuid', /// status: ExecutionStatus.completed, /// response: 'Here is the factorial function...', /// inputTokens: 100, /// outputTokens: 200, /// estimatedCost: 0.003, /// ), /// ); /// ``` Future> completeAgentExecution( CompleteAgentExecutionCommand command, ) async { return executeCommand( endpoint: 'completeAgentExecution', command: command, ); } /// Get a single execution by ID with full details /// /// Example: /// ```dart /// final result = await client.getAgentExecution('execution-uuid'); /// /// result.when( /// success: (execution) { /// print('Status: ${execution.status.value}'); /// print('Prompt: ${execution.userPrompt}'); /// print('Response: ${execution.response}'); /// print('Tokens: ${execution.inputTokens} in, ${execution.outputTokens} out'); /// }, /// error: (error) => print('Error: ${error.message}'), /// ); /// ``` Future> getAgentExecution(String id) async { return executeQuery( endpoint: 'getAgentExecution', query: GetAgentExecutionQuery(id: id), fromJson: (Object? json) => AgentExecutionDto.fromJson(json as Map), ); } /// List all executions /// /// Returns a list of all executions from the backend. /// Backend endpoint: GET /api/executions /// /// Example: /// ```dart /// final result = await client.listExecutions(); /// /// result.when( /// success: (executions) { /// print('Found ${executions.length} executions'); /// for (final exec in executions) { /// print('${exec.agentName}: ${exec.status.value}'); /// } /// }, /// error: (error) => print('Error: ${error.message}'), /// ); /// ``` Future>> listExecutions() async { try { final Uri url = Uri.parse('${config.baseUrl}/api/executions'); final http.Response response = await http .get(url, headers: config.defaultHeaders) .timeout(config.timeout); if (response.statusCode >= 200 && response.statusCode < 300) { final Object? jsonData = jsonDecode(response.body); if (jsonData is! List) { return ApiError>(ApiErrorInfo( message: 'Expected array response, got ${jsonData.runtimeType}', type: ApiErrorType.serialization, )); } final List executions = jsonData .map((Object? item) => ExecutionListItemDto.fromJson(item as Map)) .toList(); return ApiSuccess>(executions); } return ApiError>(ApiErrorInfo( message: 'Failed to load executions', type: ApiErrorType.http, statusCode: response.statusCode, )); } on TimeoutException { return ApiError>(ApiErrorInfo( message: 'Request timed out', type: ApiErrorType.timeout, )); } on SocketException { return ApiError>(ApiErrorInfo( message: 'No internet connection', type: ApiErrorType.network, )); } catch (e) { return ApiError>(ApiErrorInfo( message: 'Unexpected error: $e', type: ApiErrorType.unknown, )); } } /// List executions filtered by status /// /// Returns executions matching the specified status. /// Backend endpoint: GET /api/executions/status/{status} /// /// Example: /// ```dart /// final result = await client.listExecutionsByStatus(ExecutionStatus.running); /// /// result.when( /// success: (executions) { /// print('Found ${executions.length} running executions'); /// }, /// error: (error) => print('Error: ${error.message}'), /// ); /// ``` Future>> listExecutionsByStatus( ExecutionStatus status, ) async { try { final String statusValue = ExecutionStatus.values.indexOf(status).toString(); final Uri url = Uri.parse('${config.baseUrl}/api/executions/status/$statusValue'); final http.Response response = await http .get(url, headers: config.defaultHeaders) .timeout(config.timeout); if (response.statusCode >= 200 && response.statusCode < 300) { final Object? jsonData = jsonDecode(response.body); if (jsonData is! List) { return ApiError>(ApiErrorInfo( message: 'Expected array response, got ${jsonData.runtimeType}', type: ApiErrorType.serialization, )); } final List executions = jsonData .map((Object? item) => ExecutionListItemDto.fromJson(item as Map)) .toList(); return ApiSuccess>(executions); } return ApiError>(ApiErrorInfo( message: 'Failed to load executions by status', type: ApiErrorType.http, statusCode: response.statusCode, )); } on TimeoutException { return ApiError>(ApiErrorInfo( message: 'Request timed out', type: ApiErrorType.timeout, )); } on SocketException { return ApiError>(ApiErrorInfo( message: 'No internet connection', type: ApiErrorType.network, )); } catch (e) { return ApiError>(ApiErrorInfo( message: 'Unexpected error: $e', type: ApiErrorType.unknown, )); } } }