Implement full CQRS API integration with type-safe endpoints for all core backend operations. ## What's New - **Agent Management**: 4 endpoints (create, get, update, delete) with 3 enums - **Conversations**: 2 endpoints (create, get) with message support - **Executions**: 3 endpoints (start, complete, get) with status tracking - **OpenAPI Schema**: Updated to backend v1.0.0-mvp (10 endpoints) ## Implementation Details - All endpoints follow CQRS pattern (commands/queries) - 100% strict typing (no dynamic, all explicit types) - Functional error handling with Result<T> pattern - 3,136+ lines of production code - 1,500+ lines of comprehensive documentation ## Files Added - lib/api/endpoints/agent_endpoint.dart (364 lines) - lib/api/endpoints/conversation_endpoint.dart (319 lines) - lib/api/endpoints/execution_endpoint.dart (434 lines) - lib/api/examples/agent_example.dart (212 lines) - docs/AGENT_API_INTEGRATION.md (431 lines) - docs/COMPLETE_API_INTEGRATION.md (555 lines) - docs/INTEGRATION_STATUS.md (339 lines) ## Quality Metrics - Flutter analyze: 0 errors ✅ - Type safety: 100% (0 dynamic types) ✅ - CQRS compliance: 100% ✅ - Backend compatibility: v1.0.0-mvp ✅ ## Backend Integration - Updated api-schema.json from backend openapi.json - Supports all MVP endpoints except list operations (deferred to Phase 3) - Ready for JWT authentication (infrastructure in place) ## Usage ```dart import 'package:console/api/api.dart'; final client = CqrsApiClient(config: ApiClientConfig.development); // Agent CRUD await client.createAgent(CreateAgentCommand(...)); await client.getAgent('uuid'); // Conversations await client.createConversation(CreateConversationCommand(...)); // Executions await client.startAgentExecution(StartAgentExecutionCommand(...)); ``` 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
435 lines
13 KiB
Dart
435 lines
13 KiB
Dart
/// 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<String, Object?> 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<String, Object?> 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<String, Object?> 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<String, Object?> json) {
|
|
return StartExecutionResult(
|
|
id: json['id'] as String,
|
|
);
|
|
}
|
|
|
|
Map<String, Object?> 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<String, Object?> 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<String, Object?> 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<String, Object?> 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<String, Object?> 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<Result<StartExecutionResult>> 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<String, Object?>);
|
|
return ApiSuccess<StartExecutionResult>(result);
|
|
} catch (e) {
|
|
return ApiError<StartExecutionResult>(
|
|
ApiErrorInfo(
|
|
message: 'Failed to parse start execution response',
|
|
statusCode: response.statusCode,
|
|
type: ApiErrorType.serialization,
|
|
details: e.toString(),
|
|
),
|
|
);
|
|
}
|
|
} else {
|
|
return ApiError<StartExecutionResult>(
|
|
ApiErrorInfo(
|
|
message: 'Start execution failed',
|
|
statusCode: response.statusCode,
|
|
type: ApiErrorType.http,
|
|
),
|
|
);
|
|
}
|
|
} on TimeoutException catch (e) {
|
|
return ApiError<StartExecutionResult>(
|
|
ApiErrorInfo(
|
|
message: 'Request timeout: ${e.message ?? "Operation took too long"}',
|
|
type: ApiErrorType.timeout,
|
|
),
|
|
);
|
|
} on SocketException catch (e) {
|
|
return ApiError<StartExecutionResult>(
|
|
ApiErrorInfo(
|
|
message: 'Network error: ${e.message}',
|
|
type: ApiErrorType.network,
|
|
details: e.osError?.message,
|
|
),
|
|
);
|
|
} catch (e) {
|
|
return ApiError<StartExecutionResult>(
|
|
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<Result<void>> 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<Result<AgentExecutionDto>> getAgentExecution(String id) async {
|
|
return executeQuery<AgentExecutionDto>(
|
|
endpoint: 'getAgentExecution',
|
|
query: GetAgentExecutionQuery(id: id),
|
|
fromJson: (Object? json) =>
|
|
AgentExecutionDto.fromJson(json as Map<String, Object?>),
|
|
);
|
|
}
|
|
}
|