@ziggs-ai/api-client 0.1.29 → 0.2.0

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.
@@ -1,268 +0,0 @@
1
- import { io } from 'socket.io-client';
2
- import 'dotenv/config';
3
- import { getWebSocketUrl } from '../utils/urlUtils.js';
4
- import { runtimeLog } from '../shared/runtimeLog.js';
5
- export class WebSocketClient {
6
- wsUrl;
7
- operatorKey;
8
- agentId;
9
- userToken;
10
- label;
11
- socket;
12
- messageHandler;
13
- resourceEventHandler;
14
- constructor(options = {}) {
15
- this.wsUrl = options.wsUrl || getWebSocketUrl();
16
- this.userToken = options.userToken || null;
17
- this.operatorKey = this.userToken ? null : (options.operatorKey || process.env.ZIGGS_OPERATOR_KEY || null);
18
- this.agentId = this.userToken ? null : (options.agentId || null);
19
- this.label = options.label || 'agent';
20
- this.socket = null;
21
- this.messageHandler = null;
22
- this.resourceEventHandler = null;
23
- }
24
- setMessageHandler(handler) {
25
- this.messageHandler = handler;
26
- }
27
- /**
28
- * Subscribe to `resource_changed` notifications — the unified push channel
29
- * for non-message resource changes (artifact, task-state, agreement). The
30
- * agent decides whether to pull the corresponding primitive based on
31
- * `event.kind` + ids. Replaces cursor-based polling for delta detection.
32
- */
33
- setResourceEventHandler(handler) {
34
- this.resourceEventHandler = handler;
35
- }
36
- connectHandlers = [];
37
- /**
38
- * Called on every socket connect, including socket.io reconnects.
39
- * Used for inbox catch-up (ZIG-454) so missed pushes are recovered from MongoDB.
40
- */
41
- onConnect(handler) {
42
- this.connectHandlers.push(handler);
43
- }
44
- async _emitConnect() {
45
- for (const handler of this.connectHandlers) {
46
- try {
47
- await handler();
48
- }
49
- catch (error) {
50
- const msg = error instanceof Error ? error.message : String(error);
51
- runtimeLog.warn(this.label, `connect handler error: ${msg}`);
52
- }
53
- }
54
- }
55
- _connect() {
56
- if (this.socket?.connected)
57
- return;
58
- runtimeLog.debug(this.label, `Connecting to ${this.wsUrl}`);
59
- const socketOptions = this.buildSocketOptions();
60
- this.socket = io(this.wsUrl, socketOptions);
61
- this.socket.on('connect', () => {
62
- runtimeLog.info(this.label, 'Connected');
63
- void this._emitConnect();
64
- });
65
- this.socket.on('connect_error', (error) => {
66
- runtimeLog.error(this.label, `Connection error: ${error.message}`);
67
- });
68
- this.socket.on('disconnect', (reason) => {
69
- const reasonDescriptions = {
70
- 'io server disconnect': 'Server forcefully disconnected the client',
71
- 'io client disconnect': 'Client manually disconnected',
72
- 'ping timeout': 'Server did not respond to ping (connection timeout)',
73
- 'transport close': 'Connection closed by transport layer',
74
- 'transport error': 'Transport error occurred',
75
- 'parse error': 'Error parsing server message',
76
- 'forced close': 'Connection was forcibly closed',
77
- 'forced server close': 'Server forcibly closed the connection',
78
- };
79
- const description = reasonDescriptions[reason] || 'Unknown disconnect reason';
80
- runtimeLog.debug(this.label, `Disconnected: ${reason} — ${description}`);
81
- });
82
- // ZIG-207: subscribe to the canonical event name. Backend dual-emits
83
- // `messages` + `chat:message:new` with byte-identical payloads, so
84
- // listening to ONE of the two is exactly correct — listening to both
85
- // would double-invoke handleIncomingMessage(). We pick the canonical
86
- // namespaced name so once the sunset PR drops the legacy `messages`
87
- // emit, no client change is needed.
88
- this.socket.on('chat:message:new', async (payload) => {
89
- try {
90
- await this.handleIncomingMessage(payload);
91
- }
92
- catch (error) {
93
- const msg = error instanceof Error ? error.message : String(error);
94
- runtimeLog.error(this.label, `Error handling incoming message: ${msg}`);
95
- throw error;
96
- }
97
- });
98
- this.socket.on('resource_changed', async (payload) => {
99
- if (!this.resourceEventHandler)
100
- return;
101
- try {
102
- await this.resourceEventHandler(payload);
103
- }
104
- catch (error) {
105
- const msg = error instanceof Error ? error.message : String(error);
106
- runtimeLog.error(this.label, `Error handling resource_changed: ${msg}`);
107
- }
108
- });
109
- this.socket.on('error', (error) => {
110
- runtimeLog.error(this.label, `Socket.IO error: ${String(error)}`);
111
- });
112
- // ZIG-860: the send path is fire-and-forget (no ack callback), so the
113
- // backend's structured rejection is the ONLY failure signal. Not listening
114
- // made every rejected send silent — an agent's message just vanished (the
115
- // a2a schema bug hid behind exactly this). Log loudly; delivery recovery
116
- // stays with the inbox/catch-up machinery.
117
- this.socket.on('chat:error', (payload) => {
118
- const p = (payload ?? {});
119
- runtimeLog.error(this.label, `chat:error [${p['code'] ?? 'UNKNOWN'}] chatId=${p['chatId'] ?? '-'} messageId=${p['messageId'] ?? '-'}: ${p['message'] ?? ''}`);
120
- });
121
- }
122
- connectAsync(timeout = 10_000) {
123
- if (this.socket?.connected)
124
- return Promise.resolve(this);
125
- return new Promise((resolve, reject) => {
126
- this._connect();
127
- // Only reject on timeout — not on the first connect_error. socket.io
128
- // retries automatically (reconnectionAttempts: Infinity), so a single
129
- // network hiccup or a briefly-down backend shouldn't crash the caller.
130
- const timer = setTimeout(() => reject(new Error(`[${this.label}] Connection timeout after ${timeout}ms`)), timeout);
131
- this.socket.once('connect', () => { clearTimeout(timer); resolve(this); });
132
- });
133
- }
134
- disconnect() {
135
- if (this.socket) {
136
- this.socket.disconnect();
137
- this.socket = null;
138
- }
139
- }
140
- send(chatId, receiverId, content, options = {}) {
141
- if (!chatId || typeof chatId !== 'string')
142
- throw new Error('send: chatId is required');
143
- if (!receiverId || typeof receiverId !== 'string')
144
- throw new Error('send: receiverId is required');
145
- if (typeof content !== 'string' || content.trim().length === 0)
146
- throw new Error('send: content must be a non-empty string');
147
- if (!this.socket)
148
- throw new Error('send: socket is not initialized. Call connect() first.');
149
- if (!this.socket.connected)
150
- throw new Error('send: socket is not connected. Call connect() and wait for connection.');
151
- const messageId = options.messageId ?? this.generateMessageId();
152
- if (!messageId || typeof messageId !== 'string')
153
- throw new Error('send: failed to generate valid messageId');
154
- const message = {
155
- receiverId,
156
- chatId,
157
- messageId,
158
- text: content,
159
- entryType: options.entryType || 'message',
160
- content_type: options.content_type || options.contentType || 'text',
161
- };
162
- const messagePreview = content.length > 100 ? content.substring(0, 100) + '...' : content;
163
- // ZIG-207: emit the canonical namespaced inbound event. Backend
164
- // registers `chat:message:send` as a one-line alias on `handleChat`,
165
- // so this is byte-identical to the legacy `chat` emit. Streaming
166
- // chunks keep their existing `chat:chunk` name (already namespaced).
167
- const event = options.partial ? 'chat:chunk' : 'chat:message:send';
168
- runtimeLog.debug(this.label, `Sending message (${event}) - chatId: ${chatId}, receiverId: ${receiverId}\n Message: ${messagePreview}`);
169
- this.socket.emit(event, message);
170
- }
171
- isConnected() {
172
- return this.socket?.connected || false;
173
- }
174
- async handleIncomingMessage(payload) {
175
- if (!payload || typeof payload !== 'object') {
176
- throw new Error('handleIncomingMessage: payload must be an object');
177
- }
178
- const p = payload;
179
- if (!p['text'] || typeof p['text'] !== 'string' || p['text'].trim().length === 0) {
180
- throw new Error('handleIncomingMessage: payload.text is required and must be a non-empty string');
181
- }
182
- if (!p['chatId'] || typeof p['chatId'] !== 'string') {
183
- throw new Error('handleIncomingMessage: payload.chatId is required and must be a string');
184
- }
185
- const sender = p['sender'];
186
- const senderId = sender?.['id'];
187
- const senderType = sender?.['type'];
188
- if (!senderId || typeof senderId !== 'string') {
189
- throw new Error('handleIncomingMessage: payload.sender.id is required and must be a string');
190
- }
191
- runtimeLog.debug(this.label, `Received message - chatId: ${p['chatId']}, sender: ${senderId} (${senderType})`);
192
- if (this.agentId && senderId === this.agentId)
193
- return;
194
- if (!this.messageHandler) {
195
- throw new Error('handleIncomingMessage: messageHandler is not set. Call setMessageHandler() first.');
196
- }
197
- const chatId = p['chatId'];
198
- const receiver = p['receiver'];
199
- const task = p['task'] ?? null;
200
- const agreement = p['agreement'] ?? null;
201
- const operation = p['operation'] ?? null;
202
- const agreementId = p['agreementId'] ?? null;
203
- runtimeLog.info(this.label, `[wire-debug] incoming chatId=${chatId} sender=${senderId} content_type=${p['content_type'] ?? p['contentType'] ?? '<none>'} entryType=${p['entryType'] ?? '<none>'} ts=${typeof p['timestamp']}/${typeof p['sentTimestamp']} keys=${Object.keys(p).join(',')} task?=${task ? `taskId=${task['taskId']} state=${task['state']}` : 'no'} agreement?=${agreement ? `agreementId=${agreement['agreementId']} status=${agreement['status']}` : 'no'} operation?=${operation ?? 'no'}`);
204
- const metadata = {
205
- chatId,
206
- messageId: typeof p['messageId'] === 'string' ? p['messageId'] : undefined,
207
- userId: senderId,
208
- sender: { id: senderId, type: senderType },
209
- senderId,
210
- senderType,
211
- receiver: receiver || null,
212
- receiverId: receiver?.['id'] || null,
213
- entryType: p['entryType'],
214
- content_type: (p['content_type'] ?? p['contentType']),
215
- taskId: task?.['taskId'] ?? null,
216
- task,
217
- agreement,
218
- operation,
219
- agreementId,
220
- // ZIG-860: carry the send time through — the live-path watermark ack
221
- // (AgentHost, ZIG-832 P4) reads it, and dropping it here meant the ack
222
- // never fired for any live message.
223
- timestamp: typeof p['timestamp'] === 'string' ? p['timestamp'] : undefined,
224
- sentTimestamp: typeof p['sentTimestamp'] === 'string' ? p['sentTimestamp'] : undefined,
225
- };
226
- await this.messageHandler(p['text'], metadata);
227
- }
228
- buildSocketOptions() {
229
- // `forceNew: true` is required when multiple WebSocketClient instances
230
- // hit the same URL with DIFFERENT auth. socket.io's default
231
- // `multiplex: true` shares the engine.io Manager across `io()` calls
232
- // to the same URL — and the Manager's auth is fixed by whoever opened
233
- // it first. So a second client (e.g. an eval's user-mode connection
234
- // alongside agent-host operator-key connections) would silently inherit
235
- // the first client's auth and get rejected. Forcing a fresh Manager
236
- // per client avoids that aliasing.
237
- const options = {
238
- transports: ['websocket'],
239
- reconnection: true,
240
- reconnectionAttempts: Infinity,
241
- reconnectionDelay: 1000,
242
- reconnectionDelayMax: 30000,
243
- randomizationFactor: 0.5,
244
- timeout: 20000,
245
- autoConnect: true,
246
- forceNew: true,
247
- multiplex: false,
248
- };
249
- if (this.userToken) {
250
- const bearer = `Bearer ${this.userToken}`;
251
- options.auth = { token: bearer };
252
- options.extraHeaders = { Authorization: bearer };
253
- // No agentId — the gateway sees the `userId` claim and routes as a human user.
254
- }
255
- else if (this.operatorKey) {
256
- const bearer = `Bearer ${this.operatorKey}`;
257
- options.auth = { token: bearer };
258
- options.extraHeaders = { Authorization: bearer };
259
- // agentId goes in query for fleet keys; agent-scoped keys omit it (ZIG-279).
260
- if (this.agentId)
261
- options.query = { agentId: this.agentId };
262
- }
263
- return options;
264
- }
265
- generateMessageId() {
266
- return crypto.randomUUID();
267
- }
268
- }
@@ -1 +0,0 @@
1
- export { WebSocketClient } from './WebSocketClient.js';
@@ -1 +0,0 @@
1
- export { WebSocketClient } from './WebSocketClient.js';