package grpcapi import ( "context" "fmt" "io" "log" "net" "net/http" "sync" "time" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/anypb" "google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/timestamppb" "synapse/pkg/types" ) // ============================================================================= // gRPC Server Implementation // ============================================================================= // GRPCServer wraps the gRPC server and gateway type GRPCServer struct { grpcServer *grpc.Server gateway *http.Server mu sync.RWMutex // Services taskService *TaskServiceImpl messageService *MessageServiceImpl capabilitiesService *CapabilitiesServiceImpl groupService *GroupServiceImpl healthService *HealthServiceImpl // Dependencies taskExecutor TaskExecutor msgRouter MessageRouter capsProvider CapabilitiesProvider groupManager GroupManager } // TaskExecutor interface for task operations type TaskExecutor interface { SubmitTask(ctx context.Context, taskType string, payload map[string]any, externalTaskID, priority, callbackURL, callbackToken string) (string, string, error) GetTaskStatus(ctx context.Context, taskID string) (*types.TaskInfo, error) ListTasks(ctx context.Context, status string, limit, offset int) ([]types.TaskInfo, int, error) CancelTask(ctx context.Context, taskID string) error } type MessageRouter interface { RouteMessage(ctx context.Context, msg *types.UnifiedSynapseMessage) (*types.UnifiedSynapseMessage, error) } type CapabilitiesProvider interface { GetCapabilities(ctx context.Context) (*types.AgentCapabilities, error) } type GroupManager interface { CreateGroup(ctx context.Context, name, description, coordinatorID string, initialMembers []string) (string, error) JoinGroup(ctx context.Context, groupID string, agent *types.AgentEndpoint) error LeaveGroup(ctx context.Context, groupID, agentID string) error DisbandGroup(ctx context.Context, groupID string) error ListMembers(ctx context.Context, groupID string) ([]types.GroupMember, error) // Voting operations InitiateVote(ctx context.Context, groupID, initiatorID, question string, options []string, threshold float64, timeoutSeconds int32) (string, error) SubmitVote(ctx context.Context, voteID, agentID, option, reason string) error GetVoteStatus(ctx context.Context, voteID string) (*VoteStatus, error) // Task split and merge SplitTask(ctx context.Context, groupID, taskID, taskType, requirements string, strategy SplitStrategy) ([]*SubtaskInfo, error) MergeResults(ctx context.Context, groupID, parentTaskID string, strategy MergeStrategy) (*anypb.Any, string, error) } // VoteStatus represents the status of a vote type VoteStatus struct { VoteID string Status string // open, decided, expired Question string TotalVotes int RequiredVotes int WinningOption string Votes map[string]string // agentID -> option } // ============================================================================= // Server Constructor // ============================================================================= // NewGRPCServer creates a new gRPC server func NewGRPCServer(taskExec TaskExecutor, msgRouter MessageRouter, capsProvider CapabilitiesProvider, groupManager GroupManager) *GRPCServer { return &GRPCServer{ taskExecutor: taskExec, msgRouter: msgRouter, capsProvider: capsProvider, groupManager: groupManager, taskService: &TaskServiceImpl{ taskExecutor: taskExec, streamClients: make(map[string]chan *TaskStatusUpdate), }, messageService: &MessageServiceImpl{ msgRouter: msgRouter, }, capabilitiesService: &CapabilitiesServiceImpl{ provider: capsProvider, }, groupService: &GroupServiceImpl{ groupManager: groupManager, }, healthService: &HealthServiceImpl{}, } } // Start starts both gRPC and gateway servers func (s *GRPCServer) Start(grpcPort, gatewayPort int) error { // Start gRPC server go func() { lis, err := net.Listen("tcp", fmt.Sprintf(":%d", grpcPort)) if err != nil { log.Fatalf("[gRPC] Failed to listen: %v", err) } // Create gRPC server with interceptors opts := []grpc.ServerOption{ grpc.UnaryInterceptor(s.unaryInterceptor), grpc.StreamInterceptor(s.streamInterceptor), } s.grpcServer = grpc.NewServer(opts...) // Register services using ServiceDesc s.grpcServer.RegisterService(&TaskService_ServiceDesc, s.taskService) s.grpcServer.RegisterService(&A2AMessageService_ServiceDesc, s.messageService) s.grpcServer.RegisterService(&CapabilitiesService_ServiceDesc, s.capabilitiesService) s.grpcServer.RegisterService(&GroupService_ServiceDesc, s.groupService) s.grpcServer.RegisterService(&HealthService_ServiceDesc, s.healthService) log.Printf("[gRPC] Server started on port %d", grpcPort) if err := s.grpcServer.Serve(lis); err != nil { log.Fatalf("[gRPC] Failed to serve: %v", err) } }() // Start gRPC-Gateway go func() { _ = context.Background() // ctx reserved for future gateway handler registration mux := http.NewServeMux() // Register gateway handlers // Note: These would be generated from proto by grpc-gateway // err := RegisterTaskServiceHandlerFromEndpoint(ctx, mux, fmt.Sprintf("localhost:%d", grpcPort), opts) // ... etc s.gateway = &http.Server{ Addr: fmt.Sprintf(":%d", gatewayPort), Handler: mux, } log.Printf("[Gateway] HTTP server started on port %d", gatewayPort) if err := s.gateway.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("[Gateway] Failed to serve: %v", err) } }() return nil } // Stop gracefully stops both servers func (s *GRPCServer) Stop() { if s.grpcServer != nil { s.grpcServer.GracefulStop() } if s.gateway != nil { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() s.gateway.Shutdown(ctx) } } // ============================================================================= // Interceptors // ============================================================================= func (s *GRPCServer) unaryInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { // Extract metadata md, ok := metadata.FromIncomingContext(ctx) if ok { // Check API key apiKeys := md.Get("x-api-key") if len(apiKeys) > 0 { // Validate API key log.Printf("[gRPC] API Key: %s", apiKeys[0]) } } // Call handler return handler(ctx, req) } func (s *GRPCServer) streamInterceptor(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { // Extract metadata md, ok := metadata.FromIncomingContext(ss.Context()) if ok { apiKeys := md.Get("x-api-key") if len(apiKeys) > 0 { log.Printf("[gRPC Stream] API Key: %s", apiKeys[0]) } } return handler(srv, ss) } // ============================================================================= // Task Service Implementation // ============================================================================= type TaskServiceImpl struct { UnimplementedTaskServiceServer taskExecutor TaskExecutor mu sync.RWMutex streamClients map[string]chan *TaskStatusUpdate } func (s *TaskServiceImpl) SubmitTask(ctx context.Context, req *SubmitTaskRequest) (*SubmitTaskResponse, error) { // Convert payload payload := make(map[string]any) if req.Payload != nil { // Unpack Any to map // This would use protojson or similar } taskID, status, err := s.taskExecutor.SubmitTask( ctx, req.TaskType, payload, req.ExternalTaskId, req.Priority, req.CallbackUrl, req.CallbackToken, ) if err != nil { return &SubmitTaskResponse{ Success: false, ErrorCode: "TASK_SUBMISSION_FAILED", Message: err.Error(), }, nil } return &SubmitTaskResponse{ Success: true, TaskId: taskID, Status: parseTaskStatus(status), Message: "任务已接收", }, nil } func (s *TaskServiceImpl) SubmitTaskWithFiles(ctx context.Context, req *SubmitTaskWithFilesRequest) (*SubmitTaskResponse, error) { // Handle file uploads // Convert files and call SubmitTask return s.SubmitTask(ctx, req.Request) } func (s *TaskServiceImpl) GetTask(ctx context.Context, req *GetTaskRequest) (*TaskStatusResponse, error) { task, err := s.taskExecutor.GetTaskStatus(ctx, req.TaskId) if err != nil { return &TaskStatusResponse{ Success: false, ErrorCode: "TASK_NOT_FOUND", ErrorMessage: err.Error(), }, nil } return &TaskStatusResponse{ Success: true, Task: convertTaskInfo(task), }, nil } func (s *TaskServiceImpl) GetTaskStatus(ctx context.Context, req *GetTaskRequest) (*TaskStatusResponse, error) { return s.GetTask(ctx, req) } func (s *TaskServiceImpl) ListTasks(ctx context.Context, req *ListTasksRequest) (*ListTasksResponse, error) { tasks, total, err := s.taskExecutor.ListTasks(ctx, req.Status, int(req.Limit), int(req.Offset)) if err != nil { return &ListTasksResponse{ Success: false, }, err } var protoTasks []*TaskInfo for _, t := range tasks { protoTasks = append(protoTasks, convertTaskInfo(&t)) } return &ListTasksResponse{ Success: true, Tasks: protoTasks, Total: int32(total), Limit: req.Limit, Offset: req.Offset, HasMore: int(req.Offset+req.Limit) < total, }, nil } func (s *TaskServiceImpl) CancelTask(ctx context.Context, req *CancelTaskRequest) (*CancelTaskResponse, error) { if err := s.taskExecutor.CancelTask(ctx, req.TaskId); err != nil { return &CancelTaskResponse{ Success: false, Message: err.Error(), }, nil } return &CancelTaskResponse{ Success: true, TaskId: req.TaskId, Status: TaskStatus_TASK_STATUS_CANCELLED, Message: "任务已取消", }, nil } // StreamTaskStatus implements bidirectional streaming for task status updates func (s *TaskServiceImpl) StreamTaskStatus(stream TaskService_StreamTaskStatusServer) error { // Register client clientID := fmt.Sprintf("stream-%d", time.Now().UnixNano()) statusChan := make(chan *TaskStatusUpdate, 100) s.mu.Lock() s.streamClients[clientID] = statusChan s.mu.Unlock() defer func() { s.mu.Lock() delete(s.streamClients, clientID) s.mu.Unlock() close(statusChan) }() // Goroutine to send status updates go func() { for update := range statusChan { if err := stream.Send(update); err != nil { log.Printf("[gRPC Stream] Send error: %v", err) return } } }() // Receive subscription requests for { req, err := stream.Recv() if err == io.EOF { return nil } if err != nil { return err } if req.Subscribe { log.Printf("[gRPC Stream] Client %s subscribed to task %s", clientID, req.TaskId) // Start watching task status go s.watchTaskStatus(req.TaskId, statusChan) } else { log.Printf("[gRPC Stream] Client %s unsubscribed from task %s", clientID, req.TaskId) } } } func (s *TaskServiceImpl) watchTaskStatus(taskID string, ch chan *TaskStatusUpdate) { ticker := time.NewTicker(2 * time.Second) defer ticker.Stop() for range ticker.C { task, err := s.taskExecutor.GetTaskStatus(context.Background(), taskID) if err != nil { continue } ch <- &TaskStatusUpdate{ TaskId: taskID, Status: parseTaskStatus(task.Status), Progress: int32(task.Progress), Message: task.Message, } // Stop if terminal state if task.Status == "completed" || task.Status == "failed" || task.Status == "cancelled" { return } } } // ============================================================================= // Message Service Implementation // ============================================================================= type MessageServiceImpl struct { UnimplementedA2AMessageServiceServer msgRouter MessageRouter } func (s *MessageServiceImpl) SendMessage(ctx context.Context, req *SendMessageRequest) (*SendMessageResponse, error) { // Convert proto message to internal format msg := convertProtoToInternalMessage(req.Message) resp, err := s.msgRouter.RouteMessage(ctx, msg) if err != nil { return &SendMessageResponse{ Success: false, Error: err.Error(), }, nil } return &SendMessageResponse{ Success: true, MessageId: resp.ID, }, nil } // StreamMessages implements bidirectional streaming for Synapse messages func (s *MessageServiceImpl) StreamMessages(stream A2AMessageService_StreamMessagesServer) error { ctx := stream.Context() clientID := fmt.Sprintf("msg-stream-%d", time.Now().UnixNano()) log.Printf("[gRPC Stream] New message stream: %s", clientID) // Goroutine to receive messages go func() { for { msg, err := stream.Recv() if err == io.EOF { return } if err != nil { log.Printf("[gRPC Stream] Receive error: %v", err) return } internalMsg := convertProtoToInternalMessage(msg) resp, err := s.msgRouter.RouteMessage(ctx, internalMsg) if err != nil { log.Printf("[gRPC Stream] Route error: %v", err) continue } protoResp := convertInternalToProtoMessage(resp) if err := stream.Send(protoResp); err != nil { log.Printf("[gRPC Stream] Send error: %v", err) return } } }() // Keep stream alive <-ctx.Done() log.Printf("[gRPC Stream] Message stream closed: %s", clientID) return nil } func (s *MessageServiceImpl) ReceiveCallback(req *emptypb.Empty, stream A2AMessageService_ReceiveCallbackServer) error { // This would be used by agents to receive callback messages via gRPC stream // Implementation depends on callback routing mechanism return status.Error(codes.Unimplemented, "not implemented") } // ============================================================================= // Capabilities Service Implementation // ============================================================================= type CapabilitiesServiceImpl struct { UnimplementedCapabilitiesServiceServer provider CapabilitiesProvider } func (s *CapabilitiesServiceImpl) GetCapabilities(ctx context.Context, req *emptypb.Empty) (*CapabilitiesResponse, error) { caps, err := s.provider.GetCapabilities(ctx) if err != nil { return &CapabilitiesResponse{ Success: false, Error: err.Error(), }, nil } return &CapabilitiesResponse{ Success: true, Capabilities: convertAgentCapabilities(caps), }, nil } func (s *CapabilitiesServiceImpl) QueryAgent(ctx context.Context, req *QueryAgentRequest) (*CapabilitiesResponse, error) { // Query specific agent's capabilities return s.GetCapabilities(ctx, nil) } func (s *CapabilitiesServiceImpl) ListAgents(ctx context.Context, req *emptypb.Empty) (*ListAgentsResponse, error) { return &ListAgentsResponse{ Success: true, Agents: []*AgentInfo{}, }, nil } // ============================================================================= // Group Service Implementation // ============================================================================= type GroupServiceImpl struct { UnimplementedGroupServiceServer groupManager GroupManager } func (s *GroupServiceImpl) CreateGroup(ctx context.Context, req *CreateGroupRequest) (*CreateGroupResponse, error) { groupID, err := s.groupManager.CreateGroup(ctx, req.Name, req.Description, req.CoordinatorId, req.InitialMembers) if err != nil { return &CreateGroupResponse{ Success: false, Error: err.Error(), }, nil } return &CreateGroupResponse{ Success: true, GroupId: groupID, Group: &GroupInfo{ Id: groupID, Name: req.Name, Description: req.Description, CoordinatorId: req.CoordinatorId, Status: "active", CreatedAt: timestamppb.Now(), }, }, nil } func (s *GroupServiceImpl) JoinGroup(ctx context.Context, req *JoinGroupRequest) (*JoinGroupResponse, error) { err := s.groupManager.JoinGroup(ctx, req.GroupId, convertProtoEndpoint(req.Agent)) if err != nil { return &JoinGroupResponse{Success: false, Error: err.Error()}, nil } return &JoinGroupResponse{Success: true}, nil } func (s *GroupServiceImpl) LeaveGroup(ctx context.Context, req *LeaveGroupRequest) (*LeaveGroupResponse, error) { err := s.groupManager.LeaveGroup(ctx, req.GroupId, req.AgentId) if err != nil { return &LeaveGroupResponse{Success: false, Error: err.Error()}, nil } return &LeaveGroupResponse{Success: true}, nil } func (s *GroupServiceImpl) DisbandGroup(ctx context.Context, req *DisbandGroupRequest) (*DisbandGroupResponse, error) { err := s.groupManager.DisbandGroup(ctx, req.GroupId) if err != nil { return &DisbandGroupResponse{Success: false, Error: err.Error()}, nil } return &DisbandGroupResponse{Success: true}, nil } func (s *GroupServiceImpl) ListGroupMembers(ctx context.Context, req *ListGroupMembersRequest) (*ListGroupMembersResponse, error) { members, err := s.groupManager.ListMembers(ctx, req.GroupId) if err != nil { return &ListGroupMembersResponse{Success: false}, nil } var protoMembers []*GroupMember for _, m := range members { protoMembers = append(protoMembers, &GroupMember{ AgentId: m.AgentID, AgentName: m.AgentName, Role: m.Role, Status: string(m.Status), JoinedAt: timestamppb.Now(), }) } return &ListGroupMembersResponse{ Success: true, Members: protoMembers, }, nil } // StreamGroupMessages implements bidirectional streaming for group messages func (s *GroupServiceImpl) StreamGroupMessages(stream GroupService_StreamGroupMessagesServer) error { ctx := stream.Context() clientID := fmt.Sprintf("group-stream-%d", time.Now().UnixNano()) log.Printf("[gRPC Stream] New group stream: %s", clientID) go func() { for { msg, err := stream.Recv() if err == io.EOF { return } if err != nil { log.Printf("[gRPC Stream] Receive error: %v", err) return } log.Printf("[gRPC Stream] Group message received: group=%s, type=%v", msg.GroupId, msg.Type) // Echo back for now if err := stream.Send(msg); err != nil { log.Printf("[gRPC Stream] Send error: %v", err) return } } }() <-ctx.Done() log.Printf("[gRPC Stream] Group stream closed: %s", clientID) return nil } // InitiateVote initiates a voting process in the group func (s *GroupServiceImpl) InitiateVote(ctx context.Context, req *VoteRequest) (*VoteResponse, error) { voteID, err := s.groupManager.InitiateVote(ctx, req.GroupId, req.InitiatorId, req.Question, req.Options, req.Threshold, req.TimeoutSeconds) if err != nil { return &VoteResponse{ Success: false, Error: err.Error(), }, nil } return &VoteResponse{ Success: true, VoteId: voteID, }, nil } // SubmitVote submits a vote in an ongoing voting process func (s *GroupServiceImpl) SubmitVote(ctx context.Context, req *SubmitVoteRequest) (*SubmitVoteResponse, error) { err := s.groupManager.SubmitVote(ctx, req.VoteId, req.AgentId, req.Option, req.Reason) if err != nil { return &SubmitVoteResponse{ Success: false, Error: err.Error(), }, nil } return &SubmitVoteResponse{ Success: true, }, nil } // GetVoteStatus gets the current status of a vote func (s *GroupServiceImpl) GetVoteStatus(ctx context.Context, req *GetVoteStatusRequest) (*VoteStatusResponse, error) { status, err := s.groupManager.GetVoteStatus(ctx, req.VoteId) if err != nil { return &VoteStatusResponse{ Success: false, Error: err.Error(), }, nil } return &VoteStatusResponse{ Success: true, VoteId: status.VoteID, Status: status.Status, Question: status.Question, TotalVotes: int32(status.TotalVotes), RequiredVotes: int32(status.RequiredVotes), WinningOption: status.WinningOption, }, nil } // SplitTask splits a task into subtasks across group members func (s *GroupServiceImpl) SplitTask(ctx context.Context, req *SplitTaskRequest) (*SplitTaskResponse, error) { subtasks, err := s.groupManager.SplitTask(ctx, req.GroupId, req.TaskId, req.TaskType, req.Requirements, req.Strategy) if err != nil { return &SplitTaskResponse{ Success: false, Error: err.Error(), }, nil } return &SplitTaskResponse{ Success: true, GroupId: req.GroupId, ParentTaskId: req.TaskId, Subtasks: subtasks, }, nil } // MergeResults merges results from subtasks func (s *GroupServiceImpl) MergeResults(ctx context.Context, req *MergeResultsRequest) (*MergeResultsResponse, error) { mergedResult, summary, err := s.groupManager.MergeResults(ctx, req.GroupId, req.ParentTaskId, req.Strategy) if err != nil { return &MergeResultsResponse{ Success: false, Error: err.Error(), }, nil } return &MergeResultsResponse{ Success: true, ParentTaskId: req.ParentTaskId, MergedResult: mergedResult, Summary: summary, }, nil } // ============================================================================= // Health Service Implementation // ============================================================================= type HealthServiceImpl struct { UnimplementedHealthServiceServer } func (s *HealthServiceImpl) HealthCheck(ctx context.Context, req *emptypb.Empty) (*HealthCheckResponse, error) { return &HealthCheckResponse{ Status: "healthy", Version: "1.0.0", UptimeSeconds: 3600, ActiveTasks: 0, }, nil } // ============================================================================= // Conversion Helpers // ============================================================================= func parseTaskStatus(status string) TaskStatus { switch status { case "received": return TaskStatus_TASK_STATUS_RECEIVED case "processing": return TaskStatus_TASK_STATUS_PROCESSING case "completed": return TaskStatus_TASK_STATUS_COMPLETED case "failed": return TaskStatus_TASK_STATUS_FAILED case "cancelled": return TaskStatus_TASK_STATUS_CANCELLED case "rejected": return TaskStatus_TASK_STATUS_REJECTED case "waiting": return TaskStatus_TASK_STATUS_WAITING default: return TaskStatus_TASK_STATUS_UNSPECIFIED } } func convertTaskInfo(task *types.TaskInfo) *TaskInfo { if task == nil { return nil } var result *anypb.Any if task.Result != nil { result, _ = anypb.New(&anypb.Any{Value: []byte(fmt.Sprintf("%v", task.Result))}) } return &TaskInfo{ Id: task.ID, Type: task.Type, Status: parseTaskStatus(task.Status), Progress: int32(task.Progress), Message: task.Message, CreatedAt: timestamppb.Now(), UpdatedAt: timestamppb.Now(), Result: result, } } func convertAgentCapabilities(caps *types.AgentCapabilities) *AgentCapabilities { if caps == nil { return nil } var taskTypes []*SupportedTaskType for _, tt := range caps.SupportedTaskTypes { taskTypes = append(taskTypes, &SupportedTaskType{ Type: tt.Type, Name: tt.Name, Description: tt.Description, EstimatedTime: tt.EstimatedTime, InputSchema: tt.InputSchema, OutputSchema: tt.OutputSchema, Capabilities: tt.Capabilities, }) } var transportModes []TransportMode for _, tm := range caps.SupportedTransportModes { transportModes = append(transportModes, TransportMode(TransportMode_value[string(tm)])) } return &AgentCapabilities{ AgentId: caps.AgentID, AgentName: caps.AgentName, SystemId: caps.SystemID, ProtocolVersion: caps.ProtocolVersion, SupportedTaskTypes: taskTypes, SupportedContentTypes: caps.SupportedContentTypes, SupportedTransportModes: transportModes, MaxFileSize: caps.MaxFileSize, MaxConcurrentTasks: int32(caps.MaxConcurrentTasks), MaxTaskDurationSeconds: int32(caps.MaxTaskDuration), SupportProgressUpdates: caps.SupportProgressUpdates, SupportStreaming: caps.SupportStreaming, SupportHil: caps.SupportHiL, SupportGroups: caps.SupportGroups, } } func convertProtoToInternalMessage(msg *A2AMessage) *types.UnifiedSynapseMessage { if msg == nil { return nil } var payload []byte if msg.Payload != nil { payload = msg.Payload.Value } return &types.UnifiedSynapseMessage{ ID: msg.Id, Version: msg.Version, Type: string(MessageType_name[int32(msg.Type)]), Sender: types.AgentEndpoint{AgentID: msg.Sender.AgentId}, CorrelationID: msg.CorrelationId, ReplyTo: msg.ReplyTo, GroupID: msg.GroupId, Timestamp: types.Timestamp(msg.Timestamp.AsTime().Format(time.RFC3339)), Payload: payload, Signature: msg.Signature, } } func convertInternalToProtoMessage(msg *types.UnifiedSynapseMessage) *A2AMessage { if msg == nil { return nil } var payload *anypb.Any if len(msg.Payload) > 0 { payload = &anypb.Any{Value: msg.Payload} } return &A2AMessage{ Id: msg.ID, Version: msg.Version, Type: MessageType(MessageType_value[msg.Type]), Sender: &AgentEndpoint{AgentId: msg.Sender.AgentID}, CorrelationId: msg.CorrelationID, ReplyTo: msg.ReplyTo, GroupId: msg.GroupID, Timestamp: timestamppb.Now(), Payload: payload, Signature: msg.Signature, } } func convertProtoEndpoint(endpoint *AgentEndpoint) *types.AgentEndpoint { if endpoint == nil { return nil } return &types.AgentEndpoint{ AgentID: endpoint.AgentId, AgentName: endpoint.AgentName, SystemID: endpoint.SystemId, Endpoint: endpoint.Endpoint, Version: endpoint.Version, } }