///|
/// The spawned-agent handle handed back with client-side process ports.  The
/// `ports` value drives the single connection engine; `child`, `child_stdin`,
/// and `child_stdout` expose the real `moonbitlang/async` process seams so a
/// composition root can close the child's stdin (the ACP stdio shutdown
/// signal), wait for the exit status, or cancel the child without any wrapper
/// API duplicating the underlying library.
pub(all) struct RuntimeProcessPorts {
  ports : RuntimePorts
  child : @process.Process
  child_stdin : @process.WriteToProcess
  child_stdout : @process.ReadFromProcess
}

///|
/// Construct the client side of one ACP stdio connection over a spawned child
/// process.  The child's stdin is the writer target and its stdout is the
/// reader source; the child's stderr is redirected to this process's real
/// stderr so agent diagnostics stay diagnostics and stdout stays pure frames.
/// Both pipe ends are raw, unbuffered `@process` handles, so every frame
/// write is delivered to the operating system in that call — the pinned
/// flush-per-write discipline, structurally identical to
/// `runtime_stdio_ports`.
///
/// The child is spawned with `no_wait = true` inside the caller's task group:
/// when the group terminates, the async process layer cancels its wait task,
/// which gracefully terminates the child (then forcefully after its timeout)
/// and reaps it.  Child lifetime is therefore bound to the structured
/// concurrency scope that owns the connection — no background reaper loop, no
/// detached process.  A well-behaved agent additionally observes the stdin EOF
/// a composition sends through `child_stdin.close()` before teardown.
///
/// Fail-fast mapping (the closed stable `RuntimeError` set has no process
/// category, so each failure keeps its precise kind in the trace, mirroring
/// the documented closest-category mapping of the outbound channel):
/// an empty command is rejected as `InvalidOptions` before any OS call; a
/// failed stdout pipe is `ReaderFailed` and a failed stdin pipe is
/// `WriterFailed` (the seam that could not be constructed); an OS spawn
/// rejection — including a missing command — is `InvalidOptions` with trace
/// kind `process_spawn_failed`, because the caller's process configuration
/// was rejected before any I/O seam ever ran.  Every failure is typed and
/// accompanied by one trace event; nothing degrades silently.
pub async fn[G] runtime_process_ports(
  group : @async.TaskGroup[G],
  handlers~ : RuntimeHandlerPort,
  command~ : String,
  args? : Array[String] = [],
  extra_env? : Map[String, String],
  inherit_env? : Bool = true,
  cancel_outbound? : RuntimeCancelOutboundHandler? = None,
  trace? : (RuntimeTraceEvent) -> Unit = runtime_stderr_trace,
) -> RuntimeProcessPorts raise RuntimeError {
  if command.is_empty() {
    trace(
      runtime_trace_event(
        "runtime", "process_spawn_rejected", "", "", "process_command_invalid",
      ),
    )
    raise InvalidOptions
  }
  let (child_stdin_end, child_stdin) = @process.write_to_process() catch {
    _ => {
      trace(
        runtime_trace_event(
          "runtime", "process_spawn_rejected", "", "", "process_stdin_pipe_failed",
        ),
      )
      raise WriterFailed
    }
  }
  let (child_stdout, child_stdout_end) = @process.read_from_process() catch {
    _ => {
      child_stdin.close()
      trace(
        runtime_trace_event(
          "runtime", "process_spawn_rejected", "", "", "process_stdout_pipe_failed",
        ),
      )
      raise ReaderFailed
    }
  }
  let child = @process.spawn(
    group,
    command,
    args,
    extra_env?,
    inherit_env~,
    stdin=child_stdin_end,
    stdout=child_stdout_end,
    stderr=@stdio.stderr,
    no_wait=true,
  ) catch {
    _ => {
      child_stdin.close()
      child_stdout.close()
      trace(
        runtime_trace_event(
          "runtime", "process_spawn_rejected", "", "", "process_spawn_failed",
        ),
      )
      raise InvalidOptions
    }
  }
  {
    ports: {
      reader: { read: max_len => child_stdout.read_some(max_len~) },
      writer: { write: frame => child_stdin.write(frame) },
      handlers,
      cancel_outbound,
      trace,
    },
    child,
    child_stdin,
    child_stdout,
  }
}