multica/docs/rpc.md
Naiyuan Qing 1f22c3e578 feat(hub): register RPC handlers for hub, agent, and gateway operations
Add 5 new RPC methods (getHubInfo, listAgents, createAgent, deleteAgent,
updateGateway) mirroring the existing Console HTTP API, enabling pure
WebSocket communication from the frontend.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 15:27:07 +08:00

9.1 KiB

Hub RPC Protocol

The Hub exposes an RPC (Remote Procedure Call) interface over the Gateway WebSocket transport. Clients can invoke methods on the Hub and receive structured responses, all routed through the same Gateway message layer used for regular chat.

Architecture Overview

Client (SDK)                   Gateway (WebSocket)              Hub
    |                               |                            |
    |-- send(RequestAction) ------->|-- route to Hub ----------->|
    |                               |                            |-- dispatch(method, params)
    |                               |                            |-- handler executes
    |<-- receive(ResponseAction) ---|<-- route to Client --------|
    |                               |                            |
  1. The Client calls client.request(hubDeviceId, method, params).
  2. The SDK generates a requestId (UUIDv7), wraps it into a RequestPayload, and sends a message with action = "request" to the Hub via the Gateway.
  3. The Gateway routes the message to the Hub's socket (standard device-to-device routing).
  4. The Hub detects action === "request" in its onMessage handler and delegates to RpcDispatcher.dispatch().
  5. The dispatcher looks up the registered handler for the given method and invokes it.
  6. The Hub sends back a message with action = "response" containing either a success or error payload, addressed to the original sender.
  7. The Client SDK intercepts incoming "response" messages in its RECEIVE listener, matches by requestId, and resolves (or rejects) the corresponding Promise.

Message Format

All RPC messages use the standard RoutedMessage envelope:

interface RoutedMessage<T> {
  id: string;       // UUIDv7 message ID
  uid: string | null;
  from: string;     // sender deviceId
  to: string;       // recipient deviceId
  action: string;   // "request" or "response"
  payload: T;
}

Request Payload

interface RequestPayload<T = unknown> {
  requestId: string;  // UUIDv7, generated by the SDK
  method: string;     // RPC method name
  params?: T;         // method-specific parameters
}

Response Payload (Success)

interface ResponseSuccessPayload<T = unknown> {
  requestId: string;  // matches the request
  ok: true;
  payload: T;         // method-specific result
}

Response Payload (Error)

interface ResponseErrorPayload {
  requestId: string;  // matches the request
  ok: false;
  error: {
    code: string;     // machine-readable error code
    message: string;  // human-readable description
  };
}

Error Codes

Code Description
METHOD_NOT_FOUND The requested RPC method does not exist.
INVALID_PARAMS Missing or malformed parameters.
AGENT_NOT_FOUND No session file found for the given agent ID.
RPC_ERROR Catch-all for unexpected errors.

Client SDK Usage

The GatewayClient provides a request() method that handles the full request/response lifecycle:

request<T = unknown>(
  to: string,           // target deviceId (Hub's deviceId)
  method: string,       // RPC method name
  params?: unknown,     // method parameters
  timeout?: number,     // timeout in ms (default: 10000)
): Promise<T>

The method:

  • Generates a requestId internally.
  • Sends a RequestPayload via the Gateway.
  • Returns a Promise that resolves with the response payload on success, or rejects with an Error on failure or timeout.
  • Automatically cleans up pending requests on disconnect.

Example

import { GatewayClient, type GetAgentMessagesResult } from "@multica/sdk";

const client = new GatewayClient({
  url: "http://localhost:3000",
  deviceId: "my-client",
  deviceType: "client",
});

client.connect();

client.onRegistered(async () => {
  try {
    const result = await client.request<GetAgentMessagesResult>(
      "hub-device-id",
      "getAgentMessages",
      { agentId: "019abc12-...", offset: 0, limit: 20 },
    );
    console.log(`Total: ${result.total}, returned: ${result.messages.length}`);
  } catch (err) {
    console.error("RPC failed:", err.message);
  }
});

Available RPC Methods

getAgentMessages

Retrieves the message history for a given agent session. Works for both active and closed agents as long as the session file exists on disk.

Parameters:

interface GetAgentMessagesParams {
  agentId: string;    // required - the agent/session ID
  offset?: number;    // starting index (default: 0)
  limit?: number;     // max messages to return (default: 50)
}

Response:

interface GetAgentMessagesResult {
  messages: AgentMessage[];  // array of messages
  total: number;             // total message count in the session
  offset: number;            // the offset used
  limit: number;             // the limit used
}

Each AgentMessage in the array is one of:

  • UserMessage (role: "user") - User input (text or multimodal content).
  • AssistantMessage (role: "assistant") - LLM response, may contain TextContent, ThinkingContent, or ToolCall blocks. Includes usage (token counts and costs), model, provider, and stopReason.
  • ToolResultMessage (role: "toolResult") - Result of a tool invocation, with toolCallId, toolName, content, and isError.

Example request:

const result = await client.request<GetAgentMessagesResult>(
  hubDeviceId,
  "getAgentMessages",
  { agentId: "019abc12-3def-7000-8000-000000000001", offset: 0, limit: 10 },
);

Example success response payload:

{
  "requestId": "019abc12-...",
  "ok": true,
  "payload": {
    "messages": [
      { "role": "user", "content": "Hello", "timestamp": 1700000000000 },
      {
        "role": "assistant",
        "content": [{ "type": "text", "text": "Hi! How can I help?" }],
        "model": "claude-sonnet-4-20250514",
        "provider": "anthropic",
        "usage": { "input": 10, "output": 15, "totalTokens": 25 },
        "stopReason": "end_turn",
        "timestamp": 1700000001000
      }
    ],
    "total": 42,
    "offset": 0,
    "limit": 10
  }
}

Example error response payload:

{
  "requestId": "019abc12-...",
  "ok": false,
  "error": {
    "code": "AGENT_NOT_FOUND",
    "message": "No session found for agent: 019abc12-bad-id"
  }
}

getHubInfo

Returns Hub status information. No parameters required.

Response:

interface GetHubInfoResult {
  hubId: string;          // Hub device ID
  url: string;            // Current Gateway URL
  connectionState: string; // "disconnected" | "connecting" | "connected" | "registered"
  agentCount: number;     // Number of active agents
}

Example:

const info = await client.request<GetHubInfoResult>(hubDeviceId, "getHubInfo");

listAgents

Lists all active agents. No parameters required.

Response:

interface ListAgentsResult {
  agents: { id: string; closed: boolean }[];
}

Example:

const result = await client.request<ListAgentsResult>(hubDeviceId, "listAgents");

createAgent

Creates a new agent or restores an existing one.

Parameters:

interface CreateAgentParams {
  id?: string;  // optional - reuse existing session ID
}

Response:

interface CreateAgentResult {
  id: string;   // the created/restored agent session ID
}

Example:

const result = await client.request<CreateAgentResult>(hubDeviceId, "createAgent");
// or with specific ID:
const result = await client.request<CreateAgentResult>(hubDeviceId, "createAgent", { id: "existing-id" });

deleteAgent

Closes and removes an agent.

Parameters:

interface DeleteAgentParams {
  id: string;   // required - agent ID to delete
}

Response:

interface DeleteAgentResult {
  ok: boolean;  // true if agent was found and deleted
}

Example:

const result = await client.request<DeleteAgentResult>(hubDeviceId, "deleteAgent", { id: "019abc12-..." });

updateGateway

Reconnects the Hub to a different Gateway URL.

Parameters:

interface UpdateGatewayParams {
  url: string;  // required - new Gateway URL
}

Response:

interface UpdateGatewayResult {
  url: string;            // the new URL
  connectionState: string; // connection state after reconnect
}

Example:

const result = await client.request<UpdateGatewayResult>(hubDeviceId, "updateGateway", { url: "http://localhost:4000" });

Adding New RPC Methods

  1. Create a handler file in src/hub/rpc/handlers/:
// src/hub/rpc/handlers/my-method.ts
import { RpcError, type RpcHandler } from "../dispatcher.js";

export function createMyMethodHandler(): RpcHandler {
  return (params: unknown) => {
    if (!params || typeof params !== "object") {
      throw new RpcError("INVALID_PARAMS", "params must be an object");
    }
    // ... validate and handle
    return { /* result */ };
  };
}
  1. Register it in src/hub/hub.ts constructor:
this.rpc.register("myMethod", createMyMethodHandler());
  1. (Optional) Add typed params/result interfaces in packages/sdk/src/actions/rpc.ts and export them from packages/sdk/src/actions/index.ts for client-side type safety.