@ziggs-ai/api-client 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +20 -14
- package/src/ConnectionManager.ts +0 -172
- package/src/http/AgentSearchClient.ts +0 -115
- package/src/http/AgreementClient.ts +0 -721
- package/src/http/ArtifactsClient.ts +0 -133
- package/src/http/ChatClient.ts +0 -147
- package/src/http/ContextDiscoveryClient.ts +0 -52
- package/src/http/ContextReadClient.ts +0 -83
- package/src/http/MarketplaceClient.ts +0 -94
- package/src/http/MessagesClient.ts +0 -71
- package/src/http/ScopeClient.ts +0 -64
- package/src/http/TaskClient.ts +0 -450
- package/src/http/TelemetryClient.ts +0 -57
- package/src/http/index.ts +0 -26
- package/src/index.ts +0 -27
- package/src/shared/runtimeLog.ts +0 -68
- package/src/types.ts +0 -158
- package/src/utils/urlUtils.ts +0 -9
- package/src/websocket/ControlSocket.ts +0 -51
- package/src/websocket/WebSocketClient.ts +0 -315
- package/src/websocket/index.ts +0 -1
package/src/types.ts
DELETED
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
export class ApiError extends Error {
|
|
2
|
-
constructor(message: string, public readonly status: number, public readonly body?: string) {
|
|
3
|
-
super(message);
|
|
4
|
-
this.name = 'ApiError';
|
|
5
|
-
}
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export interface Creds {
|
|
9
|
-
operatorKey: string;
|
|
10
|
-
agentId: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export type TaskState =
|
|
14
|
-
| 'active'
|
|
15
|
-
| 'proposal'
|
|
16
|
-
| 'completed'
|
|
17
|
-
| 'failed'
|
|
18
|
-
| 'cancelled'
|
|
19
|
-
| 'ledger_open';
|
|
20
|
-
|
|
21
|
-
export type PlanStepStatus = 'pending' | 'in_progress' | 'completed' | 'skipped';
|
|
22
|
-
|
|
23
|
-
export interface PlanStep {
|
|
24
|
-
stepId: string;
|
|
25
|
-
status: PlanStepStatus;
|
|
26
|
-
result?: unknown;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface Task {
|
|
30
|
-
taskId: string;
|
|
31
|
-
description: string;
|
|
32
|
-
agentId?: string;
|
|
33
|
-
executorId?: string;
|
|
34
|
-
payerId?: string;
|
|
35
|
-
agreementId?: string;
|
|
36
|
-
agreement?: Agreement;
|
|
37
|
-
state: TaskState;
|
|
38
|
-
processing?: boolean;
|
|
39
|
-
deleted?: boolean;
|
|
40
|
-
result?: unknown;
|
|
41
|
-
errorMessage?: string;
|
|
42
|
-
createdAt?: string;
|
|
43
|
-
updatedAt?: string;
|
|
44
|
-
parentTaskId?: string;
|
|
45
|
-
rootTaskId?: string;
|
|
46
|
-
plan?: PlanStep[];
|
|
47
|
-
history?: unknown[];
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export type AgreementStatus = 'pending' | 'active' | 'fulfilled' | 'cancelled' | 'rejected' | (string & {});
|
|
51
|
-
export type ProposalStatus = 'pending' | 'approved' | 'rejected' | 'countered' | 'expired' | (string & {});
|
|
52
|
-
/** ⚠️ SYNC: backend src/agreements/agreements.constants.ts AGREEMENT_ENGAGEMENT_KIND */
|
|
53
|
-
export const AGREEMENT_ENGAGEMENT_KIND = {
|
|
54
|
-
HIRE: 'hire',
|
|
55
|
-
SERVICE: 'service',
|
|
56
|
-
} as const;
|
|
57
|
-
|
|
58
|
-
export type EngagementKind = (typeof AGREEMENT_ENGAGEMENT_KIND)[keyof typeof AGREEMENT_ENGAGEMENT_KIND];
|
|
59
|
-
|
|
60
|
-
export interface Agreement {
|
|
61
|
-
agreementId: string;
|
|
62
|
-
description?: string;
|
|
63
|
-
status?: AgreementStatus;
|
|
64
|
-
engagementKind?: EngagementKind;
|
|
65
|
-
proposalStatus?: ProposalStatus;
|
|
66
|
-
parties?: {
|
|
67
|
-
proposedTo?: string;
|
|
68
|
-
provider?: string;
|
|
69
|
-
payer?: string;
|
|
70
|
-
creator?: string;
|
|
71
|
-
creatorAgent?: string;
|
|
72
|
-
};
|
|
73
|
-
/** Legacy root-level price — always null in practice. Use money.price instead. */
|
|
74
|
-
price?: number | null;
|
|
75
|
-
/** Price and payment state. price is in cents (e.g. 6000 = 6000 KRW). */
|
|
76
|
-
money?: {
|
|
77
|
-
price?: number | null;
|
|
78
|
-
paymentStatus?: string;
|
|
79
|
-
};
|
|
80
|
-
lifecycle?: string;
|
|
81
|
-
expiresAt?: string;
|
|
82
|
-
maxExecutions?: number;
|
|
83
|
-
metadata?: Record<string, unknown>;
|
|
84
|
-
createdAt?: string;
|
|
85
|
-
updatedAt?: string;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** ⚠️ SYNC: Keep in sync with backend src/chat/chat.types.ts ENTRY_TYPE */
|
|
89
|
-
export const EntryTypes = {
|
|
90
|
-
MESSAGE: 'message',
|
|
91
|
-
NOTIFICATION: 'notification',
|
|
92
|
-
ARTIFACT: 'artifact',
|
|
93
|
-
AGREEMENT_HISTORY: 'agreement_history',
|
|
94
|
-
/** @deprecated legacy alias for AGREEMENT_HISTORY */
|
|
95
|
-
TASK_HISTORY: 'task_history',
|
|
96
|
-
} as const;
|
|
97
|
-
|
|
98
|
-
export type EntryType = (typeof EntryTypes)[keyof typeof EntryTypes];
|
|
99
|
-
|
|
100
|
-
/** ⚠️ SYNC: Keep in sync with backend src/chat/chat.types.ts CONTENT_TYPE */
|
|
101
|
-
export const ContentTypes = {
|
|
102
|
-
TEXT: 'text',
|
|
103
|
-
OPERATION: 'operation',
|
|
104
|
-
CONTEXT: 'context',
|
|
105
|
-
RESULT: 'result',
|
|
106
|
-
AGREEMENT_UPDATE: 'agreement_update',
|
|
107
|
-
TASK: 'task',
|
|
108
|
-
PROPOSAL: 'proposal',
|
|
109
|
-
THOUGHT: 'thought',
|
|
110
|
-
/** @deprecated legacy alias for AGREEMENT_UPDATE */
|
|
111
|
-
TASK_UPDATE: 'task_update',
|
|
112
|
-
} as const;
|
|
113
|
-
|
|
114
|
-
export type ContentType = (typeof ContentTypes)[keyof typeof ContentTypes];
|
|
115
|
-
|
|
116
|
-
const VALID_CONTENT_TYPES: Record<string, string[]> = {
|
|
117
|
-
[EntryTypes.MESSAGE]: [ContentTypes.TEXT],
|
|
118
|
-
[EntryTypes.NOTIFICATION]: [ContentTypes.TEXT, ContentTypes.TASK, ContentTypes.PROPOSAL],
|
|
119
|
-
[EntryTypes.ARTIFACT]: [ContentTypes.OPERATION, ContentTypes.CONTEXT, ContentTypes.RESULT, ContentTypes.TEXT, ContentTypes.THOUGHT],
|
|
120
|
-
[EntryTypes.AGREEMENT_HISTORY]: [ContentTypes.AGREEMENT_UPDATE, ContentTypes.TASK_UPDATE],
|
|
121
|
-
[EntryTypes.TASK_HISTORY]: [ContentTypes.AGREEMENT_UPDATE, ContentTypes.TASK_UPDATE],
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
/** ⚠️ SYNC: Mirrors backend src/chat/chat.types.ts isValidContentType */
|
|
125
|
-
export function isValidContentType(contentType: string, entryType: string): boolean {
|
|
126
|
-
return VALID_CONTENT_TYPES[entryType]?.includes(contentType) ?? false;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/** ⚠️ SYNC: Keep in sync with backend. This is the canonical SDK-side definition. */
|
|
130
|
-
export const OPEN_AGREEMENT_TARGET = 'everyone' as const;
|
|
131
|
-
|
|
132
|
-
export interface MessageMetadata {
|
|
133
|
-
chatId: string;
|
|
134
|
-
userId: string;
|
|
135
|
-
sender: { id: string; type?: string };
|
|
136
|
-
senderId: string;
|
|
137
|
-
senderType?: string;
|
|
138
|
-
receiver?: { id: string } | null;
|
|
139
|
-
receiverId?: string | null;
|
|
140
|
-
entryType?: string;
|
|
141
|
-
content_type?: string;
|
|
142
|
-
taskId?: string | null;
|
|
143
|
-
/**
|
|
144
|
-
* Optional task / agreement snapshot pinned by the backend on
|
|
145
|
-
* `task.notify.chat` / `agreement.notify.direct` traffic. The agent SDK's
|
|
146
|
-
* `normalizeIncomingEvent` reads `metadata.task` to upgrade structured
|
|
147
|
-
* task notifications into `task_result` events (so executors see
|
|
148
|
-
* `task-assigned` outcomes when the orchestrator calls `task_spawn`),
|
|
149
|
-
* and maps explicit wire `operation` + `agreementId` lifecycle fields
|
|
150
|
-
* into `agreement_lifecycle` events for proposal approve/reject.
|
|
151
|
-
*/
|
|
152
|
-
task?: Record<string, unknown> | null;
|
|
153
|
-
agreement?: Record<string, unknown> | null;
|
|
154
|
-
operation?: string | null;
|
|
155
|
-
agreementId?: string | null;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
export type MessageHandler = (text: string, metadata: MessageMetadata) => Promise<void>;
|
package/src/utils/urlUtils.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
export function getBackendUrl(): string {
|
|
2
|
-
const url = process.env.HTTP_URL || 'https://api.ziggsai.com';
|
|
3
|
-
return url.startsWith('http') ? url : `https://${url}`;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
export function getWebSocketUrl(): string {
|
|
7
|
-
const wsUrl = process.env.WS_URL || 'wss://api.ziggsai.com';
|
|
8
|
-
return wsUrl.startsWith('ws') ? wsUrl : `wss://${wsUrl}`;
|
|
9
|
-
}
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { io, type Socket } from 'socket.io-client';
|
|
2
|
-
import { runtimeLog } from '../shared/runtimeLog.js';
|
|
3
|
-
|
|
4
|
-
export interface ControlSocketOptions {
|
|
5
|
-
wsUrl: string;
|
|
6
|
-
operatorKey: string;
|
|
7
|
-
agentIds: () => string[];
|
|
8
|
-
onWake: (agentId: string) => void;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface ControlSocketHandle {
|
|
12
|
-
socket: Socket;
|
|
13
|
-
close: () => void;
|
|
14
|
-
resync: () => void;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function createControlSocket({ wsUrl, operatorKey, agentIds, onWake }: ControlSocketOptions): ControlSocketHandle {
|
|
18
|
-
if (!wsUrl || !operatorKey) throw new Error('[createControlSocket] wsUrl and operatorKey are required');
|
|
19
|
-
if (typeof agentIds !== 'function') throw new Error('[createControlSocket] agentIds must be a function returning string[]');
|
|
20
|
-
if (typeof onWake !== 'function') throw new Error('[createControlSocket] onWake must be a function');
|
|
21
|
-
|
|
22
|
-
const socket = io(wsUrl, {
|
|
23
|
-
auth: { token: `Bearer ${operatorKey}` },
|
|
24
|
-
query: { token: operatorKey, role: 'launcher' },
|
|
25
|
-
extraHeaders: { Authorization: `Bearer ${operatorKey}` },
|
|
26
|
-
transports: ['websocket'],
|
|
27
|
-
reconnection: true,
|
|
28
|
-
reconnectionDelay: 1000,
|
|
29
|
-
reconnectionDelayMax: 30000,
|
|
30
|
-
randomizationFactor: 0.5,
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
socket.on('connect', () => {
|
|
34
|
-
const ids = agentIds();
|
|
35
|
-
runtimeLog.info('ControlSocket', `connected (${socket.id}); registering ${ids.length} agent(s)`);
|
|
36
|
-
socket.emit('launcher:register', { agentIds: ids });
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
socket.on('launcher:wake', ({ agentId }: { agentId: string }) => {
|
|
40
|
-
if (agentId) onWake(agentId);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
socket.on('disconnect', (reason: string) => runtimeLog.debug('ControlSocket', `disconnected: ${reason}`));
|
|
44
|
-
socket.on('connect_error', (err: Error) => runtimeLog.warn('ControlSocket', `connect error: ${err.message}`));
|
|
45
|
-
|
|
46
|
-
return {
|
|
47
|
-
socket,
|
|
48
|
-
close: () => socket.disconnect(),
|
|
49
|
-
resync: () => { if (socket.connected) socket.emit('launcher:register', { agentIds: agentIds() }); },
|
|
50
|
-
};
|
|
51
|
-
}
|
|
@@ -1,315 +0,0 @@
|
|
|
1
|
-
import { io, type Socket } from 'socket.io-client';
|
|
2
|
-
import 'dotenv/config';
|
|
3
|
-
import { getWebSocketUrl } from '../utils/urlUtils.js';
|
|
4
|
-
import { runtimeLog } from '../shared/runtimeLog.js';
|
|
5
|
-
import { type MessageHandler, type MessageMetadata } from '../types.js';
|
|
6
|
-
|
|
7
|
-
export interface WebSocketClientOptions {
|
|
8
|
-
wsUrl?: string;
|
|
9
|
-
operatorKey?: string;
|
|
10
|
-
agentId?: string;
|
|
11
|
-
/**
|
|
12
|
-
* Optional user JWT (from `POST /users/login`). When set, the socket
|
|
13
|
-
* authenticates as that user — the gateway populates `socket.userId` and
|
|
14
|
-
* routes messages to them as a human user. Mutually exclusive with
|
|
15
|
-
* `operatorKey` + `agentId`. Used by the eval harness to seed chats as a
|
|
16
|
-
* real user instead of impersonating an agent.
|
|
17
|
-
*/
|
|
18
|
-
userToken?: string;
|
|
19
|
-
label?: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Mirrors the backend `ResourceEvent` shape. Emitted on the wire as
|
|
24
|
-
* `resource_changed` when a resource the agent has access to mutates.
|
|
25
|
-
*/
|
|
26
|
-
export interface ResourceEvent {
|
|
27
|
-
kind: 'message' | 'artifact' | 'task-state' | 'agreement';
|
|
28
|
-
ts: string;
|
|
29
|
-
resourceId: string;
|
|
30
|
-
chatId?: string;
|
|
31
|
-
agreementId?: string;
|
|
32
|
-
taskId?: string;
|
|
33
|
-
change?: 'created' | 'updated' | 'state-changed';
|
|
34
|
-
reason?: string;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export type ResourceEventHandler = (event: ResourceEvent) => void | Promise<void>;
|
|
38
|
-
|
|
39
|
-
export interface SendOptions {
|
|
40
|
-
entryType?: string;
|
|
41
|
-
content_type?: string;
|
|
42
|
-
contentType?: string;
|
|
43
|
-
taskId?: string;
|
|
44
|
-
/**
|
|
45
|
-
* When true, emits a 'chat:chunk' event instead of 'chat'.
|
|
46
|
-
* Use for streaming partial responses; send a final message
|
|
47
|
-
* (partial: false or omitted) to signal end of stream.
|
|
48
|
-
* Requires backend streaming support.
|
|
49
|
-
*/
|
|
50
|
-
partial?: boolean;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export class WebSocketClient {
|
|
54
|
-
private wsUrl: string;
|
|
55
|
-
private operatorKey: string | null;
|
|
56
|
-
private agentId: string | null;
|
|
57
|
-
private userToken: string | null;
|
|
58
|
-
private label: string;
|
|
59
|
-
private socket: Socket | null;
|
|
60
|
-
private messageHandler: MessageHandler | null;
|
|
61
|
-
private resourceEventHandler: ResourceEventHandler | null;
|
|
62
|
-
|
|
63
|
-
constructor(options: WebSocketClientOptions = {}) {
|
|
64
|
-
this.wsUrl = options.wsUrl || getWebSocketUrl();
|
|
65
|
-
this.userToken = options.userToken || null;
|
|
66
|
-
this.operatorKey = this.userToken ? null : (options.operatorKey || process.env.ZIGGS_OPERATOR_KEY || null);
|
|
67
|
-
this.agentId = this.userToken ? null : (options.agentId || null);
|
|
68
|
-
this.label = options.label || 'agent';
|
|
69
|
-
this.socket = null;
|
|
70
|
-
this.messageHandler = null;
|
|
71
|
-
this.resourceEventHandler = null;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
setMessageHandler(handler: MessageHandler): void {
|
|
75
|
-
this.messageHandler = handler;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Subscribe to `resource_changed` notifications — the unified push channel
|
|
80
|
-
* for non-message resource changes (artifact, task-state, agreement). The
|
|
81
|
-
* agent decides whether to pull the corresponding primitive based on
|
|
82
|
-
* `event.kind` + ids. Replaces cursor-based polling for delta detection.
|
|
83
|
-
*/
|
|
84
|
-
setResourceEventHandler(handler: ResourceEventHandler): void {
|
|
85
|
-
this.resourceEventHandler = handler;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
private _connect(): void {
|
|
89
|
-
if (this.socket?.connected) return;
|
|
90
|
-
|
|
91
|
-
runtimeLog.debug(this.label, `Connecting to ${this.wsUrl}`);
|
|
92
|
-
const socketOptions = this.buildSocketOptions();
|
|
93
|
-
this.socket = io(this.wsUrl, socketOptions);
|
|
94
|
-
|
|
95
|
-
this.socket.on('connect', () => {
|
|
96
|
-
runtimeLog.info(this.label, 'Connected');
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
this.socket.on('connect_error', (error: Error) => {
|
|
100
|
-
runtimeLog.error(this.label, `Connection error: ${error.message}`);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
this.socket.on('disconnect', (reason: string) => {
|
|
104
|
-
const reasonDescriptions: Record<string, string> = {
|
|
105
|
-
'io server disconnect': 'Server forcefully disconnected the client',
|
|
106
|
-
'io client disconnect': 'Client manually disconnected',
|
|
107
|
-
'ping timeout': 'Server did not respond to ping (connection timeout)',
|
|
108
|
-
'transport close': 'Connection closed by transport layer',
|
|
109
|
-
'transport error': 'Transport error occurred',
|
|
110
|
-
'parse error': 'Error parsing server message',
|
|
111
|
-
'forced close': 'Connection was forcibly closed',
|
|
112
|
-
'forced server close': 'Server forcibly closed the connection',
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
const description = reasonDescriptions[reason] || 'Unknown disconnect reason';
|
|
116
|
-
runtimeLog.debug(this.label, `Disconnected: ${reason} — ${description}`);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
// ZIG-207: subscribe to the canonical event name. Backend dual-emits
|
|
120
|
-
// `messages` + `chat:message:new` with byte-identical payloads, so
|
|
121
|
-
// listening to ONE of the two is exactly correct — listening to both
|
|
122
|
-
// would double-invoke handleIncomingMessage(). We pick the canonical
|
|
123
|
-
// namespaced name so once the sunset PR drops the legacy `messages`
|
|
124
|
-
// emit, no client change is needed.
|
|
125
|
-
this.socket.on('chat:message:new', async (payload: unknown) => {
|
|
126
|
-
try {
|
|
127
|
-
await this.handleIncomingMessage(payload);
|
|
128
|
-
} catch (error) {
|
|
129
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
130
|
-
runtimeLog.error(this.label, `Error handling incoming message: ${msg}`);
|
|
131
|
-
throw error;
|
|
132
|
-
}
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
this.socket.on('resource_changed', async (payload: unknown) => {
|
|
136
|
-
if (!this.resourceEventHandler) return;
|
|
137
|
-
try {
|
|
138
|
-
await this.resourceEventHandler(payload as ResourceEvent);
|
|
139
|
-
} catch (error) {
|
|
140
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
141
|
-
runtimeLog.error(this.label, `Error handling resource_changed: ${msg}`);
|
|
142
|
-
}
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
this.socket.on('error', (error: unknown) => {
|
|
146
|
-
runtimeLog.error(this.label, `Socket.IO error: ${String(error)}`);
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
connectAsync(timeout = 10_000): Promise<this> {
|
|
151
|
-
if (this.socket?.connected) return Promise.resolve(this);
|
|
152
|
-
return new Promise((resolve, reject) => {
|
|
153
|
-
this._connect();
|
|
154
|
-
// Only reject on timeout — not on the first connect_error. socket.io
|
|
155
|
-
// retries automatically (reconnectionAttempts: Infinity), so a single
|
|
156
|
-
// network hiccup or a briefly-down backend shouldn't crash the caller.
|
|
157
|
-
const timer = setTimeout(
|
|
158
|
-
() => reject(new Error(`[${this.label}] Connection timeout after ${timeout}ms`)),
|
|
159
|
-
timeout,
|
|
160
|
-
);
|
|
161
|
-
this.socket!.once('connect', () => { clearTimeout(timer); resolve(this); });
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
disconnect(): void {
|
|
166
|
-
if (this.socket) {
|
|
167
|
-
this.socket.disconnect();
|
|
168
|
-
this.socket = null;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
send(chatId: string, receiverId: string, content: string, options: SendOptions = {}): void {
|
|
173
|
-
if (!chatId || typeof chatId !== 'string') throw new Error('send: chatId is required');
|
|
174
|
-
if (!receiverId || typeof receiverId !== 'string') throw new Error('send: receiverId is required');
|
|
175
|
-
if (typeof content !== 'string' || content.trim().length === 0) throw new Error('send: content must be a non-empty string');
|
|
176
|
-
|
|
177
|
-
if (!this.socket) throw new Error('send: socket is not initialized. Call connect() first.');
|
|
178
|
-
if (!this.socket.connected) throw new Error('send: socket is not connected. Call connect() and wait for connection.');
|
|
179
|
-
|
|
180
|
-
const messageId = this.generateMessageId();
|
|
181
|
-
if (!messageId || typeof messageId !== 'string') throw new Error('send: failed to generate valid messageId');
|
|
182
|
-
|
|
183
|
-
const message: Record<string, unknown> = {
|
|
184
|
-
receiverId,
|
|
185
|
-
chatId,
|
|
186
|
-
messageId,
|
|
187
|
-
text: content,
|
|
188
|
-
entryType: options.entryType || 'message',
|
|
189
|
-
content_type: options.content_type || options.contentType || 'text',
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
const messagePreview = content.length > 100 ? content.substring(0, 100) + '...' : content;
|
|
193
|
-
// ZIG-207: emit the canonical namespaced inbound event. Backend
|
|
194
|
-
// registers `chat:message:send` as a one-line alias on `handleChat`,
|
|
195
|
-
// so this is byte-identical to the legacy `chat` emit. Streaming
|
|
196
|
-
// chunks keep their existing `chat:chunk` name (already namespaced).
|
|
197
|
-
const event = options.partial ? 'chat:chunk' : 'chat:message:send';
|
|
198
|
-
runtimeLog.debug(
|
|
199
|
-
this.label,
|
|
200
|
-
`Sending message (${event}) - chatId: ${chatId}, receiverId: ${receiverId}\n Message: ${messagePreview}`,
|
|
201
|
-
);
|
|
202
|
-
|
|
203
|
-
this.socket.emit(event, message);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
isConnected(): boolean {
|
|
207
|
-
return this.socket?.connected || false;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
async handleIncomingMessage(payload: unknown): Promise<void> {
|
|
211
|
-
if (!payload || typeof payload !== 'object') {
|
|
212
|
-
throw new Error('handleIncomingMessage: payload must be an object');
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
const p = payload as Record<string, unknown>;
|
|
216
|
-
|
|
217
|
-
if (!p['text'] || typeof p['text'] !== 'string' || (p['text'] as string).trim().length === 0) {
|
|
218
|
-
throw new Error('handleIncomingMessage: payload.text is required and must be a non-empty string');
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
if (!p['chatId'] || typeof p['chatId'] !== 'string') {
|
|
222
|
-
throw new Error('handleIncomingMessage: payload.chatId is required and must be a string');
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
const sender = p['sender'] as Record<string, unknown> | undefined;
|
|
226
|
-
const senderId = sender?.['id'];
|
|
227
|
-
const senderType = sender?.['type'] as string | undefined;
|
|
228
|
-
|
|
229
|
-
if (!senderId || typeof senderId !== 'string') {
|
|
230
|
-
throw new Error('handleIncomingMessage: payload.sender.id is required and must be a string');
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
runtimeLog.debug(
|
|
234
|
-
this.label,
|
|
235
|
-
`Received message - chatId: ${p['chatId']}, sender: ${senderId} (${senderType})`,
|
|
236
|
-
);
|
|
237
|
-
|
|
238
|
-
if (this.agentId && senderId === this.agentId) return;
|
|
239
|
-
|
|
240
|
-
if (!this.messageHandler) {
|
|
241
|
-
throw new Error('handleIncomingMessage: messageHandler is not set. Call setMessageHandler() first.');
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
const chatId = p['chatId'] as string;
|
|
245
|
-
const receiver = p['receiver'] as { id: string } | null | undefined;
|
|
246
|
-
const task = (p['task'] as Record<string, unknown> | null | undefined) ?? null;
|
|
247
|
-
const agreement = (p['agreement'] as Record<string, unknown> | null | undefined) ?? null;
|
|
248
|
-
const operation = (p['operation'] as string | null | undefined) ?? null;
|
|
249
|
-
const agreementId = (p['agreementId'] as string | null | undefined) ?? null;
|
|
250
|
-
runtimeLog.info(
|
|
251
|
-
this.label,
|
|
252
|
-
`[wire-debug] incoming chatId=${chatId} sender=${senderId} content_type=${p['content_type'] ?? p['contentType'] ?? '<none>'} entryType=${p['entryType'] ?? '<none>'} task?=${task ? `taskId=${task['taskId']} state=${task['state']}` : 'no'} agreement?=${agreement ? `agreementId=${agreement['agreementId']} status=${agreement['status']}` : 'no'} operation?=${operation ?? 'no'}`,
|
|
253
|
-
);
|
|
254
|
-
const metadata: MessageMetadata = {
|
|
255
|
-
chatId,
|
|
256
|
-
userId: senderId,
|
|
257
|
-
sender: { id: senderId, type: senderType },
|
|
258
|
-
senderId,
|
|
259
|
-
senderType,
|
|
260
|
-
receiver: receiver || null,
|
|
261
|
-
receiverId: receiver?.['id'] || null,
|
|
262
|
-
entryType: p['entryType'] as string | undefined,
|
|
263
|
-
content_type: (p['content_type'] ?? p['contentType']) as string | undefined,
|
|
264
|
-
taskId: (task?.['taskId'] as string | undefined) ?? null,
|
|
265
|
-
task,
|
|
266
|
-
agreement,
|
|
267
|
-
operation,
|
|
268
|
-
agreementId,
|
|
269
|
-
};
|
|
270
|
-
|
|
271
|
-
await this.messageHandler(p['text'] as string, metadata);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
private buildSocketOptions(): Parameters<typeof io>[1] {
|
|
275
|
-
// `forceNew: true` is required when multiple WebSocketClient instances
|
|
276
|
-
// hit the same URL with DIFFERENT auth. socket.io's default
|
|
277
|
-
// `multiplex: true` shares the engine.io Manager across `io()` calls
|
|
278
|
-
// to the same URL — and the Manager's auth is fixed by whoever opened
|
|
279
|
-
// it first. So a second client (e.g. an eval's user-mode connection
|
|
280
|
-
// alongside agent-host operator-key connections) would silently inherit
|
|
281
|
-
// the first client's auth and get rejected. Forcing a fresh Manager
|
|
282
|
-
// per client avoids that aliasing.
|
|
283
|
-
const options: Parameters<typeof io>[1] = {
|
|
284
|
-
transports: ['websocket'],
|
|
285
|
-
reconnection: true,
|
|
286
|
-
reconnectionAttempts: Infinity,
|
|
287
|
-
reconnectionDelay: 1000,
|
|
288
|
-
reconnectionDelayMax: 30000,
|
|
289
|
-
randomizationFactor: 0.5,
|
|
290
|
-
timeout: 20000,
|
|
291
|
-
autoConnect: true,
|
|
292
|
-
forceNew: true,
|
|
293
|
-
multiplex: false,
|
|
294
|
-
};
|
|
295
|
-
|
|
296
|
-
if (this.userToken) {
|
|
297
|
-
const bearer = `Bearer ${this.userToken}`;
|
|
298
|
-
options!.auth = { token: bearer };
|
|
299
|
-
options!.extraHeaders = { Authorization: bearer };
|
|
300
|
-
// No agentId — the gateway sees the `userId` claim and routes as a human user.
|
|
301
|
-
} else if (this.operatorKey) {
|
|
302
|
-
const bearer = `Bearer ${this.operatorKey}`;
|
|
303
|
-
options!.auth = { token: bearer };
|
|
304
|
-
options!.extraHeaders = { Authorization: bearer };
|
|
305
|
-
// agentId goes in query for fleet keys; agent-scoped keys omit it (ZIG-279).
|
|
306
|
-
if (this.agentId) options!.query = { agentId: this.agentId };
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
return options;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
private generateMessageId(): string {
|
|
313
|
-
return crypto.randomUUID();
|
|
314
|
-
}
|
|
315
|
-
}
|
package/src/websocket/index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { WebSocketClient } from './WebSocketClient.js';
|