> ## 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.

# Agent Manager

> The Hiro Server component that turns agent-ready messages into assistant replies.

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>
    </>;
};

Agent Manager is the assistant runtime inside Hiro Server.

Communication Manager prepares inbound messages and places them on `inbound_queue`. Agent Manager consumes that queue, resolves the conversation and character, builds the agent input, invokes the LLM agent with tools and memory, then sends replies back through Communication Manager.

The component is the link between Hiro's message pipeline and the LLM runtime.

<DiagramViewer title="Agent Manager Architecture">
  ```mermaid actions={false} theme={null}
  flowchart TB
      subgraph InputSide["Input"]
          direction TB
          InboundQueue["Communication Manager<br/>inbound_queue"]
          PreparedMessage["Agent-ready<br/>UnifiedMessage"]
      end

      subgraph AgentManager["Agent Manager"]
          direction TB
          Worker["Queue Worker"]

          subgraph Preparation["Message Preparation"]
              direction TB
              ThreadResolver["Thread + Character Resolver"]
              InputBuilder["Agent Input Builder"]
              VoiceInputPolicy["Voice Input Policy"]
          end

          subgraph Runtime["Agent Runtime"]
              direction TB
              AgentCache["Compiled Agent Cache"]
              LangGraph["LangChain / LangGraph Agent"]
              SummaryGraph["Summarization Graph"]
              Tools["Tool Registry"]
          end

          subgraph Output["Reply Output"]
              direction TB
              ReplyPipeline["Reply Pipeline"]
              VoicePipeline["Voice Reply Pipeline"]
          end
      end

      subgraph RuntimeState["Workspace Runtime State"]
          direction TB
          Preferences["Workspace Preferences"]
          Characters["Characters"]
          Credentials["Credential Store"]
          WorkspaceDB["workspace.db<br/>checkpoints + messages"]
      end

      subgraph OutputSide["Output"]
          direction TB
          OutboundQueue["Communication Manager<br/>outbound queue"]
          TextReply["Text reply"]
          VoicedEvent["message.voiced event"]
      end

      InboundQueue --> PreparedMessage
      PreparedMessage --> Worker
      Worker --> ThreadResolver
      Worker --> InputBuilder
      Worker --> VoiceInputPolicy
      ThreadResolver --> Characters
      ThreadResolver --> WorkspaceDB
      InputBuilder --> LangGraph
      Preferences --> AgentCache
      Characters --> AgentCache
      Credentials --> AgentCache
      Tools --> LangGraph
      AgentCache --> LangGraph
      SummaryGraph --> LangGraph
      WorkspaceDB --> LangGraph
      LangGraph --> ReplyPipeline
      ReplyPipeline --> TextReply
      ReplyPipeline --> WorkspaceDB
      TextReply --> OutboundQueue
      ReplyPipeline --> VoicePipeline
      VoicePipeline --> VoicedEvent
      VoicedEvent --> OutboundQueue

      style AgentManager fill:#EF444455
  ```
</DiagramViewer>

## Responsibilities

**Queue consumption** - Agent Manager runs as a long-lived worker. It waits on `CommunicationManager.inbound_queue`, processes one prepared `UnifiedMessage` at a time, and always marks the queue item done.

**Conversation resolution** - For each message, it maps `routing.channel` and `routing.sender_id` to a conversation channel row. That row supplies the memory thread ID, database channel ID, and character ID.

**Character and model resolution** - It loads the conversation character, builds the effective system prompt, resolves the chat model from character preferences and workspace defaults, and reuses a workspace credential store.

**Agent input preparation** - It builds a text input from direct text content plus adapter-provided descriptions on non-text content. Audio transcripts and image descriptions enter the LLM here.

**Agent execution** - It invokes a LangChain or LangGraph agent with Hiro tools, persistent conversation memory, and optional summarization.

**Reply delivery** - It converts provider output into a Hiro text reply, saves the reply to `data.db`, and enqueues the reply through Communication Manager.

**Voice reply delivery** - When the device requests a voice reply and policy allows it, Agent Manager synthesizes speech after the text reply has already been queued, then emits a `message.voiced` event.

***

## Main components

### **`AgentManager`**

The worker and per-message orchestrator in `runtime/agent_manager.py`.

It owns the run loop, agent cache, checkpointer lifecycle, credential store reuse, per-message processing, reply construction, and optional TTS scheduling.

### **Queue worker**

The `run()` coroutine opens the LangGraph SQLite checkpointer against `workspace.db`, creates a workspace `CredentialStore`, clears the compiled agent cache, then drains `CommunicationManager.inbound_queue`.

The queue boundary is important. Communication Manager owns message validation, adaptation, transcript events, and inbound persistence. Agent Manager starts only after a message is agent-ready.

### **Message preparation**

Message preparation converts a `UnifiedMessage` into one agent input string.

| Content source             | How it becomes agent input                                                                  |
| -------------------------- | ------------------------------------------------------------------------------------------- |
| Text content item          | Uses the item body directly.                                                                |
| Audio content item         | Uses `metadata.description`, produced by the audio adapter.                                 |
| Image content item         | Uses `metadata.description`, produced by the image adapter, prefixed with the content type. |
| Unsupported or failed item | Logged and skipped.                                                                         |

If no usable text remains, the message is dropped for agent purposes.

### **Thread and character resolver**

The resolver uses `ConversationChannelGetTool` with the canonical channel name:

```text theme={null}
{routing.channel}:{routing.sender_id}
```

The conversation channel row provides:

| Value          | Purpose                                                        |
| -------------- | -------------------------------------------------------------- |
| `id`           | LangGraph thread ID and message-store channel ID.              |
| `character_id` | Character profile and model preferences for this conversation. |

If the row has no character ID, Agent Manager falls back to the workspace default character.

### **Agent runtime factory**

Agent Manager compiles the actual LLM runtime from:

| Input                   | Role                                            |
| ----------------------- | ----------------------------------------------- |
| Character system prompt | Persona and behavior instructions.              |
| Resolved chat model     | Provider/model/temperature/token configuration. |
| Hiro tool registry      | Tool set exposed to the agent.                  |
| LangGraph checkpointer  | Persistent per-thread memory.                   |
| Memory preferences      | Whether to use summarization.                   |

Compiled agents are cached by model settings, summarization settings, and system prompt hash. This avoids rebuilding the graph for every message while still separating distinct character/model configurations.

### **Memory and summarization**

Conversation memory is backed by `AsyncSqliteSaver` in `workspace.db`.

When summarization is disabled, Agent Manager uses LangChain's standard `create_agent()` with the shared checkpointer. When summarization is enabled and a summarization model is available, it builds the custom graph in `summarizing_agent_graph.py`.

That graph runs:

```text theme={null}
summarize -> call_model -> tools -> summarize -> call_model
```

The full message history remains in the checkpoint. The model receives the summarized working context when the configured token threshold is crossed.

### **Reply pipeline**

The reply pipeline normalizes provider-native content into plain text, constructs an outbound `UnifiedMessage`, saves the reply to the message store, and enqueues it through Communication Manager.

If the LLM call fails, Agent Manager still sends a human-readable fallback reply. If saving the reply fails, delivery still proceeds.

### **Voice reply pipeline**

Voice output is a post-reply side effect.

The text reply is queued first. If `routing.metadata.request_voice_reply` is true and workspace/character voice settings resolve to a usable TTS model, Agent Manager starts a detached TTS task. The task emits `message.voiced` with base64 audio, MIME type, duration, and a `ref_id` pointing to the text reply.

***

## Processing flow

<DiagramViewer title="Agent message processing">
  ```mermaid actions={false} theme={null}
  sequenceDiagram
      participant Device as Device
      participant Gate as Hiro Gate
      participant Plugin as devices channel plugin
      participant Channel as Channel Manager
      participant Comm as Communication Manager
      participant Adapter as Adapter Pipeline
      participant Agent as Agent Manager
      participant DB as workspace.db / data.db
      participant Runtime as Agent Runtime
      participant LLM as LLM Provider
      participant Tools as Hiro Tools
      participant TTS as TTS Service

      Device->>Gate: User message
      Gate->>Plugin: Gateway delivery
      Plugin->>Channel: channel.receive(UnifiedMessage)
      Channel->>Comm: on_message(raw dict)
      Comm->>Comm: Validate and route by message_type
      Comm-->>Channel: enqueue message.received
      Channel-->>Plugin: channel.send(message.received)
      Plugin-->>Gate: Delivery ack
      Gate-->>Device: Message delivered

      Comm->>Adapter: Adapt message content
      Adapter-->>Comm: Enriched UnifiedMessage
      opt Audio transcript available
          Comm-->>Channel: enqueue message.transcribed
          Channel-->>Plugin: channel.send(message.transcribed)
          Plugin-->>Gate: Transcript event
          Gate-->>Device: Show transcript
      end

      Comm->>DB: Persist inbound message
      Comm->>Agent: inbound_queue.put(agent-ready message)
      Agent->>DB: Resolve conversation channel and memory thread
      Agent->>Agent: Load character, preferences, credentials
      Agent->>Agent: Build agent input from text + descriptions
      Agent->>Runtime: Get or build compiled agent
      Runtime->>DB: Load checkpointed conversation state
      Runtime->>LLM: Invoke chat model
      opt Tool call requested
          LLM->>Runtime: Tool call
          Runtime->>Tools: Execute tool
          Tools-->>Runtime: Tool result
          Runtime->>LLM: Continue with tool result
      end
      LLM-->>Runtime: Assistant response
      Runtime-->>Agent: Final reply content
      Agent->>Agent: Normalize provider output
      Agent->>DB: Save outbound reply
      Agent->>Comm: enqueue_outbound(text reply)
      Comm->>Channel: send_to_channel(text reply)
      Channel->>Plugin: channel.send(text reply)
      Plugin->>Gate: Reply delivery
      Gate->>Device: Show assistant reply

      opt Voice reply requested and allowed
          Agent->>TTS: Synthesize reply audio
          TTS-->>Agent: Audio bytes + metadata
          Agent->>Comm: enqueue_outbound(message.voiced)
          Comm->>Channel: send_to_channel(message.voiced)
          Channel->>Plugin: channel.send(message.voiced)
          Plugin->>Gate: Voice event delivery
          Gate->>Device: Attach audio playback to reply
      end
  ```
</DiagramViewer>

***

## Related components

### **Communication Manager**

Communication Manager is Agent Manager's input and output boundary. It supplies agent-ready messages through `inbound_queue` and accepts replies through `enqueue_outbound()`.

### **Message Adapter Pipeline**

The adapter pipeline prepares non-text content before Agent Manager sees it. Agent Manager reads adapter output from `metadata.description`.

### **Character and preferences domain**

Characters define the system prompt and optional model preferences. Workspace preferences define default chat, summarization, STT, and TTS behavior.

### **Tool registry**

Agent Manager exposes Hiro tools to the LangChain or LangGraph agent. The same tool implementations are shared with CLI and server surfaces.

### **Message store**

Inbound persistence happens before Agent Manager through Communication Manager's post-adapt hooks. Outbound agent replies are saved by Agent Manager after reply construction.

***

## See also

<CardGroup cols={2}>
  <Card title="Communication Manager" icon="arrows-left-right" href="/architecture/concepts/communication-manager">
    The message router that prepares inbound messages and dispatches replies.
  </Card>

  <Card title="Hiro Server components" icon="server" href="/architecture/concepts/hiro-server-components">
    How the main server components relate to each other.
  </Card>

  <Card title="Channel Manager" icon="plug" href="/architecture/concepts/channel-manager">
    The transport manager that owns channel plugins and WebSocket JSON-RPC.
  </Card>
</CardGroup>
