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>
365 lines
9.1 KiB
Markdown
365 lines
9.1 KiB
Markdown
# 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:
|
|
|
|
```ts
|
|
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
|
|
|
|
```ts
|
|
interface RequestPayload<T = unknown> {
|
|
requestId: string; // UUIDv7, generated by the SDK
|
|
method: string; // RPC method name
|
|
params?: T; // method-specific parameters
|
|
}
|
|
```
|
|
|
|
### Response Payload (Success)
|
|
|
|
```ts
|
|
interface ResponseSuccessPayload<T = unknown> {
|
|
requestId: string; // matches the request
|
|
ok: true;
|
|
payload: T; // method-specific result
|
|
}
|
|
```
|
|
|
|
### Response Payload (Error)
|
|
|
|
```ts
|
|
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:
|
|
|
|
```ts
|
|
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
|
|
|
|
```ts
|
|
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:**
|
|
|
|
```ts
|
|
interface GetAgentMessagesParams {
|
|
agentId: string; // required - the agent/session ID
|
|
offset?: number; // starting index (default: 0)
|
|
limit?: number; // max messages to return (default: 50)
|
|
}
|
|
```
|
|
|
|
**Response:**
|
|
|
|
```ts
|
|
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:**
|
|
|
|
```ts
|
|
const result = await client.request<GetAgentMessagesResult>(
|
|
hubDeviceId,
|
|
"getAgentMessages",
|
|
{ agentId: "019abc12-3def-7000-8000-000000000001", offset: 0, limit: 10 },
|
|
);
|
|
```
|
|
|
|
**Example success response payload:**
|
|
|
|
```json
|
|
{
|
|
"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:**
|
|
|
|
```json
|
|
{
|
|
"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:**
|
|
|
|
```ts
|
|
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:**
|
|
|
|
```ts
|
|
const info = await client.request<GetHubInfoResult>(hubDeviceId, "getHubInfo");
|
|
```
|
|
|
|
---
|
|
|
|
### `listAgents`
|
|
|
|
Lists all active agents. No parameters required.
|
|
|
|
**Response:**
|
|
|
|
```ts
|
|
interface ListAgentsResult {
|
|
agents: { id: string; closed: boolean }[];
|
|
}
|
|
```
|
|
|
|
**Example:**
|
|
|
|
```ts
|
|
const result = await client.request<ListAgentsResult>(hubDeviceId, "listAgents");
|
|
```
|
|
|
|
---
|
|
|
|
### `createAgent`
|
|
|
|
Creates a new agent or restores an existing one.
|
|
|
|
**Parameters:**
|
|
|
|
```ts
|
|
interface CreateAgentParams {
|
|
id?: string; // optional - reuse existing session ID
|
|
}
|
|
```
|
|
|
|
**Response:**
|
|
|
|
```ts
|
|
interface CreateAgentResult {
|
|
id: string; // the created/restored agent session ID
|
|
}
|
|
```
|
|
|
|
**Example:**
|
|
|
|
```ts
|
|
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:**
|
|
|
|
```ts
|
|
interface DeleteAgentParams {
|
|
id: string; // required - agent ID to delete
|
|
}
|
|
```
|
|
|
|
**Response:**
|
|
|
|
```ts
|
|
interface DeleteAgentResult {
|
|
ok: boolean; // true if agent was found and deleted
|
|
}
|
|
```
|
|
|
|
**Example:**
|
|
|
|
```ts
|
|
const result = await client.request<DeleteAgentResult>(hubDeviceId, "deleteAgent", { id: "019abc12-..." });
|
|
```
|
|
|
|
---
|
|
|
|
### `updateGateway`
|
|
|
|
Reconnects the Hub to a different Gateway URL.
|
|
|
|
**Parameters:**
|
|
|
|
```ts
|
|
interface UpdateGatewayParams {
|
|
url: string; // required - new Gateway URL
|
|
}
|
|
```
|
|
|
|
**Response:**
|
|
|
|
```ts
|
|
interface UpdateGatewayResult {
|
|
url: string; // the new URL
|
|
connectionState: string; // connection state after reconnect
|
|
}
|
|
```
|
|
|
|
**Example:**
|
|
|
|
```ts
|
|
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/`:
|
|
|
|
```ts
|
|
// 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 */ };
|
|
};
|
|
}
|
|
```
|
|
|
|
2. Register it in `src/hub/hub.ts` constructor:
|
|
|
|
```ts
|
|
this.rpc.register("myMethod", createMyMethodHandler());
|
|
```
|
|
|
|
3. (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.
|