> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hiroleague.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Hiro Server Components

> The main runtime components inside a Hiro workspace server process.

export const DiagramViewer = ({children, title = "Diagram"}) => {
  const dialogRef = useRef(null);
  const viewportRef = useRef(null);
  const contentRef = useRef(null);
  const [isOpen, setIsOpen] = useState(false);
  const [zoom, setZoom] = useState(1);
  const [pan, setPan] = useState({
    x: 0,
    y: 0
  });
  const zoomRef = useRef(1);
  const panRef = useRef({
    x: 0,
    y: 0
  });
  const dragRef = useRef(null);
  const open = () => {
    setZoom(1);
    setPan({
      x: 0,
      y: 0
    });
    setIsOpen(true);
  };
  const close = () => {
    setZoom(1);
    setPan({
      x: 0,
      y: 0
    });
    setIsOpen(false);
  };
  const resetView = () => {
    fitDiagramToViewport();
    setPan({
      x: 0,
      y: 0
    });
  };
  const setView = (nextZoom, nextPan) => {
    zoomRef.current = nextZoom;
    panRef.current = nextPan;
    setZoom(nextZoom);
    setPan(nextPan);
  };
  const MaximizeIcon = () => <svg aria-hidden="true" className="diagram-viewer__icon" viewBox="0 0 24 24">
      <path d="M8 3H3v5M16 3h5v5M8 21H3v-5M16 21h5v-5" />
    </svg>;
  const ResetIcon = () => <svg aria-hidden="true" className="diagram-viewer__icon" viewBox="0 0 24 24">
      <path d="M3 12a9 9 0 1 0 3-6.7L3 8" />
      <path d="M3 3v5h5" />
    </svg>;
  const CloseIcon = () => <svg aria-hidden="true" className="diagram-viewer__icon" viewBox="0 0 24 24">
      <path d="M18 6 6 18M6 6l12 12" />
    </svg>;
  const fitDiagramToViewport = () => {
    const viewport = viewportRef.current;
    const content = contentRef.current;
    const svg = content?.querySelector(".mermaid svg[viewBox]");
    if (!viewport || !svg) return;
    const viewBox = svg.getAttribute("viewBox")?.split(/\s+/).map(Number);
    if (!viewBox || viewBox.length !== 4 || !viewBox[2] || !viewBox[3]) return;
    const [, , width, height] = viewBox;
    svg.style.width = `${width}px`;
    svg.style.maxWidth = "none";
    const rect = viewport.getBoundingClientRect();
    const nextZoom = Math.min(2.2, Math.max(0.75, Number((Math.min((rect.width - 96) / width, (rect.height - 96) / height) * 0.98).toFixed(3))));
    setView(nextZoom, {
      x: 0,
      y: 0
    });
  };
  const handleWheel = event => {
    if (!isOpen) return;
    event.preventDefault();
    event.stopPropagation();
    const viewport = viewportRef.current;
    if (!viewport) return;
    const rect = viewport.getBoundingClientRect();
    const pointerX = event.clientX - rect.left - rect.width / 2;
    const pointerY = event.clientY - rect.top - rect.height / 2;
    const currentZoom = zoomRef.current;
    const currentPan = panRef.current;
    const nextZoom = Math.max(0.25, Math.min(5, Number((currentZoom * (event.deltaY < 0 ? 1.12 : 0.88)).toFixed(3))));
    const ratio = nextZoom / currentZoom;
    setView(nextZoom, {
      x: pointerX - (pointerX - currentPan.x) * ratio,
      y: pointerY - (pointerY - currentPan.y) * ratio
    });
  };
  const onPointerDown = event => {
    if (!isOpen || event.button !== 0) return;
    event.preventDefault();
    dragRef.current = {
      pointerId: event.pointerId,
      startX: event.clientX,
      startY: event.clientY,
      pan: panRef.current
    };
    try {
      event.currentTarget.setPointerCapture?.(event.pointerId);
    } catch {}
  };
  const onPointerMove = event => {
    const drag = dragRef.current;
    if (!isOpen || !drag || drag.pointerId !== event.pointerId) return;
    event.preventDefault();
    const nextPan = {
      x: drag.pan.x + event.clientX - drag.startX,
      y: drag.pan.y + event.clientY - drag.startY
    };
    panRef.current = nextPan;
    setPan(nextPan);
  };
  const endDrag = event => {
    if (dragRef.current?.pointerId === event.pointerId) {
      dragRef.current = null;
    }
  };
  useEffect(() => {
    const dialog = dialogRef.current;
    if (isOpen && dialog && !dialog.open) {
      dialog.showModal();
      requestAnimationFrame(() => {
        requestAnimationFrame(fitDiagramToViewport);
      });
    }
    if (!isOpen && dialog?.open) {
      dialog.close();
    }
  }, [isOpen]);
  useEffect(() => {
    if (!isOpen) return;
    const onKeyDown = event => {
      if (event.key === "Escape") close();
    };
    const onWheel = event => {
      event.preventDefault();
      event.stopPropagation();
      if (viewportRef.current?.contains(event.target)) {
        handleWheel(event);
      }
    };
    const previousOverflow = document.body.style.overflow;
    const previousHtmlOverflow = document.documentElement.style.overflow;
    document.body.style.overflow = "hidden";
    document.documentElement.style.overflow = "hidden";
    window.addEventListener("keydown", onKeyDown);
    window.addEventListener("wheel", onWheel, {
      capture: true,
      passive: false
    });
    return () => {
      document.body.style.overflow = previousOverflow;
      document.documentElement.style.overflow = previousHtmlOverflow;
      window.removeEventListener("keydown", onKeyDown);
      window.removeEventListener("wheel", onWheel, {
        capture: true
      });
    };
  }, [isOpen]);
  const toolbar = <div className="diagram-viewer__bar">
      <span className="diagram-viewer__title">{title}</span>
      <div className="diagram-viewer__actions">
        {isOpen && <span className="diagram-viewer__zoom" aria-live="polite">
            {Math.round(zoom * 100)}%
          </span>}
        {isOpen && <button className="diagram-viewer__button" type="button" onClick={resetView} aria-label="Reset diagram">
            <ResetIcon />
          </button>}
        <button className="diagram-viewer__button" type="button" onClick={isOpen ? close : open} aria-label={isOpen ? "Close" : "Maximize"}>
          {isOpen ? <CloseIcon /> : <MaximizeIcon />}
        </button>
      </div>
    </div>;
  const diagram = <div ref={viewportRef} className="diagram-viewer__viewport" onPointerDown={onPointerDown} onPointerMove={onPointerMove} onPointerUp={endDrag} onPointerCancel={endDrag}>
      <div className="diagram-viewer__stage" style={{
    transform: isOpen ? `translate(${pan.x}px, ${pan.y}px)` : undefined
  }}>
        <div className="diagram-viewer__centerer">
          <div ref={contentRef} className="diagram-viewer__content" style={{
    transform: isOpen ? `scale(${zoom})` : "none"
  }}>
            {children}
          </div>
        </div>
      </div>
    </div>;
  return <>
      {!isOpen && <div className="diagram-viewer not-prose">
          {toolbar}
          {diagram}
        </div>}

      <dialog ref={dialogRef} className="diagram-viewer-dialog" onWheel={event => {
    if (event.target === event.currentTarget) {
      event.preventDefault();
      event.stopPropagation();
    }
  }} onCancel={event => {
    event.preventDefault();
    close();
  }} onClose={() => setIsOpen(false)}>
        {isOpen && <div className="diagram-viewer diagram-viewer--open not-prose">
            {toolbar}
            {diagram}
          </div>}
      </dialog>
    </>;
};

Hiro Server runs on the **Root Node**. Each running workspace has one server process.

The server process is the runtime container for the workspace. It loads workspace configuration, prepares shared runtime state, wires the main components together, then runs them in one asyncio event loop.

The main components are active services. Shared objects such as `ServerContext` are not components in this sense. They are runtime context passed into the components.

<DiagramViewer title="Hiro Server process components">
  ```mermaid actions={false} theme={null}
  flowchart LR
      CLI["hiro CLI"]
      ControlRoom["Control Room"]
      DeviceNode["Device node"]
      Gateway["Hiro Gate<br/>(Gateway)"]
      ChannelPlugin["Channel plugin<br/>(devices, etc.)"]

      subgraph ServerProcess["Hiro Server process"]
          HTTP["HTTP Server"]
          ChannelManager["Channel Manager"]
          CommunicationManager["Communication Manager"]
          AgentManager["Agent Manager"]
          MetricsCollector["Metrics Collector"]
          AdminUI["Admin UI<br/>(optional)"]
      end

      CLI --> HTTP
      ControlRoom --> AdminUI

      DeviceNode <--> Gateway
      Gateway <--> ChannelPlugin
      ChannelPlugin <--> ChannelManager

      ChannelManager -->|"inbound messages<br/>and channel events"| CommunicationManager
      CommunicationManager -->|"outbound messages"| ChannelManager

      CommunicationManager -->|"adapted inbound queue"| AgentManager
      AgentManager -->|"assistant replies<br/>and voice events"| CommunicationManager

      HTTP -. "status, tools,<br/>characters, metrics" .-> CommunicationManager
      HTTP -. "channel status" .-> ChannelManager
      HTTP -. "metrics API" .-> MetricsCollector
      AdminUI -. "inspect and control" .-> MetricsCollector
      MetricsCollector -. "child process metrics" .-> ChannelManager
  ```
</DiagramViewer>

## Channel Manager

The Channel Manager owns the boundary between Hiro Server and channel plugins.

It starts enabled channel plugins as subprocesses, accepts their local WebSocket connections, and routes plugin messages into the server. The mandatory `devices` channel plugin bridges device nodes through Hiro Gate.

**Inputs:** Plugin registration, inbound `UnifiedMessage` payloads, channel events, and outbound messages from the Communication Manager.

**Outputs:** Plugin configuration, outbound `channel.send` messages, channel events, and stop signals during shutdown.

## Communication Manager

The Communication Manager is the internal message router.

It sits between the Channel Manager and the Agent Manager. It validates inbound messages, routes requests and events, runs message adaptation, queues agent-ready messages, and sends outbound messages back to channels.

**Inputs:** Raw inbound message dictionaries from the Channel Manager, plus outbound `UnifiedMessage` objects from the Agent Manager and other server services.

**Outputs:** Adapted messages on the Agent Manager inbound queue, request responses, events such as `message.received`, and outbound messages to the Channel Manager.

## Agent Manager

The Agent Manager runs the assistant side of Hiro Server.

It consumes prepared inbound messages from the Communication Manager, resolves the conversation and character, invokes the LLM agent, and sends replies back through the Communication Manager.

**Inputs:** Agent-ready `UnifiedMessage` objects, workspace preferences, character configuration, conversation history, and available tools.

**Outputs:** Text replies, saved conversation messages, and optional `message.voiced` events when voice replies are available.

## HTTP Server

The HTTP Server is the local control API for the running workspace server.

It fits beside the message pipeline, not inside it. Local clients use it to inspect live server state, control server lifecycle, read local character/profile data, inspect channels and metrics, and access the workspace Tool Registry over HTTP.

**Inputs:** Local HTTP requests for status, channels, tool schemas, tool invocation, character profiles and photos, metrics, shutdown, and restart.

**Outputs:** JSON status and inspection responses, registered tool schemas, tool execution results, character profile/photo responses, metrics snapshots, and lifecycle signals such as shutdown or restart.

## Admin UI

The Admin UI is a local Control Room for a Hiro League. It runs on the Root Node and gives a browser interface for managing workspaces, gateways, LLM Providers, characters, channels, device pairing, logs, metrics and more.

## Metrics Collector

The Metrics Collector observes runtime health.

It runs beside the main pipeline. It samples the server process, system resources, and channel plugin subprocesses.

**Inputs:** Timer ticks, process stats, and child process information from the Channel Manager.

**Outputs:** Latest metrics, metrics history, and live data for the HTTP Server and Control Room.
