365 lines
10 KiB
TypeScript
365 lines
10 KiB
TypeScript
// Gateway protocol types — JSON-RPC-like messages over WebSocket.
|
|
|
|
// ── Client → Server ────────────────────────────────────────────
|
|
|
|
export interface GatewayRequest {
|
|
id: number;
|
|
method: string;
|
|
params?: Record<string, unknown>;
|
|
}
|
|
|
|
export const GATEWAY_PROTOCOL_VERSION = 1;
|
|
|
|
export interface NodeRegisterParams {
|
|
connectionId: string;
|
|
nodeId: string;
|
|
role: string;
|
|
protocolVersion: number;
|
|
capabilities: string[];
|
|
}
|
|
|
|
export interface NodeLocationSetParams {
|
|
connectionId: string;
|
|
latitude: number;
|
|
longitude: number;
|
|
accuracyMeters?: number;
|
|
altitudeMeters?: number;
|
|
headingDegrees?: number;
|
|
speedMps?: number;
|
|
source?: 'gps' | 'network' | 'manual' | 'unknown';
|
|
capturedAt?: number;
|
|
}
|
|
|
|
export interface NodeLocationGetParams {
|
|
connectionId: string;
|
|
}
|
|
|
|
export interface NodeStatusSetParams {
|
|
connectionId: string;
|
|
platform: 'macos' | 'ios' | 'android' | 'linux' | 'windows' | 'unknown';
|
|
appVersion?: string;
|
|
deviceName?: string;
|
|
statusText?: string;
|
|
batteryPct?: number;
|
|
powerSource?: 'ac' | 'battery' | 'unknown';
|
|
}
|
|
|
|
export interface NodePushTokenSetParams {
|
|
connectionId: string;
|
|
provider: 'apns' | 'fcm';
|
|
token: string;
|
|
topic?: string;
|
|
environment?: 'sandbox' | 'production';
|
|
}
|
|
|
|
// ── Server → Client ────────────────────────────────────────────
|
|
|
|
export interface GatewayResponse {
|
|
id: number;
|
|
result: unknown;
|
|
}
|
|
|
|
export interface GatewayError {
|
|
id: number;
|
|
error: {
|
|
code: ErrorCode;
|
|
message: string;
|
|
};
|
|
}
|
|
|
|
export interface GatewayEvent {
|
|
id: number;
|
|
event: EventType;
|
|
data: unknown;
|
|
}
|
|
|
|
// ── Attachment data for gateway protocol messages ───────────────
|
|
|
|
/** Attachment data sent in agent.send params or emitted as events. */
|
|
export interface GatewayAttachment {
|
|
/** MIME type (e.g. "image/jpeg", "audio/ogg") */
|
|
mimeType: string;
|
|
/** Base64-encoded data */
|
|
data?: string;
|
|
/** URL to the resource */
|
|
url?: string;
|
|
/** Filename hint */
|
|
filename?: string;
|
|
}
|
|
|
|
// ── Event types emitted during agent.send ──────────────────────
|
|
|
|
export type EventType =
|
|
| 'content'
|
|
| 'tool_start'
|
|
| 'tool_end'
|
|
| 'context_warning'
|
|
| 'attachment'
|
|
| 'done'
|
|
| 'error';
|
|
|
|
export interface ContentEventData {
|
|
text: string;
|
|
}
|
|
|
|
export interface ToolStartEventData {
|
|
tool: string;
|
|
args: unknown;
|
|
}
|
|
|
|
export interface ToolEndEventData {
|
|
tool: string;
|
|
result: {
|
|
success: boolean;
|
|
output: string;
|
|
error?: string;
|
|
};
|
|
}
|
|
|
|
export interface ContextWarningEventData {
|
|
level: 'warning' | 'checkpoint' | 'critical';
|
|
message: string;
|
|
budget: {
|
|
estimatedTokens: number;
|
|
contextWindow: number;
|
|
remainingTokens: number;
|
|
usagePct: number;
|
|
thresholdPct: number;
|
|
thresholdTokens: number;
|
|
shouldCompact: boolean;
|
|
};
|
|
actions: {
|
|
checkpointSaved: boolean;
|
|
autoCompacted: boolean;
|
|
checkpointNamespace?: string;
|
|
};
|
|
}
|
|
|
|
export interface AttachmentEventData {
|
|
mimeType: string;
|
|
data?: string;
|
|
url?: string;
|
|
filename?: string;
|
|
}
|
|
|
|
export interface DoneEventData {
|
|
content: string;
|
|
}
|
|
|
|
export interface ErrorEventData {
|
|
code: ErrorCode;
|
|
message: string;
|
|
queue?: Record<string, unknown>;
|
|
}
|
|
|
|
// ── Error codes ────────────────────────────────────────────────
|
|
|
|
export enum ErrorCode {
|
|
ParseError = -1,
|
|
InvalidRequest = -2,
|
|
MethodNotFound = -3,
|
|
AuthRequired = -4,
|
|
AuthFailed = -5,
|
|
SessionNotFound = 1,
|
|
ToolNotFound = 2,
|
|
AgentBusy = 3,
|
|
RequestCancelled = 4,
|
|
InternalError = 5,
|
|
}
|
|
|
|
// ── Outbound message (union of all server → client types) ──────
|
|
|
|
export type OutboundMessage = GatewayResponse | GatewayError | GatewayEvent;
|
|
|
|
// ── Validation helpers ─────────────────────────────────────────
|
|
|
|
export function isValidRequest(msg: unknown): msg is GatewayRequest {
|
|
if (typeof msg !== 'object' || msg === null) {return false;}
|
|
const obj = msg as Record<string, unknown>;
|
|
return (
|
|
typeof obj.id === 'number' &&
|
|
typeof obj.method === 'string' &&
|
|
(obj.params === undefined || (typeof obj.params === 'object' && obj.params !== null))
|
|
);
|
|
}
|
|
|
|
export function parseMessage(raw: string): GatewayRequest | null {
|
|
try {
|
|
const parsed = JSON.parse(raw);
|
|
if (isValidRequest(parsed)) {return parsed;}
|
|
return null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function parseNodeRegisterParams(params: unknown): NodeRegisterParams | null {
|
|
if (!params || typeof params !== 'object') {
|
|
return null;
|
|
}
|
|
const p = params as Record<string, unknown>;
|
|
if (typeof p.connectionId !== 'string' || !p.connectionId.trim()) {
|
|
return null;
|
|
}
|
|
if (typeof p.nodeId !== 'string' || !p.nodeId.trim()) {
|
|
return null;
|
|
}
|
|
if (typeof p.role !== 'string' || !p.role.trim()) {
|
|
return null;
|
|
}
|
|
if (typeof p.protocolVersion !== 'number' || !Number.isFinite(p.protocolVersion) || p.protocolVersion < 1) {
|
|
return null;
|
|
}
|
|
const capabilitiesRaw = p.capabilities;
|
|
if (!Array.isArray(capabilitiesRaw) || !capabilitiesRaw.every((entry) => typeof entry === 'string')) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
connectionId: p.connectionId,
|
|
nodeId: p.nodeId,
|
|
role: p.role,
|
|
protocolVersion: Math.floor(p.protocolVersion),
|
|
capabilities: capabilitiesRaw,
|
|
};
|
|
}
|
|
|
|
export function parseNodeLocationSetParams(params: unknown): NodeLocationSetParams | null {
|
|
if (!params || typeof params !== 'object') {
|
|
return null;
|
|
}
|
|
const p = params as Record<string, unknown>;
|
|
if (typeof p.connectionId !== 'string' || !p.connectionId.trim()) {
|
|
return null;
|
|
}
|
|
if (typeof p.latitude !== 'number' || !Number.isFinite(p.latitude) || p.latitude < -90 || p.latitude > 90) {
|
|
return null;
|
|
}
|
|
if (typeof p.longitude !== 'number' || !Number.isFinite(p.longitude) || p.longitude < -180 || p.longitude > 180) {
|
|
return null;
|
|
}
|
|
if (p.accuracyMeters !== undefined && (typeof p.accuracyMeters !== 'number' || !Number.isFinite(p.accuracyMeters) || p.accuracyMeters < 0)) {
|
|
return null;
|
|
}
|
|
if (p.altitudeMeters !== undefined && (typeof p.altitudeMeters !== 'number' || !Number.isFinite(p.altitudeMeters))) {
|
|
return null;
|
|
}
|
|
if (p.headingDegrees !== undefined && (typeof p.headingDegrees !== 'number' || !Number.isFinite(p.headingDegrees) || p.headingDegrees < 0 || p.headingDegrees > 360)) {
|
|
return null;
|
|
}
|
|
if (p.speedMps !== undefined && (typeof p.speedMps !== 'number' || !Number.isFinite(p.speedMps) || p.speedMps < 0)) {
|
|
return null;
|
|
}
|
|
if (p.capturedAt !== undefined && (typeof p.capturedAt !== 'number' || !Number.isFinite(p.capturedAt) || p.capturedAt <= 0)) {
|
|
return null;
|
|
}
|
|
if (p.source !== undefined && !['gps', 'network', 'manual', 'unknown'].includes(String(p.source))) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
connectionId: p.connectionId,
|
|
latitude: p.latitude,
|
|
longitude: p.longitude,
|
|
accuracyMeters: p.accuracyMeters as number | undefined,
|
|
altitudeMeters: p.altitudeMeters as number | undefined,
|
|
headingDegrees: p.headingDegrees as number | undefined,
|
|
speedMps: p.speedMps as number | undefined,
|
|
source: p.source as NodeLocationSetParams['source'] | undefined,
|
|
capturedAt: p.capturedAt as number | undefined,
|
|
};
|
|
}
|
|
|
|
export function parseNodeLocationGetParams(params: unknown): NodeLocationGetParams | null {
|
|
if (!params || typeof params !== 'object') {
|
|
return null;
|
|
}
|
|
const p = params as Record<string, unknown>;
|
|
if (typeof p.connectionId !== 'string' || !p.connectionId.trim()) {
|
|
return null;
|
|
}
|
|
return {
|
|
connectionId: p.connectionId,
|
|
};
|
|
}
|
|
|
|
export function parseNodeStatusSetParams(params: unknown): NodeStatusSetParams | null {
|
|
if (!params || typeof params !== 'object') {
|
|
return null;
|
|
}
|
|
const p = params as Record<string, unknown>;
|
|
if (typeof p.connectionId !== 'string' || !p.connectionId.trim()) {
|
|
return null;
|
|
}
|
|
if (typeof p.platform !== 'string' || !['macos', 'ios', 'android', 'linux', 'windows', 'unknown'].includes(p.platform)) {
|
|
return null;
|
|
}
|
|
if (p.appVersion !== undefined && typeof p.appVersion !== 'string') {
|
|
return null;
|
|
}
|
|
if (p.deviceName !== undefined && typeof p.deviceName !== 'string') {
|
|
return null;
|
|
}
|
|
if (p.statusText !== undefined && typeof p.statusText !== 'string') {
|
|
return null;
|
|
}
|
|
if (p.batteryPct !== undefined && (typeof p.batteryPct !== 'number' || !Number.isFinite(p.batteryPct) || p.batteryPct < 0 || p.batteryPct > 100)) {
|
|
return null;
|
|
}
|
|
if (p.powerSource !== undefined && !['ac', 'battery', 'unknown'].includes(String(p.powerSource))) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
connectionId: p.connectionId,
|
|
platform: p.platform as NodeStatusSetParams['platform'],
|
|
appVersion: typeof p.appVersion === 'string' ? p.appVersion : undefined,
|
|
deviceName: typeof p.deviceName === 'string' ? p.deviceName : undefined,
|
|
statusText: typeof p.statusText === 'string' ? p.statusText : undefined,
|
|
batteryPct: typeof p.batteryPct === 'number' ? p.batteryPct : undefined,
|
|
powerSource: p.powerSource as NodeStatusSetParams['powerSource'] | undefined,
|
|
};
|
|
}
|
|
|
|
export function parseNodePushTokenSetParams(params: unknown): NodePushTokenSetParams | null {
|
|
if (!params || typeof params !== 'object') {
|
|
return null;
|
|
}
|
|
const p = params as Record<string, unknown>;
|
|
if (typeof p.connectionId !== 'string' || !p.connectionId.trim()) {
|
|
return null;
|
|
}
|
|
if (p.provider !== 'apns' && p.provider !== 'fcm') {
|
|
return null;
|
|
}
|
|
if (typeof p.token !== 'string' || p.token.trim().length < 16) {
|
|
return null;
|
|
}
|
|
if (p.topic !== undefined && typeof p.topic !== 'string') {
|
|
return null;
|
|
}
|
|
if (p.environment !== undefined && p.environment !== 'sandbox' && p.environment !== 'production') {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
connectionId: p.connectionId,
|
|
provider: p.provider,
|
|
token: p.token.trim(),
|
|
topic: typeof p.topic === 'string' ? p.topic.trim() : undefined,
|
|
environment: p.environment as NodePushTokenSetParams['environment'] | undefined,
|
|
};
|
|
}
|
|
|
|
export function makeResponse(id: number, result: unknown): GatewayResponse {
|
|
return { id, result };
|
|
}
|
|
|
|
export function makeError(id: number, code: ErrorCode, message: string): GatewayError {
|
|
return { id, error: { code, message } };
|
|
}
|
|
|
|
export function makeEvent(id: number, event: EventType, data: unknown): GatewayEvent {
|
|
return { id, event, data };
|
|
}
|