Because Intel designed it that way, and having the flexibility to encode or [di], [si] would have made the instruction longer even when you were using or al, [si].
What you read about read or write mode doesn't make sense, because or [rdi], rax does a read-modify-write. So there always were instructions that did both, even apart from the few instructions that could read one location and write another (see below).
When Intel designed x86, they decided to limit the complexity of instruction encoding by only allowing a choice of addressing modes for one operand per instruction. The other register operand is selected by 3 bits in the byte that also selects an addressing mode. (Or 3 bits as part of the opcode, or 3 bits stuffed somewhere else... current x86 instruction encoding has so many special cases...)
x86-64 added an extra bit to double the amount of registers, but didn't change anything fundamental. (It did add RIP-relative addressing, though.)
See links to Intel manuals from https://stackoverflow.com/tags/x86/info.
Most x86 instructions take one of these forms
op r, r/m32 op r/m32, r
The r being one of the gp registers, and the r/m32 being a register or memory operand, depending on the addressing-mode.
The two versions are usually different opcodes, even though they share the same mnemonic. So from a hardware / machine-language POV, the memory source and memory dest versions are two separate instructions. mov itself has many different opcodes, for various special cases. (especially if you count move to/from control registers.)
As people note in comments, there are instructions that both read and write memory, with at least one of the operands having an implicit location not encoded in the usual mod/rm addressing mode encoding.
movs: string move. (don't use except with rep, it's super slow) push r/m32 (e.g. push [rax]).
MOVSmentioned above or evenPUSH/POP, and those can do memory-to-memory copies.MOVSdoes not use modrm at all, it doesn't have explicit operands. It's a single byte opcode, and the cpu knows to useESI/EDIautomatically (that's why it's implicit).